/* ===== ART-PROMPtimizer Studio — component/layout layer ===== */
/* The palette lives in theme/space.css (the same colours the browser
extension uses) and is prepended to this file by src/ui/assets.py. Never
define colours here: hard-coded rgba() values are what make a theme switch
look half-applied. Use a token -- add one to space.css if none fits. */
/* Page backdrop: a deep vertical wash plus the brand glows, echoing the
project logo's navy -> indigo -> violet field. --bg-deep darkens the
lower half so panels read as raised. */
html, body { background: var(--bg) !important; font-family: var(--font) !important; }
#root, .gradio-container { background: transparent !important; }
/* Ambient glow, deliberately faint: the tokens are tuned to read as a deep
field, not as neon. Derived with color-mix rather than a literal so themes
still work. */
body::before {
content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none;
background:
radial-gradient(1100px 540px at 50% -12%, color-mix(in srgb, var(--glow-1) 50%, transparent), transparent 62%),
radial-gradient(860px 500px at 94% 6%, color-mix(in srgb, var(--glow-2) 45%, transparent), transparent 58%),
linear-gradient(180deg, color-mix(in srgb, var(--bg-deep) 55%, var(--bg)) 0%, var(--bg-deep) 100%);
}
.gradio-container { position: relative; z-index: 1; max-width: 1280px !important; margin: 0 auto !important; padding: 16px 20px 48px !important; }
/* Gradio caps its own .main.fillable at 768px, which made the multi-column
cockpit collapse into a single 704px stack on any desktop. Raise it so the
grid rules below actually have room to lay out side by side. */
.main.fillable, .main.fillable > .wrap { max-width: 1240px !important; margin-left: auto !important; margin-right: auto !important; }
:focus-visible { outline: 2px solid var(--accent) !important; outline-offset: 2px !important; }
*::-webkit-scrollbar { width: 6px; height: 6px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--track) !important; border-radius: 3px; }
*::-webkit-scrollbar-thumb:hover { background: var(--chip-border) !important; }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
/* ---------- header ----------
Single compact bar. The brand mark, wordmark and subtitle sit inline; the
theme/language chips run horizontally on the right and are allowed to
wrap onto a second line rather than growing a 486px-tall column.
The wordmark uses the display serif with a two-tone "PRO", following the
project logo. Both tones come from tokens so every theme re-colours it. */
.art-brand {
display: flex !important; align-items: center !important; gap: 11px !important; min-width: 0 !important;
/* Framed, like the rest of the header. Also load-bearing: the wordmark is
gradient-clipped text on a bare page surface, so it needs a stable
backdrop rather than the ambient glow behind the header. */
background: var(--panel) !important; border: 1px solid var(--panel-border) !important;
border-radius: var(--r-md) !important; padding: 9px 14px 9px 10px !important;
box-shadow: inset 0 1px 0 var(--spec-1) !important;
/* Floor: logo (38) + gap (11) + the widest wordmark (~240) + padding (24).
Without it a narrow viewport squeezes the column to a sliver and the
nowrap title spills out of the frame. */
min-width: 313px !important;
}
.art-logo {
flex: none !important; width: 38px !important; height: 38px !important; border-radius: 11px !important;
background: linear-gradient(140deg, var(--grad-1), var(--accent-2)) !important;
box-shadow: 0 2px 10px var(--logo-shadow) !important; overflow: hidden !important;
display: inline-flex !important; align-items: center !important; justify-content: center !important;
}
.art-logo img { width: 100% !important; height: 100% !important; object-fit: cover !important; display: block !important; }
.art-brand-text { display: flex !important; flex-direction: column !important; gap: 0 !important; min-width: 0 !important; }
.art-title {
font-family: var(--font-display) !important; font-weight: 600 !important; font-size: 27px !important;
letter-spacing: 0.2px !important; line-height: 1.05 !important; white-space: nowrap !important;
color: var(--text) !important;
}
/* The two-tone "PRO" from the logo.
Uses the `background-image` LONGHAND, not the `background` shorthand:
Gradio resets the shorthand on these elements, so a shorthand gradient was
silently dropped while `-webkit-text-fill-color: transparent` stayed --
which rendered the letters invisible. Same bug that hid the whole wordmark
earlier; this form survives it. The @supports fallback keeps the wordmark
readable where background-clip:text is unavailable.
Endpoints come from --wordmark-*, not --grad-*: the header is a bare page
surface, and a light theme's --grad-1 fails contrast there. */
.art-title-accent {
color: var(--wordmark-1) !important;
background-image: linear-gradient(100deg, var(--wordmark-1), var(--wordmark-2)) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
-webkit-text-fill-color: transparent !important;
}
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
.art-title-accent { color: var(--wordmark-1) !important; -webkit-text-fill-color: currentColor !important; }
}
.art-subtitle { font-size: 11px !important; color: var(--muted) !important; line-height: 1.35 !important;
white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; }
/* `auto`, not 0: for a flex item `min-width: auto` is the min-content floor,
which here is .art-brand's own 313px (set so the subtitle fits unclipped on
one line). With 0 the box was allowed to collapse to the wordmark's ~204px
while the brand kept its 313px, so the brand overflowed its own parent and
the subtitle ran underneath the theme picker -- measured at 1290px, the
picker won every hit-test from x=277 on and the subtitle's tail (drawn to
x=371) was simply painted over, ellipsis and all. Keeping the floor makes
head-main reserve the width the brand needs; head-side gives ground instead,
which it can now do because .art-controls below is shrinkable. */
.art-head-main { min-width: auto !important; justify-content: flex-start !important; }
/* 640px is the width that holds all five theme chips on one row (5 x ~126px).
It used to be set as `min-width`, which is a floor the row can never cross:
brand 313px + 640px + gap + topbar padding is a 1015px min-content, and
Gradio carried that up the tree unchanged (.wrap 1015 -> .main 1079 ->
.gradio-container 1119), so any viewport under 1119px scrolled sideways for
the sake of a header. Flex-basis keeps the intent -- the row still asks for
640px and takes it whenever there is room -- but lets it give ground, and the
clusters below already wrap inside their frames when they do.
`.art-controls` has to be shrinkable as well: it was `flex: none`, so its
640px of content was an unbreakable block sitting under head-side and
min-width:0 on head-side alone would only have moved the overflow into it. */
.art-head-side { min-width: 0 !important; flex: 0 1 auto !important; justify-content: flex-end !important; }
.art-controls { gap: 6px !important; justify-content: flex-end !important; align-items: stretch !important;
flex-wrap: wrap !important; flex: 0 1 auto !important; min-width: 0 !important; }
/* Framed settings cluster: the theme and language pickers get their own
bordered shell so they read as one control, not as loose floating chips.
The radius is a rounded rectangle, not --pill: these wrap onto two rows on
narrow viewports, and a 999px radius on a multi-row box renders as a
stadium/ellipse. */
.art-theme-btn, .art-lang-btn {
gap: 4px !important; margin: 0 !important; flex-wrap: wrap !important; justify-content: flex-end !important;
padding: 5px 6px !important;
background: var(--inset-bg) !important; border: 1px solid var(--panel-border) !important;
border-radius: var(--r-md) !important; box-shadow: inset 0 1px 0 var(--spec-1) !important;
display: flex !important; flex-direction: row !important; align-items: center !important;
}
.art-theme-btn label, .art-lang-btn label {
display: inline-flex !important; align-items: center !important; height: 24px !important; box-sizing: border-box !important;
border: 1px solid transparent !important; background: transparent !important;
border-radius: var(--pill) !important; padding: 0 10px !important;
font-size: 11px !important; font-weight: 600 !important; line-height: 1 !important;
white-space: nowrap !important; cursor: pointer !important; transition: all 0.15s !important;
}
.art-theme-btn label:hover, .art-lang-btn label:hover { background: var(--chip-hover) !important; color: var(--text) !important; }
.art-theme-btn label.selected, .art-lang-btn label.selected {
border-color: var(--chip-border) !important; color: var(--chip-ink) !important;
background: var(--accent-soft) !important;
box-shadow: inset 0 1px 0 var(--spec-1) !important;
}
.art-divider { height: 1px !important; border: none !important; margin: 10px 0 12px !important;
background: var(--panel-border) !important; }
/* ---------- layout stack & cards ---------- */
.art-tree { display: flex !important; flex-direction: column !important; gap: 12px !important; }
/* Cards that host Gradio components: NO backdrop-filter, deliberately.
A filter (or backdrop-filter) on an ancestor makes that ancestor the
containing block for its `position: fixed` descendants, and Gradio anchors
every dropdown option panel (`ul.options`) with `position: fixed` plus an
inline `bottom` solved against the viewport. With the blur on the card, that
`bottom` was instead resolved against the ~1200px card, so the panel opened
hundreds of pixels away from its own field -- usually below the fold, once
mid-screen. Users read that as "the model list is empty" and "the option
list jumps to the middle of the page", and every tab was affected because
.art-tree-node wraps them all.
Measured in-browser for one field: top 1065.28px (card-relative) vs
742.48px (viewport-correct), list landing at y=1354 in an 886px viewport.
The blur itself is invisible here anyway: --panel is 0.78 opaque over a
smooth gradient with --noise 0.035, i.e. 0.22 * 0.035 = 0.008 of grain.
`.art-tree-node::before` (specular rim) is unaffected: a pseudo-element is
never an ancestor of anything. */
.art-tree-node, .art-result-card {
background: var(--panel) !important;
border: 1px solid var(--panel-border) !important;
border-radius: var(--radius) !important;
padding: 16px !important;
box-shadow: 0 4px 20px var(--shadow) !important;
}
/* Emitted as markup *inside* gr.HTML (see tagger_tab.py / generate_tab.py),
so these can never contain a Gradio component and keep the glass. */
.art-tagger-panel, .art-suggest-box {
background: var(--panel) !important;
border: 1px solid var(--panel-border) !important;
border-radius: var(--radius) !important;
padding: 16px !important;
backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) !important;
-webkit-backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) !important;
box-shadow: 0 4px 20px var(--shadow) !important;
}
.art-tree-header { display: flex !important; align-items: flex-start !important; gap: 10px !important; margin-bottom: 12px !important; }
.art-tree-header-icon { font-size: 17px !important; line-height: 1.3 !important; }
.art-tree-header-title { color: var(--text) !important; font-weight: 700 !important; font-size: 11px !important; letter-spacing: 1px !important; text-transform: uppercase !important; }
.art-tree-header-desc { color: var(--muted) !important; font-size: 11.5px !important; line-height: 1.5 !important; margin-top: 3px !important; }
.art-grid { display: flex !important; flex-wrap: wrap !important; gap: 12px !important; align-items: flex-start !important; }
.art-grid > .art-col { flex: 1 1 100% !important; min-width: 0 !important; }
/* The control stack is itself a grid, so the cards pair up instead of
queueing down one 1150px-wide ribbon with a lot of dead space beside each
one. align-items:start stops a short card from stretching to its row. */
.art-grid > .art-col {
display: grid !important; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)) !important;
gap: 12px !important; align-items: start !important;
}
/* Cards that are inherently wide. Without this the 28-item category grid got
squeezed into a third of the row and stacked into a ~1200px ribbon, while
the two short cards beside it left a large dead area. */
.art-grid .art-span-all { grid-column: 1 / -1 !important; }
/* Results keep the full width: ten prompt textareas need the room. */
.art-grid > .art-output-col { flex: 1 1 100% !important; display: flex !important; flex-direction: column !important; gap: 12px !important; }
.art-col { display: flex !important; flex-direction: column !important; gap: 12px !important; min-width: 0 !important; }
/* ---------- Aurora glass ----------
Ported from the extension's popup.css so both surfaces share the same
depth cues: a 1px specular edge that catches light, a fine noise texture
over the whole page, and blur+saturate on every raised surface. */
/* fine noise texture (inline SVG feTurbulence), sits under the content */
body::after {
content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none;
opacity: var(--noise);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* specular edge: a 1px gradient border masked to sit only on the rim, so the
card reads as catching light instead of wearing a flat outline. Needs
position:relative on the host for the absolutely positioned overlay. */
.art-tree-node, .art-result-card, .art-tagger-panel, .art-suggest-box,
.art-batch-card { position: relative !important; }
.art-tree-node::before, .art-result-card::before, .art-tagger-panel::before,
.art-suggest-box::before, .art-batch-card::before {
content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
border-radius: inherit; padding: 1px;
background: linear-gradient(145deg, var(--spec-1), transparent 38%, transparent 62%, var(--spec-2));
/* `black` here is a mask cutout: only the alpha channel matters for a mask,
so it is a geometry value rather than a paint colour. */
-webkit-mask: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0);
mask-composite: exclude;
}
/* ---------- sticky HUD header ----------
Mirrors the extension's .topbar.sticky-hud: the brand row stays put while
the long forms scroll under it. Single rule on purpose -- a second
.art-topbar block would split the header across two places. */
.art-topbar {
position: sticky !important; top: 0 !important; z-index: 100 !important;
align-items: center !important; flex-wrap: nowrap !important;
background: var(--bar) !important;
backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) !important;
-webkit-backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) !important;
border-bottom: 1px solid var(--panel-border) !important;
padding: 8px 6px !important; margin: 0 -6px 0 !important;
border-radius: 0 !important;
}
/* ---------- segmented control ----------
Joined pills for small binary/multi-way choices (tag mode, prompt format,
control kind). The extension calls this .segment; here it is opt-in via
elem_classes so free-form radio groups keep the looser pill look.
elem_classes lands on the component's