"""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