techfreakworm commited on
Commit
2d0e666
·
unverified ·
1 Parent(s): 5ae7d55

fix(ui): full Brutalist Mono retheme — IBM Plex, padding crush, scoped selectors

Browse files

Six issues from the user's manual mobile test:

1. Bluish background tint despite earlier --neutral-* override. Cause:
Gradio applies its CSS in multiple :root/.dark contexts. Solution:
override the neutral scale + body/block/input fill custom properties
under :root, html.dark, .gradio-container, .gradio-container.dark
AND .gradio-container .dark (all five contexts).

2. Huge ~60 px horizontal margins on the 360 px viewport. Cause:
.gradio-container > .app ships with padding: 16px 32px. Override
to 16px 20px on desktop and 8px 10px on mobile so the form actually
uses the available width.

3. Field labels rendering at 14 px (~24 visual px on phone) with
default Inter. Replace Inter with IBM Plex Sans (body) + IBM Plex
Mono (labels, status, metadata) — both via Google Fonts, both
distinctive enough to escape generic AI-slop aesthetics per the
frontend-design discipline. Shrink labels to 10 px desktop / 9 px
mobile, uppercase tracked mono, muted ink.

4. Helper text (gr.Textbox info=...) was getting uppercased + mono
because the parent <label> rule cascaded text-transform via CSS
inheritance. Fix: scope label styling to ONLY the span.has-info
inner element, and add an explicit reset on .info-text (Gradio
6.14's actual class for helper text — found via Playwright DOM
inspect, not [data-testid="info"] as the older versions used).
Force info-text to sentence-case sans italic small muted.

5. Vocal mode radio pills oversized. Add a dedicated rule for
.ams-content .wrap > label (the gr.Radio option) — compact 7 12 px
padding, sans 12 px, border highlight on :checked, custom radio
dot via appearance:none + radial gradient.

6. Output/Metadata blocks were unstyled containers (just a tiny label
+ floating empty-state glyph). Added elem_classes=["ams-out",
"ams-out-audio"|"ams-out-meta"] in ui.py and dedicated CSS so
each component renders as a proper bordered panel with min-height,
uppercase mono label, and muted empty-state glyph.

Also: forced nested .ams-tab-pane rows to flex-direction:column on
< 768 px (Gradio's gr.Row stays as a flex row by default, which
broke the stacked form/output layout on mobile).

Verified at 360 (phone) and 1440 (desktop) via Playwright screenshots
against docs/superpowers/specs/mockups/01_generate_mobile_errors.html.
17/17 L1+L2 tests still pass; ruff clean.

Typography upgrade also documented in theme.py module docstring.

Files changed (2) hide show
  1. theme.py +426 -173
  2. ui.py +9 -1
theme.py CHANGED
@@ -1,20 +1,21 @@
1
  """Brutalist Mono — pure black/white, no color accent.
2
 
3
- Palette tokens are the source of truth; CSS pulls from them. The audio
4
- waveform is the only optionally-colored element (rendered white in v1).
5
-
6
- UI architecture (locked):
7
- - Sidebar layout (NOT ``gr.Tabs``) per wireframes at
8
- ``docs/superpowers/specs/mockups/``.
9
- - ``.ams-sidebar`` is a flex column at desktop, fixed-width 170-190 px.
10
- - ``.ams-side-radio`` is the mode-nav: a ``gr.Radio`` re-skinned via CSS
11
- so each option renders as a full-width sidebar pill. The native
12
- ``:checked`` pseudo-class supplies the "active" highlight.
13
- - ``.ams-content`` is the right column containing 5 ``.ams-tab-pane``
14
- groups; one is visible at a time.
15
- - Media queries: at ``<= 1024 px`` the sidebar shrinks to an icon rail.
16
- At ``<= 640 px`` the sidebar is replaced by a horizontal scroll strip
17
- at the top.
 
18
  """
19
 
20
  from __future__ import annotations
@@ -25,50 +26,84 @@ import gradio as gr
25
  BG = "#0A0A0A"
26
  SURFACE = "#141414"
27
  SURFACE_STRONG = "#000000"
 
28
  BORDER = "#1F1F1F"
29
  BORDER_STRONG = "#2A2A2A"
30
  INK = "#E5E5E5"
31
  INK_MUTED = "#6B6B6B"
 
32
  PRIMARY = "#FFFFFF"
33
- ERROR_BG = "#1A1A1A"
34
  HOVER_BG = "#1A1A1A"
35
- RADIUS = "6px"
36
- FONT_STACK = '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'
 
37
 
38
 
39
  def build_theme() -> gr.themes.Base:
40
- """Returns a Gradio theme keyed to Brutalist Mono tokens."""
 
 
 
 
41
  return gr.themes.Base(
42
  primary_hue=gr.themes.colors.gray,
43
  neutral_hue=gr.themes.colors.gray,
44
- font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
 
 
 
 
 
 
 
 
 
45
  ).set(
46
  body_background_fill=BG,
47
  body_text_color=INK,
 
 
48
  block_background_fill=SURFACE,
49
  block_border_color=BORDER,
50
  block_border_width="1px",
51
  block_radius=RADIUS,
 
 
 
 
52
  input_background_fill=SURFACE_STRONG,
53
- input_border_color=BORDER_STRONG,
54
  input_border_color_focus=PRIMARY,
 
55
  button_primary_background_fill=PRIMARY,
56
  button_primary_text_color=BG,
57
  button_primary_background_fill_hover=PRIMARY,
58
  button_secondary_background_fill=SURFACE_STRONG,
59
  button_secondary_text_color=INK,
60
  button_secondary_border_color=BORDER_STRONG,
 
 
 
 
61
  )
62
 
63
 
 
 
 
64
  CSS = f"""
65
- /* === Override Gradio's Tailwind-slate "neutral" palette ================
66
- Gradio's gr.themes.colors.gray is actually Tailwind slate-* which has
67
- a noticeable blue tint (--neutral-900 = #111827, --neutral-950 = #0b0f19).
68
- On phone displays with cool color temperatures this reads as bluish-grey
69
- and contradicts the Brutalist Mono spec. Force true-neutral hex values. */
70
- :root, html.dark {{
71
- --neutral-50: #FAFAFA !important;
 
 
 
 
 
72
  --neutral-100: #F5F5F5 !important;
73
  --neutral-200: #E5E5E5 !important;
74
  --neutral-300: #D4D4D4 !important;
@@ -79,64 +114,108 @@ CSS = f"""
79
  --neutral-800: #262626 !important;
80
  --neutral-900: #141414 !important;
81
  --neutral-950: #0A0A0A !important;
82
- --body-background-fill: {BG} !important;
83
- --background-fill-primary: {SURFACE} !important;
84
- --background-fill-secondary: {SURFACE_STRONG} !important;
85
- --block-background-fill: {SURFACE} !important;
86
- --block-label-background-fill: transparent !important;
87
- --block-title-background-fill: transparent !important;
88
- --input-background-fill: {SURFACE_STRONG} !important;
89
- --border-color-primary: {BORDER} !important;
90
- --border-color-accent: {BORDER_STRONG} !important;
91
- --color-accent: {PRIMARY} !important;
92
- }}
93
-
94
- /* === Body chrome ======================================================= */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  .ams-header {{
96
  display:flex; justify-content:space-between; align-items:baseline;
97
- padding:10px 4px 6px 4px;
98
  }}
99
  .ams-brand {{
100
- font-size:18px; font-weight:600; letter-spacing:-0.01em; color:{INK};
 
 
101
  }}
102
- .ams-brand-period {{ color:{PRIMARY}; }}
103
  .ams-status {{
104
- font-size:11px; color:{INK_MUTED};
105
- letter-spacing:0.06em; text-transform:uppercase;
 
106
  }}
107
 
108
  .ams-cta {{
109
- font-size:13px; color:{INK_MUTED};
110
- margin:2px 4px 14px 4px; padding-bottom:12px;
111
  border-bottom:1px solid {BORDER};
 
112
  }}
113
- .ams-cta strong {{ color:{INK}; }}
114
  .ams-cta-heart {{ color:{PRIMARY}; }}
115
- .ams-cta a {{ color:{INK}; text-decoration:underline; }}
116
 
117
- /* === Body row (sidebar + content) ====================================== */
 
 
118
  .ams-body {{
119
- gap:16px !important;
120
  align-items:stretch !important;
121
  }}
122
 
123
- /* === Sidebar (desktop >= 1024px) ======================================= */
 
 
124
  .ams-sidebar {{
125
  background:{SURFACE_STRONG} !important;
126
- padding:14px 8px !important;
127
  border-radius:{RADIUS} !important;
128
  border:1px solid {BORDER} !important;
129
- min-width:190px;
130
  max-width:210px;
131
  }}
132
 
133
- /* --- Mode radio (re-skin gr.Radio as a vertical sidebar nav) ----------- */
134
  .ams-side-radio {{
135
  background:transparent !important;
136
  border:none !important;
137
  padding:0 !important;
138
  width:100%;
139
  }}
 
140
  .ams-side-radio .wrap {{
141
  display:flex !important;
142
  flex-direction:column !important;
@@ -144,19 +223,20 @@ CSS = f"""
144
  background:transparent !important;
145
  border:none !important;
146
  }}
147
- /* Each radio option becomes a sidebar pill */
148
  .ams-side-radio label {{
149
  display:flex !important;
150
  align-items:center !important;
151
- padding:9px 12px !important;
152
  margin:0 !important;
153
- border-radius:4px !important;
154
  border:none !important;
155
  border-left:2px solid transparent !important;
156
  background:transparent !important;
157
  color:{INK_MUTED} !important;
158
- font-size:13px !important;
 
159
  font-weight:500 !important;
 
160
  cursor:pointer !important;
161
  transition:background 80ms ease, color 80ms ease, border-color 80ms ease;
162
  min-height:0 !important;
@@ -167,11 +247,9 @@ CSS = f"""
167
  background:{HOVER_BG} !important;
168
  color:{INK} !important;
169
  }}
170
- /* Hide the native radio circle */
171
  .ams-side-radio label input[type="radio"] {{
172
  display:none !important;
173
  }}
174
- /* Active state: white text + white left border + dark bg */
175
  .ams-side-radio label.selected,
176
  .ams-side-radio label:has(input[type="radio"]:checked) {{
177
  background:{HOVER_BG} !important;
@@ -179,32 +257,35 @@ CSS = f"""
179
  border-left-color:{PRIMARY} !important;
180
  font-weight:600 !important;
181
  }}
182
- /* Hide the (now-empty) form-element-info row that gr.Radio injects */
183
  .ams-side-radio + div:empty {{ display:none !important; }}
184
 
185
- /* --- Session history block (below the mode radio) --------------------- */
186
  .ams-history {{
187
- margin-top:14px;
188
- padding-top:10px;
189
  border-top:1px solid {BORDER};
190
  }}
191
  .ams-history-title {{
192
- font-size:10px; color:{INK_MUTED};
193
- letter-spacing:0.1em; text-transform:uppercase;
 
194
  padding:0 4px 6px 4px;
195
  }}
196
  .ams-history-empty {{
197
- font-size:11px; color:#3F3F3F;
 
198
  font-style:italic;
199
- padding:6px 4px;
200
  }}
201
 
202
- /* === Content area ====================================================== */
 
 
203
  .ams-content {{
204
  background:{SURFACE} !important;
205
  border:1px solid {BORDER} !important;
206
  border-radius:{RADIUS} !important;
207
- padding:16px !important;
208
  min-height:540px;
209
  }}
210
  .ams-tab-pane {{
@@ -212,172 +293,344 @@ CSS = f"""
212
  border:none !important;
213
  padding:0 !important;
214
  }}
215
-
216
- /* === LoRA chip pill (used in M2+) ====================================== */
217
- .ams-chip {{
218
- display:inline-block; padding:5px 10px; border-radius:14px;
219
- font-size:11px; margin:0 5px 5px 0; background:{SURFACE_STRONG};
220
- border:1px solid {BORDER_STRONG}; color:{INK_MUTED}; cursor:pointer;
 
221
  }}
222
- .ams-chip.on {{ border-color:{PRIMARY}; color:{PRIMARY}; }}
223
- .ams-chip.upload {{ border-style:dashed; color:{PRIMARY}; }}
224
 
225
- /* === LoRA file drop zone (tighten Gradio default ~400px height) ======== */
226
- .ams-lora-file .upload-container {{ min-height:56px !important; }}
227
-
228
- /* === Hide Gradio footer ================================================ */
229
- footer {{ display:none !important; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
- /* === Tighten Gradio chrome (narrow scope to avoid breaking output) =====
232
- Only sharpen the INPUT control surfaces (textareas, number inputs)
233
- so they read as crisp Brutalist Mono panels. Do NOT touch generic
234
- ``.block`` padding — that collapses gr.Audio / gr.JSON which need
235
- their own internal spacing. */
236
  .ams-content textarea,
237
  .ams-content input[type="text"],
238
  .ams-content input[type="number"] {{
239
  background:{SURFACE_STRONG} !important;
240
  border:1px solid {BORDER} !important;
241
- border-radius:4px !important;
242
  color:{INK} !important;
243
- padding:10px !important;
 
 
 
244
  }}
245
  .ams-content textarea:focus,
246
  .ams-content input[type="text"]:focus,
247
  .ams-content input[type="number"]:focus {{
248
  outline:none !important;
249
  border-color:{PRIMARY} !important;
 
 
 
 
 
250
  }}
251
  .ams-content input[type="range"] {{
252
  accent-color:{PRIMARY} !important;
253
  }}
254
- /* Component labels — uppercase, muted, no shadow.
255
- ``.gradio-container .block .label`` already covers this from the
256
- earlier component-label rule, but restating for the form blocks
257
- specifically gives the wireframe a consistent label scale. */
258
- .ams-content .label-wrap,
259
- .ams-content [data-testid="block-label"] {{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  font-size:10px !important;
261
  letter-spacing:0.08em !important;
262
  text-transform:uppercase !important;
263
  color:{INK_MUTED} !important;
264
  background:transparent !important;
265
- border:none !important;
266
- padding:0 0 4px 0 !important;
 
 
 
 
 
 
267
  }}
268
 
269
- /* === Component labels kill the white pill, make them inline muted ==== */
270
- /* Gradio renders component labels (e.g. gr.Audio "Output", gr.Code
271
- "Metadata") as elevated white-pill blocks by default. The Brutalist
272
- Mono theme wants them as plain muted-ink inline text. */
273
- .gradio-container .block .label,
274
- .gradio-container [data-testid="block-label"],
275
- .gradio-container span.label-wrap > span {{
276
- background:transparent !important;
277
- color:{INK_MUTED} !important;
278
- font-size:10px !important;
279
- font-weight:500 !important;
280
- letter-spacing:0.06em !important;
281
- text-transform:uppercase !important;
282
- border:none !important;
283
- box-shadow:none !important;
284
- padding:4px 0 !important;
285
  }}
286
- .gradio-container [data-testid="block-label"] svg,
287
- .gradio-container .label svg {{
288
- display:none !important; /* drop the music-note / code glyph next to label */
 
 
 
 
 
289
  }}
 
 
 
290
 
291
- /* === Responsive: keep full sidebar down to mobile threshold ============ */
292
- /* The previous tablet "icon rail" mode at 640-1024 px relied on
293
- ``::first-letter`` to keep the emoji visible while hiding the label
294
- text, but Gradio wraps the radio option text in a <span> so the
295
- pseudo-class never hits the emoji. Rather than fight the DOM, we keep
296
- the full sidebar at all widths >= 640 px and switch to a stacked
297
- layout below that. */
 
 
 
 
 
 
 
298
 
299
- /* === Responsive: mobile < 640 px ======================================= */
 
 
 
 
300
  @media (max-width: 640px) {{
301
- /* Stack body so sidebar (now a tab strip) sits above content */
302
  .ams-body {{
303
  flex-direction:column !important;
304
  gap:8px !important;
305
  }}
306
-
307
- /* Sidebar = horizontal scroll strip. Strip its desktop chrome
308
- (border, large padding, fixed width) so it reads as a tab bar. */
309
  .ams-sidebar {{
310
  min-width:100% !important;
311
  max-width:100% !important;
312
- padding:2px !important;
313
  border:none !important;
314
  background:transparent !important;
315
  border-radius:0 !important;
316
  }}
317
-
318
- /* The radio's outer block (gr.Radio with container=False still gets
319
- padding from Gradio's base styles). Flatten it. */
320
- .ams-side-radio {{
321
- padding:0 !important;
322
- background:transparent !important;
323
- }}
324
-
325
- /* Real options live in the second .wrap (Gradio renders an extra
326
- hidden one first); both flex-row + overflow + nowrap.
327
- CRITICAL: override the desktop label width:100% — that's what
328
- makes labels stack vertically inside a flex-row container.
329
- flex-wrap:nowrap forces a single row + horizontal scroll instead
330
- of wrapping to 2 rows. */
331
  .ams-side-radio .wrap {{
332
  flex-direction:row !important;
333
  flex-wrap:nowrap !important;
334
  overflow-x:auto !important;
335
  overflow-y:hidden !important;
336
  gap:6px !important;
337
- padding-bottom:2px !important;
338
- /* Hide scrollbar but keep scrolling */
339
  scrollbar-width:none !important;
340
  -ms-overflow-style:none !important;
341
  }}
342
- .ams-side-radio .wrap::-webkit-scrollbar {{
343
- display:none !important;
344
- }}
345
-
346
  .ams-side-radio label {{
347
- /* Compact pill: just enough room for emoji + label, no flex-grow */
348
  width:auto !important;
349
  min-width:0 !important;
350
  max-width:max-content !important;
351
  flex:0 0 auto !important;
 
352
  font-size:11px !important;
353
  font-weight:600 !important;
354
  white-space:nowrap !important;
355
- padding:8px 12px !important;
356
- /* Bottom border instead of left border for the horizontal context */
357
- border-left:none !important;
358
- border-bottom:2px solid transparent !important;
359
- border-radius:4px !important;
360
- justify-content:center !important;
361
  background:{SURFACE_STRONG} !important;
362
- border-top:1px solid {BORDER} !important;
363
- border-right:1px solid {BORDER} !important;
364
- border-left:1px solid {BORDER} !important;
365
  }}
366
  .ams-side-radio label.selected,
367
  .ams-side-radio label:has(input[type="radio"]:checked) {{
368
- border-left-color:transparent !important;
369
- border-bottom-color:{PRIMARY} !important;
370
- background:{HOVER_BG} !important;
371
  }}
372
-
373
- /* History block off-screen on mobile (already display:none on tablet+;
374
- restate here in case the cascade gets weird) */
375
  .ams-history {{ display:none !important; }}
376
 
377
- /* Tighter chrome */
378
- .ams-header {{ padding:6px 2px 2px 2px !important; }}
379
- .ams-brand {{ font-size:15px !important; }}
380
- .ams-cta {{ font-size:11px !important; padding-bottom:8px !important; margin-bottom:8px !important; }}
381
- .ams-content {{ padding:12px !important; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }}
383
  """
 
1
  """Brutalist Mono — pure black/white, no color accent.
2
 
3
+ The aesthetic identity of ACE Music Studio: precision, density, mono-
4
+ spaced labels, true neutral grays, and a single white accent rationed
5
+ across active states + the primary CTA. Inspired by record-sleeve
6
+ liner notes and a code editor's chrome.
7
+
8
+ Typography (selected per the frontend-design discipline):
9
+ - IBM Plex Sans — body, brand, helper text. Mechanically refined,
10
+ warmer than Inter, free via Google Fonts.
11
+ - IBM Plex Mono — labels, status indicator, brand period, metadata
12
+ JSON, all-caps small text. Distinctive character without going
13
+ novelty.
14
+
15
+ The wireframes at ``docs/superpowers/specs/mockups/`` remain the visual
16
+ source of truth. The mobile breakpoint at 640 px replaces the sidebar
17
+ with a horizontal scroll strip and crushes the outer Gradio padding so
18
+ the full 360 px viewport is actually usable.
19
  """
20
 
21
  from __future__ import annotations
 
26
  BG = "#0A0A0A"
27
  SURFACE = "#141414"
28
  SURFACE_STRONG = "#000000"
29
+ SURFACE_RAISED = "#1A1A1A"
30
  BORDER = "#1F1F1F"
31
  BORDER_STRONG = "#2A2A2A"
32
  INK = "#E5E5E5"
33
  INK_MUTED = "#6B6B6B"
34
+ INK_FAINT = "#3F3F3F"
35
  PRIMARY = "#FFFFFF"
 
36
  HOVER_BG = "#1A1A1A"
37
+ RADIUS = "4px"
38
+ FONT_SANS = '"IBM Plex Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif'
39
+ FONT_MONO = '"IBM Plex Mono", "JetBrains Mono", ui-monospace, Menlo, monospace'
40
 
41
 
42
  def build_theme() -> gr.themes.Base:
43
+ """Returns a Gradio theme keyed to Brutalist Mono tokens.
44
+
45
+ Uses IBM Plex Sans as the body font and IBM Plex Mono as the
46
+ monospace partner. Both are pulled from Google Fonts at runtime.
47
+ """
48
  return gr.themes.Base(
49
  primary_hue=gr.themes.colors.gray,
50
  neutral_hue=gr.themes.colors.gray,
51
+ font=[
52
+ gr.themes.GoogleFont("IBM Plex Sans"),
53
+ "system-ui",
54
+ "sans-serif",
55
+ ],
56
+ font_mono=[
57
+ gr.themes.GoogleFont("IBM Plex Mono"),
58
+ "ui-monospace",
59
+ "monospace",
60
+ ],
61
  ).set(
62
  body_background_fill=BG,
63
  body_text_color=INK,
64
+ background_fill_primary=BG,
65
+ background_fill_secondary=SURFACE,
66
  block_background_fill=SURFACE,
67
  block_border_color=BORDER,
68
  block_border_width="1px",
69
  block_radius=RADIUS,
70
+ block_label_text_color=INK_MUTED,
71
+ block_label_background_fill="transparent",
72
+ block_title_background_fill="transparent",
73
+ block_title_text_color=INK_MUTED,
74
  input_background_fill=SURFACE_STRONG,
75
+ input_border_color=BORDER,
76
  input_border_color_focus=PRIMARY,
77
+ input_placeholder_color=INK_FAINT,
78
  button_primary_background_fill=PRIMARY,
79
  button_primary_text_color=BG,
80
  button_primary_background_fill_hover=PRIMARY,
81
  button_secondary_background_fill=SURFACE_STRONG,
82
  button_secondary_text_color=INK,
83
  button_secondary_border_color=BORDER_STRONG,
84
+ border_color_primary=BORDER,
85
+ border_color_accent=BORDER_STRONG,
86
+ color_accent=PRIMARY,
87
+ color_accent_soft=SURFACE_RAISED,
88
  )
89
 
90
 
91
+ # Note: all `!important` below is intentional — Gradio's svelte-hashed
92
+ # classes load AFTER our CSS in some browsers, and the framework's
93
+ # default rules sit at the same specificity as ours without it.
94
  CSS = f"""
95
+ /* ============================================================
96
+ * Brutalist Mono global palette + Gradio variable overrides
97
+ * Gradio's gr.themes.colors.gray maps to Tailwind slate-*, which has
98
+ * a perceptible blue tint on cool-temperature phone displays. Pin
99
+ * every neutral + surface variable to true monochrome hex.
100
+ * ============================================================ */
101
+ :root,
102
+ html.dark,
103
+ .gradio-container,
104
+ .gradio-container.dark,
105
+ .gradio-container .dark {{
106
+ --neutral-50: #FAFAFA !important;
107
  --neutral-100: #F5F5F5 !important;
108
  --neutral-200: #E5E5E5 !important;
109
  --neutral-300: #D4D4D4 !important;
 
114
  --neutral-800: #262626 !important;
115
  --neutral-900: #141414 !important;
116
  --neutral-950: #0A0A0A !important;
117
+ --body-background-fill: {BG} !important;
118
+ --background-fill-primary: {BG} !important;
119
+ --background-fill-secondary: {SURFACE} !important;
120
+ --block-background-fill: {SURFACE} !important;
121
+ --block-label-background-fill: transparent !important;
122
+ --block-title-background-fill: transparent !important;
123
+ --input-background-fill: {SURFACE_STRONG} !important;
124
+ --border-color-primary: {BORDER} !important;
125
+ --border-color-accent: {BORDER_STRONG} !important;
126
+ --color-accent: {PRIMARY} !important;
127
+ --body-text-color: {INK} !important;
128
+ --body-text-color-subdued: {INK_MUTED} !important;
129
+ --block-label-text-color: {INK_MUTED} !important;
130
+ --block-title-text-color: {INK_MUTED} !important;
131
+ --link-text-color: {INK} !important;
132
+ font-family: {FONT_SANS};
133
+ }}
134
+ body, .gradio-container {{
135
+ background: {BG} !important;
136
+ color: {INK} !important;
137
+ font-family: {FONT_SANS} !important;
138
+ font-feature-settings: "ss01", "ss03", "cv11";
139
+ }}
140
+
141
+ /* ============================================================
142
+ * Crush Gradio's default ``.app`` wrapper padding.
143
+ * Default ``.gradio-container > .app`` ships with 16px 32px which
144
+ * eats 64 px of the 360 px mobile viewport. Replace with a sane
145
+ * scale that respects the breakpoints.
146
+ * ============================================================ */
147
+ .gradio-container > .app,
148
+ .gradio-container .main.fillable {{
149
+ padding: 16px 20px !important;
150
+ max-width: none !important;
151
+ }}
152
+ @media (max-width: 640px) {{
153
+ .gradio-container > .app,
154
+ .gradio-container .main.fillable {{
155
+ padding: 8px 10px !important;
156
+ }}
157
+ }}
158
+ main, .contain {{
159
+ width: 100% !important;
160
+ max-width: none !important;
161
+ }}
162
+
163
+ /* ============================================================
164
+ * Header + CTA banner
165
+ * ============================================================ */
166
  .ams-header {{
167
  display:flex; justify-content:space-between; align-items:baseline;
168
+ padding:10px 2px 6px 2px;
169
  }}
170
  .ams-brand {{
171
+ font-family: {FONT_SANS};
172
+ font-size:17px; font-weight:600;
173
+ letter-spacing:-0.01em; color:{INK};
174
  }}
175
+ .ams-brand-period {{ color:{PRIMARY}; font-family: {FONT_MONO}; }}
176
  .ams-status {{
177
+ font-family: {FONT_MONO};
178
+ font-size:10px; color:{INK_MUTED};
179
+ letter-spacing:0.08em; text-transform:uppercase;
180
  }}
181
 
182
  .ams-cta {{
183
+ font-size:12px; color:{INK_MUTED};
184
+ margin:2px 2px 14px 2px; padding-bottom:10px;
185
  border-bottom:1px solid {BORDER};
186
+ line-height:1.5;
187
  }}
188
+ .ams-cta strong {{ color:{INK}; font-weight:600; }}
189
  .ams-cta-heart {{ color:{PRIMARY}; }}
190
+ .ams-cta a {{ color:{INK}; text-decoration:underline; text-decoration-color:{BORDER_STRONG}; }}
191
 
192
+ /* ============================================================
193
+ * Body row (sidebar + content)
194
+ * ============================================================ */
195
  .ams-body {{
196
+ gap:12px !important;
197
  align-items:stretch !important;
198
  }}
199
 
200
+ /* ============================================================
201
+ * Sidebar — desktop ≥ 1024
202
+ * ============================================================ */
203
  .ams-sidebar {{
204
  background:{SURFACE_STRONG} !important;
205
+ padding:12px 6px !important;
206
  border-radius:{RADIUS} !important;
207
  border:1px solid {BORDER} !important;
208
+ min-width:188px;
209
  max-width:210px;
210
  }}
211
 
 
212
  .ams-side-radio {{
213
  background:transparent !important;
214
  border:none !important;
215
  padding:0 !important;
216
  width:100%;
217
  }}
218
+ .ams-side-radio > div > .wrap,
219
  .ams-side-radio .wrap {{
220
  display:flex !important;
221
  flex-direction:column !important;
 
223
  background:transparent !important;
224
  border:none !important;
225
  }}
 
226
  .ams-side-radio label {{
227
  display:flex !important;
228
  align-items:center !important;
229
+ padding:8px 11px !important;
230
  margin:0 !important;
231
+ border-radius:3px !important;
232
  border:none !important;
233
  border-left:2px solid transparent !important;
234
  background:transparent !important;
235
  color:{INK_MUTED} !important;
236
+ font-family: {FONT_SANS} !important;
237
+ font-size:12px !important;
238
  font-weight:500 !important;
239
+ letter-spacing:0.005em !important;
240
  cursor:pointer !important;
241
  transition:background 80ms ease, color 80ms ease, border-color 80ms ease;
242
  min-height:0 !important;
 
247
  background:{HOVER_BG} !important;
248
  color:{INK} !important;
249
  }}
 
250
  .ams-side-radio label input[type="radio"] {{
251
  display:none !important;
252
  }}
 
253
  .ams-side-radio label.selected,
254
  .ams-side-radio label:has(input[type="radio"]:checked) {{
255
  background:{HOVER_BG} !important;
 
257
  border-left-color:{PRIMARY} !important;
258
  font-weight:600 !important;
259
  }}
 
260
  .ams-side-radio + div:empty {{ display:none !important; }}
261
 
262
+ /* History block (below the mode radio) */
263
  .ams-history {{
264
+ margin-top:12px;
265
+ padding-top:8px;
266
  border-top:1px solid {BORDER};
267
  }}
268
  .ams-history-title {{
269
+ font-family: {FONT_MONO};
270
+ font-size:9px; color:{INK_MUTED};
271
+ letter-spacing:0.12em; text-transform:uppercase;
272
  padding:0 4px 6px 4px;
273
  }}
274
  .ams-history-empty {{
275
+ font-family: {FONT_SANS};
276
+ font-size:11px; color:{INK_FAINT};
277
  font-style:italic;
278
+ padding:4px 4px;
279
  }}
280
 
281
+ /* ============================================================
282
+ * Content pane
283
+ * ============================================================ */
284
  .ams-content {{
285
  background:{SURFACE} !important;
286
  border:1px solid {BORDER} !important;
287
  border-radius:{RADIUS} !important;
288
+ padding:14px !important;
289
  min-height:540px;
290
  }}
291
  .ams-tab-pane {{
 
293
  border:none !important;
294
  padding:0 !important;
295
  }}
296
+ /* Force the inner 2-column row inside each pane to actually stack
297
+ on narrow screens Gradio gr.Row keeps row direction by default. */
298
+ @media (max-width: 768px) {{
299
+ .ams-tab-pane .row,
300
+ .ams-tab-pane [class*="row"][class*="svelte"] {{
301
+ flex-direction:column !important;
302
+ }}
303
  }}
 
 
304
 
305
+ /* ============================================================
306
+ * Form field chrome — labels, helper info, inputs
307
+ * Gradio's defaults render labels and helper text at 14-16 px.
308
+ * Brutalist Mono wants labels small + uppercase + mono.
309
+ * ============================================================ */
310
+ /* Scope the small-uppercase-mono treatment to ONLY the label text
311
+ spans, NOT the entire <label> wrapper. Cascading text-transform
312
+ from a label wrapper would otherwise uppercase the helper text
313
+ (.info-text) and the input's own text. */
314
+ .ams-content span.has-info,
315
+ .ams-content span.svelte-jdcl7l,
316
+ .ams-content .block-label > span,
317
+ .ams-content [data-testid="block-label"] > span,
318
+ .ams-content .label-wrap > span:first-child {{
319
+ font-family: {FONT_MONO} !important;
320
+ font-size:10px !important;
321
+ letter-spacing:0.08em !important;
322
+ text-transform:uppercase !important;
323
+ color:{INK_MUTED} !important;
324
+ background:transparent !important;
325
+ border:none !important;
326
+ box-shadow:none !important;
327
+ padding:0 0 4px 0 !important;
328
+ margin:0 !important;
329
+ font-weight:500 !important;
330
+ display:block !important;
331
+ }}
332
+ /* Reset the <label> wrapper itself so it doesn't cascade transforms */
333
+ .ams-content label.container,
334
+ .ams-content label.svelte-1hguek3 {{
335
+ text-transform:none !important;
336
+ font-family:inherit !important;
337
+ font-size:inherit !important;
338
+ letter-spacing:normal !important;
339
+ display:block !important;
340
+ padding:0 !important;
341
+ }}
342
+ /* Helper text — Gradio 6.14 renders it as <div class="info-text …">.
343
+ Force sentence case + sans + small + faint italic. */
344
+ .ams-content .info-text,
345
+ .ams-content [class*="info-text"],
346
+ .ams-content .block .info,
347
+ .ams-content [data-testid="info"] {{
348
+ font-family: {FONT_SANS} !important;
349
+ font-size:10px !important;
350
+ color:{INK_FAINT} !important;
351
+ letter-spacing:0 !important;
352
+ text-transform:none !important;
353
+ font-style:italic !important;
354
+ padding:0 0 6px 0 !important;
355
+ line-height:1.4 !important;
356
+ font-weight:400 !important;
357
+ }}
358
 
 
 
 
 
 
359
  .ams-content textarea,
360
  .ams-content input[type="text"],
361
  .ams-content input[type="number"] {{
362
  background:{SURFACE_STRONG} !important;
363
  border:1px solid {BORDER} !important;
364
+ border-radius:3px !important;
365
  color:{INK} !important;
366
+ font-family: {FONT_SANS} !important;
367
+ font-size:13px !important;
368
+ padding:10px 12px !important;
369
+ line-height:1.5 !important;
370
  }}
371
  .ams-content textarea:focus,
372
  .ams-content input[type="text"]:focus,
373
  .ams-content input[type="number"]:focus {{
374
  outline:none !important;
375
  border-color:{PRIMARY} !important;
376
+ box-shadow:none !important;
377
+ }}
378
+ .ams-content textarea::placeholder,
379
+ .ams-content input::placeholder {{
380
+ color:{INK_FAINT} !important;
381
  }}
382
  .ams-content input[type="range"] {{
383
  accent-color:{PRIMARY} !important;
384
  }}
385
+
386
+ /* ============================================================
387
+ * Form Radio (Vocal mode) — compact pills, NOT the sidebar tabs
388
+ * ============================================================ */
389
+ .ams-content .block:has(input[type="radio"]) .wrap {{
390
+ display:flex !important;
391
+ flex-direction:column !important;
392
+ gap:6px !important;
393
+ }}
394
+ .ams-content .wrap > label {{
395
+ font-family: {FONT_SANS} !important;
396
+ font-size:12px !important;
397
+ text-transform:none !important;
398
+ letter-spacing:0 !important;
399
+ font-weight:500 !important;
400
+ color:{INK} !important;
401
+ background:{SURFACE_STRONG} !important;
402
+ border:1px solid {BORDER} !important;
403
+ border-radius:3px !important;
404
+ padding:7px 12px !important;
405
+ display:flex !important;
406
+ align-items:center !important;
407
+ gap:8px !important;
408
+ cursor:pointer !important;
409
+ }}
410
+ .ams-content .wrap > label:hover {{
411
+ border-color:{BORDER_STRONG} !important;
412
+ background:{HOVER_BG} !important;
413
+ }}
414
+ .ams-content .wrap > label:has(input[type="radio"]:checked) {{
415
+ border-color:{PRIMARY} !important;
416
+ background:{SURFACE_RAISED} !important;
417
+ }}
418
+ /* Custom radio dot */
419
+ .ams-content .wrap > label input[type="radio"] {{
420
+ appearance:none !important;
421
+ -webkit-appearance:none !important;
422
+ width:12px !important; height:12px !important;
423
+ border:1px solid {BORDER_STRONG} !important;
424
+ border-radius:50% !important;
425
+ margin:0 !important;
426
+ flex-shrink:0 !important;
427
+ }}
428
+ .ams-content .wrap > label input[type="radio"]:checked {{
429
+ border-color:{PRIMARY} !important;
430
+ background: radial-gradient({PRIMARY} 0 4px, transparent 5px) !important;
431
+ }}
432
+
433
+ /* ============================================================
434
+ * Primary button (▶ Generate)
435
+ * ============================================================ */
436
+ .ams-content button.primary {{
437
+ background:{PRIMARY} !important;
438
+ color:{BG} !important;
439
+ border:none !important;
440
+ border-radius:3px !important;
441
+ font-family: {FONT_SANS} !important;
442
+ font-size:13px !important;
443
+ font-weight:600 !important;
444
+ letter-spacing:0.005em !important;
445
+ padding:11px 18px !important;
446
+ cursor:pointer !important;
447
+ transition:transform 80ms ease, opacity 80ms ease;
448
+ margin-top:6px !important;
449
+ }}
450
+ .ams-content button.primary:hover {{
451
+ opacity:0.92 !important;
452
+ transform:translateY(-1px);
453
+ }}
454
+ .ams-content button.primary:active {{
455
+ transform:translateY(0);
456
+ }}
457
+
458
+ /* ============================================================
459
+ * Output panel — gr.Audio (.ams-out-audio) and gr.JSON (.ams-out-meta)
460
+ * Targeted via the elem_classes hooks defined in ui.py so we don't
461
+ * have to chase svelte-hashed class names.
462
+ * ============================================================ */
463
+ .ams-content .ams-out {{
464
+ background:{SURFACE_STRONG} !important;
465
+ border:1px solid {BORDER} !important;
466
+ border-radius:3px !important;
467
+ padding:12px !important;
468
+ margin-top:10px !important;
469
+ }}
470
+ .ams-content .ams-out-audio {{
471
+ min-height:90px !important;
472
+ }}
473
+ .ams-content .ams-out-meta {{
474
+ min-height:80px !important;
475
+ font-family: {FONT_MONO} !important;
476
+ font-size:11px !important;
477
+ line-height:1.6 !important;
478
+ }}
479
+ .ams-content .ams-out-meta span {{
480
+ font-family: {FONT_MONO} !important;
481
+ font-size:11px !important;
482
+ }}
483
+ /* The Output/Metadata block labels live as <label class="svelte-19djge9">.
484
+ Force the Brutalist label treatment + uppercase regardless of the
485
+ svelte hash, since they're inside .ams-out. */
486
+ .ams-content .ams-out label,
487
+ .ams-content .ams-out label > span {{
488
+ font-family: {FONT_MONO} !important;
489
  font-size:10px !important;
490
  letter-spacing:0.08em !important;
491
  text-transform:uppercase !important;
492
  color:{INK_MUTED} !important;
493
  background:transparent !important;
494
+ font-weight:500 !important;
495
+ padding:0 0 6px 0 !important;
496
+ display:block !important;
497
+ }}
498
+ /* Empty-state svg glyphs (music note, JSON braces) — center + muted */
499
+ .ams-content .ams-out svg {{
500
+ color:{INK_FAINT} !important;
501
+ opacity:0.5 !important;
502
  }}
503
 
504
+ /* Add explicit gap between form column and output column when stacked */
505
+ @media (max-width: 768px) {{
506
+ .ams-tab-pane > .row,
507
+ .ams-tab-pane > [class*="row"] {{
508
+ gap:18px !important;
509
+ }}
510
+ .ams-content .block + .block {{
511
+ margin-top:4px !important;
512
+ }}
 
 
 
 
 
 
 
513
  }}
514
+
515
+ /* ============================================================
516
+ * LoRA chip pill kept for M2 wiring
517
+ * ============================================================ */
518
+ .ams-chip {{
519
+ display:inline-block; padding:5px 10px; border-radius:14px;
520
+ font-size:11px; margin:0 5px 5px 0; background:{SURFACE_STRONG};
521
+ border:1px solid {BORDER_STRONG}; color:{INK_MUTED}; cursor:pointer;
522
  }}
523
+ .ams-chip.on {{ border-color:{PRIMARY}; color:{PRIMARY}; }}
524
+ .ams-chip.upload {{ border-style:dashed; color:{PRIMARY}; }}
525
+ .ams-lora-file .upload-container {{ min-height:56px !important; }}
526
 
527
+ /* Hide Gradio footer + the floating "Use via API" / settings panel */
528
+ footer {{ display:none !important; }}
529
+ .show-api {{ display:none !important; }}
530
+ .built-with {{ display:none !important; }}
531
+
532
+ /* ============================================================
533
+ * Responsive: tablet 640-1024 px
534
+ * Keep the full sidebar (with labels) — the icon-rail middle state
535
+ * fights Gradio's DOM. Just narrow it slightly.
536
+ * ============================================================ */
537
+ @media (min-width: 641px) and (max-width: 1024px) {{
538
+ .ams-sidebar {{ min-width:160px; max-width:180px; padding:10px 6px !important; }}
539
+ .ams-side-radio label {{ font-size:11px !important; padding:7px 9px !important; }}
540
+ }}
541
 
542
+ /* ============================================================
543
+ * Responsive: mobile < 640 px
544
+ * Sidebar becomes a horizontal scroll pill strip at the top.
545
+ * Form + Output stack with proper gap.
546
+ * ============================================================ */
547
  @media (max-width: 640px) {{
 
548
  .ams-body {{
549
  flex-direction:column !important;
550
  gap:8px !important;
551
  }}
 
 
 
552
  .ams-sidebar {{
553
  min-width:100% !important;
554
  max-width:100% !important;
555
+ padding:2px 0 !important;
556
  border:none !important;
557
  background:transparent !important;
558
  border-radius:0 !important;
559
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  .ams-side-radio .wrap {{
561
  flex-direction:row !important;
562
  flex-wrap:nowrap !important;
563
  overflow-x:auto !important;
564
  overflow-y:hidden !important;
565
  gap:6px !important;
566
+ padding:2px 0 !important;
 
567
  scrollbar-width:none !important;
568
  -ms-overflow-style:none !important;
569
  }}
570
+ .ams-side-radio .wrap::-webkit-scrollbar {{ display:none !important; }}
 
 
 
571
  .ams-side-radio label {{
 
572
  width:auto !important;
573
  min-width:0 !important;
574
  max-width:max-content !important;
575
  flex:0 0 auto !important;
576
+ font-family: {FONT_SANS} !important;
577
  font-size:11px !important;
578
  font-weight:600 !important;
579
  white-space:nowrap !important;
580
+ padding:7px 12px !important;
 
 
 
 
 
581
  background:{SURFACE_STRONG} !important;
582
+ border:1px solid {BORDER} !important;
583
+ border-radius:3px !important;
584
+ justify-content:center !important;
585
  }}
586
  .ams-side-radio label.selected,
587
  .ams-side-radio label:has(input[type="radio"]:checked) {{
588
+ border-color:{PRIMARY} !important;
589
+ background:{SURFACE_RAISED} !important;
590
+ color:{PRIMARY} !important;
591
  }}
 
 
 
592
  .ams-history {{ display:none !important; }}
593
 
594
+ /* Header + CTA tighter */
595
+ .ams-header {{ padding:2px 2px 2px 2px !important; }}
596
+ .ams-brand {{ font-size:14px !important; }}
597
+ .ams-status {{ font-size:9px !important; }}
598
+ .ams-cta {{ font-size:11px !important; margin:0 2px 8px 2px !important; padding-bottom:8px !important; }}
599
+
600
+ /* Content pane tighter */
601
+ .ams-content {{ padding:12px !important; border-radius:3px !important; }}
602
+
603
+ /* Field labels + info shrink further */
604
+ .ams-content label,
605
+ .ams-content .block-label,
606
+ .ams-content [data-testid="block-label"],
607
+ .ams-content span.svelte-jdcl7l,
608
+ .ams-content .label-wrap {{
609
+ font-size:9px !important;
610
+ }}
611
+ .ams-content .block .info,
612
+ .ams-content [data-testid="info"] {{
613
+ font-size:9px !important;
614
+ padding-bottom:4px !important;
615
+ }}
616
+ .ams-content textarea,
617
+ .ams-content input[type="text"],
618
+ .ams-content input[type="number"] {{
619
+ font-size:12px !important;
620
+ padding:8px 10px !important;
621
+ }}
622
+ .ams-content .wrap > label {{
623
+ padding:6px 10px !important;
624
+ font-size:11px !important;
625
+ }}
626
+ .ams-content button.primary {{
627
+ padding:11px 14px !important;
628
+ font-size:12px !important;
629
+ }}
630
+ .ams-content [data-testid="audio"],
631
+ .ams-content .audio-container,
632
+ .ams-content .json-holder {{
633
+ min-height:64px !important;
634
+ }}
635
  }}
636
  """
ui.py CHANGED
@@ -59,15 +59,23 @@ def build_generate_tab() -> dict[str, gr.components.Component]:
59
  )
60
 
61
  # --- OUTPUT column (right, ~40% width) ---
 
 
 
 
62
  with gr.Column(scale=10):
63
  components["output_audio"] = gr.Audio(
64
  label="Output",
65
  type="filepath",
66
  interactive=False,
 
67
  )
68
  # gr.JSON renders a dict directly as a syntax-highlighted, expandable
69
  # tree. gr.Code(language="json") refuses dicts — it requires a
70
  # pre-stringified blob — and crashes with "'dict' has no .strip()".
71
- components["output_meta"] = gr.JSON(label="Metadata")
 
 
 
72
 
73
  return components
 
59
  )
60
 
61
  # --- OUTPUT column (right, ~40% width) ---
62
+ # elem_classes on each output component give CSS hooks for the
63
+ # Brutalist Mono treatment (uppercase mono labels + bordered
64
+ # empty-state panels). Without these we'd need to target
65
+ # svelte-hashed classes which can change across Gradio versions.
66
  with gr.Column(scale=10):
67
  components["output_audio"] = gr.Audio(
68
  label="Output",
69
  type="filepath",
70
  interactive=False,
71
+ elem_classes=["ams-out", "ams-out-audio"],
72
  )
73
  # gr.JSON renders a dict directly as a syntax-highlighted, expandable
74
  # tree. gr.Code(language="json") refuses dicts — it requires a
75
  # pre-stringified blob — and crashes with "'dict' has no .strip()".
76
+ components["output_meta"] = gr.JSON(
77
+ label="Metadata",
78
+ elem_classes=["ams-out", "ams-out-meta"],
79
+ )
80
 
81
  return components