"""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; /* Cap the body at the viewport and clip any horizontal overflow so a long-clip waveform inside .ams-content can't push the row sideways. */ max-width:100% !important; overflow:hidden !important; }} /* IMPORTANT: do NOT apply ``min-width: 0`` to ``.ams-body > *`` — that selector also matches ``.ams-sidebar``, overriding its 188 px min-width and collapsing it to almost nothing on desktop (seen as a vertical sliver of stacked single characters). The flex-shrink fix we need is on ``.ams-content`` only; sidebar keeps its hard minimum. */ /* ============================================================ * 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