Spaces:
Running
Running
Download src/ui/studio.css from ArtShumov/Whyx-PROmpTea: direct link, hf CLI and curl.
- Browser
- Download file 63.9 kB
-
https://huggingface.co/spaces/ArtShumov/Whyx-PROmpTea/resolve/main/src/ui/studio.css
- Command line
-
hf download hf://spaces/ArtShumov/Whyx-PROmpTea/src/ui/studio.css
-
curl -L -o studio.css https://huggingface.co/spaces/ArtShumov/Whyx-PROmpTea/resolve/main/src/ui/studio.css
63.9 kB
| /* ===== 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) ; font-family: var(--font) ; } | |
| #root, .gradio-container { background: transparent ; } | |
| /* 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 ; margin: 0 auto ; padding: 16px 20px 48px ; } | |
| /* 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 ; margin-left: auto ; margin-right: auto ; } | |
| :focus-visible { outline: 2px solid var(--accent) ; outline-offset: 2px ; } | |
| *::-webkit-scrollbar { width: 6px; height: 6px; } | |
| *::-webkit-scrollbar-track { background: transparent; } | |
| *::-webkit-scrollbar-thumb { background: var(--track) ; border-radius: 3px; } | |
| *::-webkit-scrollbar-thumb:hover { background: var(--chip-border) ; } | |
| @media (prefers-reduced-motion: reduce) { * { animation: none ; transition: none ; } } | |
| /* ---------- 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 ; align-items: center ; gap: 11px ; min-width: 0 ; | |
| /* 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) ; border: 1px solid var(--panel-border) ; | |
| border-radius: var(--r-md) ; padding: 9px 14px 9px 10px ; | |
| box-shadow: inset 0 1px 0 var(--spec-1) ; | |
| /* 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 ; | |
| } | |
| .art-logo { | |
| flex: none ; width: 38px ; height: 38px ; border-radius: 11px ; | |
| background: linear-gradient(140deg, var(--grad-1), var(--accent-2)) ; | |
| box-shadow: 0 2px 10px var(--logo-shadow) ; overflow: hidden ; | |
| display: inline-flex ; align-items: center ; justify-content: center ; | |
| } | |
| .art-logo img { width: 100% ; height: 100% ; object-fit: cover ; display: block ; } | |
| .art-brand-text { display: flex ; flex-direction: column ; gap: 0 ; min-width: 0 ; } | |
| .art-title { | |
| font-family: var(--font-display) ; font-weight: 600 ; font-size: 27px ; | |
| letter-spacing: 0.2px ; line-height: 1.05 ; white-space: nowrap ; | |
| color: var(--text) ; | |
| } | |
| /* 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) ; | |
| background-image: linear-gradient(100deg, var(--wordmark-1), var(--wordmark-2)) ; | |
| -webkit-background-clip: text ; | |
| background-clip: text ; | |
| -webkit-text-fill-color: transparent ; | |
| } | |
| @supports not ((-webkit-background-clip: text) or (background-clip: text)) { | |
| .art-title-accent { color: var(--wordmark-1) ; -webkit-text-fill-color: currentColor ; } | |
| } | |
| .art-subtitle { font-size: 11px ; color: var(--muted) ; line-height: 1.35 ; | |
| white-space: nowrap ; overflow: hidden ; text-overflow: ellipsis ; } | |
| /* `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 ; justify-content: flex-start ; } | |
| /* 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 ; flex: 0 1 auto ; justify-content: flex-end ; } | |
| .art-controls { gap: 6px ; justify-content: flex-end ; align-items: stretch ; | |
| flex-wrap: wrap ; flex: 0 1 auto ; min-width: 0 ; } | |
| /* 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 ; margin: 0 ; flex-wrap: wrap ; justify-content: flex-end ; | |
| padding: 5px 6px ; | |
| background: var(--inset-bg) ; border: 1px solid var(--panel-border) ; | |
| border-radius: var(--r-md) ; box-shadow: inset 0 1px 0 var(--spec-1) ; | |
| display: flex ; flex-direction: row ; align-items: center ; | |
| } | |
| .art-theme-btn label, .art-lang-btn label { | |
| display: inline-flex ; align-items: center ; height: 24px ; box-sizing: border-box ; | |
| border: 1px solid transparent ; background: transparent ; | |
| border-radius: var(--pill) ; padding: 0 10px ; | |
| font-size: 11px ; font-weight: 600 ; line-height: 1 ; | |
| white-space: nowrap ; cursor: pointer ; transition: all 0.15s ; | |
| } | |
| .art-theme-btn label:hover, .art-lang-btn label:hover { background: var(--chip-hover) ; color: var(--text) ; } | |
| .art-theme-btn label.selected, .art-lang-btn label.selected { | |
| border-color: var(--chip-border) ; color: var(--chip-ink) ; | |
| background: var(--accent-soft) ; | |
| box-shadow: inset 0 1px 0 var(--spec-1) ; | |
| } | |
| .art-divider { height: 1px ; border: none ; margin: 10px 0 12px ; | |
| background: var(--panel-border) ; } | |
| /* ---------- layout stack & cards ---------- */ | |
| .art-tree { display: flex ; flex-direction: column ; gap: 12px ; } | |
| /* 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) ; | |
| border: 1px solid var(--panel-border) ; | |
| border-radius: var(--radius) ; | |
| padding: 16px ; | |
| box-shadow: 0 4px 20px var(--shadow) ; | |
| } | |
| /* 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) ; | |
| border: 1px solid var(--panel-border) ; | |
| border-radius: var(--radius) ; | |
| padding: 16px ; | |
| backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) ; | |
| -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) ; | |
| box-shadow: 0 4px 20px var(--shadow) ; | |
| } | |
| .art-tree-header { display: flex ; align-items: flex-start ; gap: 10px ; margin-bottom: 12px ; } | |
| .art-tree-header-icon { font-size: 17px ; line-height: 1.3 ; } | |
| .art-tree-header-title { color: var(--text) ; font-weight: 700 ; font-size: 11px ; letter-spacing: 1px ; text-transform: uppercase ; } | |
| .art-tree-header-desc { color: var(--muted) ; font-size: 11.5px ; line-height: 1.5 ; margin-top: 3px ; } | |
| .art-grid { display: flex ; flex-wrap: wrap ; gap: 12px ; align-items: flex-start ; } | |
| .art-grid > .art-col { flex: 1 1 100% ; min-width: 0 ; } | |
| /* 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 ; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)) ; | |
| gap: 12px ; align-items: start ; | |
| } | |
| /* 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 ; } | |
| /* Results keep the full width: ten prompt textareas need the room. */ | |
| .art-grid > .art-output-col { flex: 1 1 100% ; display: flex ; flex-direction: column ; gap: 12px ; } | |
| .art-col { display: flex ; flex-direction: column ; gap: 12px ; min-width: 0 ; } | |
| /* ---------- 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 ; } | |
| .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 ; top: 0 ; z-index: 100 ; | |
| align-items: center ; flex-wrap: nowrap ; | |
| background: var(--bar) ; | |
| backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) ; | |
| -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) ; | |
| border-bottom: 1px solid var(--panel-border) ; | |
| padding: 8px 6px ; margin: 0 -6px 0 ; | |
| border-radius: 0 ; | |
| } | |
| /* ---------- 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 <fieldset>, which also contains the | |
| label, the info text and the choices row. Laying the fieldset out as one | |
| flex ROW (the previous rule) put all three side by side: in a 482px column | |
| the label and info ate 313px and the two pills were left 158px, i.e. 75px | |
| each, so "Prompt-ready" and "Raw (booru)" rendered ellipsised. Stack the | |
| fieldset instead and move the inset frame onto the choices row, which is | |
| what "joined pills" was meant to describe. | |
| The choices row is found structurally (`> :has(> label)`) rather than by | |
| Gradio's hashed class names -- `svelte-e4x47i` differs between Gradio 5 and | |
| 6, and a version-specific selector is exactly the kind of rule that passes | |
| on one version and silently unstyles on the other. */ | |
| .art-segment { | |
| display: flex ; flex-direction: column ; align-items: stretch ; | |
| width: 100% ; gap: 6px ; | |
| background: transparent ; border: none ; padding: 0 ; | |
| } | |
| .art-segment > :has(> label) { | |
| display: flex ; flex-wrap: wrap ; gap: 3px ; | |
| width: 100% ; box-sizing: border-box ; | |
| background: var(--inset-bg) ; border: 1px solid var(--input-border) ; | |
| border-radius: 12px ; padding: 3px ; | |
| } | |
| .art-segment label { | |
| /* 160px basis rather than 0: with `flex-basis: 0` two labels always share | |
| a row however narrow it gets, and the nowrap+ellipsis then ate the text. | |
| A basis larger than half the row lets flex-wrap drop a pill onto its own | |
| full-width line instead of shrinking it below its label. */ | |
| flex: 1 1 160px ; min-width: 0 ; | |
| justify-content: center ; text-align: center ; | |
| border: none ; background: transparent ; border-radius: 9px ; | |
| padding: 6px 10px ; font-size: 11.5px ; font-weight: 650 ; | |
| color: var(--muted) ; white-space: nowrap ; | |
| overflow: hidden ; text-overflow: ellipsis ; | |
| } | |
| .art-segment label:hover { color: var(--text) ; } | |
| .art-segment label.selected, .art-segment label:has(input:checked) { | |
| color: var(--accent-ink) ; | |
| background: linear-gradient(135deg, var(--grad-1), var(--grad-2)) ; | |
| box-shadow: 0 2px 8px var(--btn-shadow) ; | |
| } | |
| /* ---------- skeletons (loading placeholders) ---------- */ | |
| .art-skeleton { position: relative ; overflow: hidden ; | |
| border-radius: var(--radius) ; padding: 11px 12px ; | |
| background: var(--panel) ; border: 1px solid var(--panel-border) ; } | |
| .art-skeleton .sk-line { height: 10px ; border-radius: 6px ; | |
| background: var(--track) ; margin-bottom: 8px ; } | |
| .art-skeleton .sk-line:last-child { margin-bottom: 0 ; } | |
| .art-skeleton .sk-line.w40 { width: 40% ; } | |
| .art-skeleton .sk-line.w60 { width: 60% ; } | |
| .art-skeleton .sk-line.w80 { width: 85% ; } | |
| .art-skeleton::after { content: ""; position: absolute; inset: 0; pointer-events: none; | |
| background: linear-gradient(100deg, transparent 30%, var(--spec-1) 50%, transparent 70%); | |
| background-size: 200% 100%; animation: art-shimmer 1.3s linear infinite; } | |
| @keyframes art-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } | |
| /* ---------- viewfinder (HUD corners + thirds grid) ---------- | |
| Drawn over the Tagger preview by the .art-viewfinder wrapper. */ | |
| .art-viewfinder { position: relative ; display: inline-block ; } | |
| .art-vf-corner { position: absolute ; width: 18px ; height: 18px ; | |
| border: 2px solid var(--spec-2) ; pointer-events: none ; opacity: 0.9 ; | |
| transition: opacity 0.2s ; } | |
| .art-vf-corner.tl { top: 8px ; left: 8px ; border-right: none ; border-bottom: none ; border-radius: 6px 0 0 0 ; } | |
| .art-vf-corner.tr { top: 8px ; right: 8px ; border-left: none ; border-bottom: none ; border-radius: 0 6px 0 0 ; } | |
| .art-vf-corner.bl { bottom: 8px ; left: 8px ; border-right: none ; border-top: none ; border-radius: 0 0 0 6px ; } | |
| .art-vf-corner.br { bottom: 8px ; right: 8px ; border-left: none ; border-top: none ; border-radius: 0 0 6px 0 ; } | |
| .art-vf-grid { position: absolute ; inset: 0 ; pointer-events: none ; | |
| display: none ; opacity: 0.5 ; | |
| background: | |
| linear-gradient(to right, transparent calc(33.3% - 0.5px), var(--spec-2) calc(33.3% - 0.5px), var(--spec-2) calc(33.3% + 0.5px), transparent calc(33.3% + 0.5px)), | |
| linear-gradient(to right, transparent calc(66.6% - 0.5px), var(--spec-2) calc(66.6% - 0.5px), var(--spec-2) calc(66.6% + 0.5px), transparent calc(66.6% + 0.5px)), | |
| linear-gradient(to bottom, transparent calc(33.3% - 0.5px), var(--spec-2) calc(33.3% - 0.5px), var(--spec-2) calc(33.3% + 0.5px), transparent calc(33.3% + 0.5px)), | |
| linear-gradient(to bottom, transparent calc(66.6% - 0.5px), var(--spec-2) calc(66.6% - 0.5px), var(--spec-2) calc(66.6% + 0.5px), transparent calc(66.6% + 0.5px)); | |
| } | |
| .art-viewfinder.grid-on .art-vf-grid { display: block ; } | |
| /* ---------- telemetry (mono readouts) ---------- */ | |
| .art-telemetry { display: flex ; flex-wrap: wrap ; gap: 6px 16px ; | |
| align-items: center ; font-family: var(--mono) ; font-size: 11px ; | |
| color: var(--muted) ; letter-spacing: 0.2px ; | |
| background: var(--inset-bg) ; border: 1px solid var(--line-soft) ; | |
| border-radius: var(--r-sm) ; padding: 6px 11px ; } | |
| .art-telemetry b { color: var(--text) ; font-weight: 600 ; } | |
| .art-telemetry .ok { color: var(--ok) ; } | |
| .art-telemetry .warn { color: var(--amber) ; } | |
| .art-telemetry .err { color: var(--danger) ; } | |
| .art-tabular { font-variant-numeric: tabular-nums ; font-family: var(--mono) ; } | |
| /* ---------- chips filter + telemetry host ---------- */ | |
| .art-chip-search { margin-bottom: 8px ; } | |
| .art-chip-search input { border-radius: var(--pill) ; font-size: 12px ; | |
| background: var(--input) ; border: 1px solid var(--input-border) ; | |
| color: var(--text) ; padding: 7px 12px ; } | |
| .art-chip-search input::placeholder { color: var(--faint) ; } | |
| .art-telemetry-host { margin-top: 10px ; } | |
| .art-telemetry-host > .art-telemetry { margin: 0 ; } | |
| .art-telemetry-fields:empty { display: none ; } | |
| /* ---------- cloud-caption progress HUD (client-side) ---------- | |
| The private caption path spends every second of its wait in this page's own | |
| fetch() to the provider, which happens BEFORE Gradio's event reaches the | |
| server. Gradio's progress bar only ever covers the server half, so for the | |
| longest wait it has nothing to show -- hence a clock of our own, next to the | |
| button. Driven by window.artHud (src/ui/assets.py) and started/stopped by | |
| the caption script in src/ui/direct_caption.py; extension parity with the | |
| popup's #elapsed readout and ring. */ | |
| .art-cloud-hud { display: flex ; flex: 1 1 auto ; min-width: 0 ; | |
| align-items: center ; gap: 8px ; } | |
| .art-cloud-hud-time { font-family: var(--mono) ; font-size: 12px ; | |
| font-variant-numeric: tabular-nums ; color: var(--muted) ; | |
| min-width: 46px ; white-space: nowrap ; } | |
| .art-cloud-hud-track { display: block ; flex: 1 1 auto ; min-width: 48px ; | |
| height: 6px ; overflow: hidden ; background: var(--track) ; | |
| border: 1px solid var(--line-soft) ; border-radius: var(--pill) ; | |
| transition: height 240ms ease, opacity 240ms ease, border-width 240ms ease ; } | |
| .art-cloud-hud-bar { display: block ; height: 100% ; | |
| /* Declared here so the token resolves even before the clock has written | |
| anything (and if the clock never runs, the bar stays empty rather than | |
| filling by accident). paint() overrides it on the element itself. */ | |
| --art-fill: 0%; | |
| /* The fill arrives as a custom property, not as an inline width: an | |
| inline width would lose to the !important below and the bar would stay | |
| at zero for ever while the clock ticked on top of it. | |
| No transition on this one, deliberately: paint() retargets it every | |
| 100ms, so the transition never gets to finish and Chrome pins the bar | |
| at its start value -- measured at a constant 1.86px while the fill | |
| climbed from 2% to 100%. Ten updates a second needs no easing. */ | |
| width: var(--art-fill, 0%) ; | |
| background: linear-gradient(90deg, var(--grad-1), var(--grad-2)) ; | |
| border-radius: var(--pill) ; } | |
| .art-cloud-hud.running .art-cloud-hud-time { color: var(--accent) ; } | |
| .art-cloud-hud.done .art-cloud-hud-time { color: var(--ok) ; } | |
| .art-cloud-hud.err .art-cloud-hud-time { color: var(--danger) ; } | |
| /* The final reading stays -- the extension keeps #elapsed for the same reason -- | |
| but the bar collapses afterwards so the button row never grows a second line. | |
| The delay lets the fill reach 100% before the track folds away. */ | |
| .art-cloud-hud.done .art-cloud-hud-track, | |
| .art-cloud-hud.err .art-cloud-hud-track { | |
| height: 0 ; opacity: 0 ; border-width: 0 ; | |
| transition: height 240ms ease 400ms, opacity 240ms ease 400ms, border-width 240ms ease 400ms ; | |
| } | |
| /* viewfinder sits above the preview image */ | |
| .art-viewfinder .art-vf-corner, .art-viewfinder .art-vf-grid { z-index: 3 ; } | |
| /* ---------- prompt + HUD ---------- */ | |
| .art-prompt-main textarea { font-size: 14px ; line-height: 1.6 ; min-height: 110px ; } | |
| .art-hud { display: flex ; gap: 8px 14px ; flex-wrap: wrap ; align-items: center ; | |
| font-size: 11px ; color: var(--muted) ; margin-top: 10px ; | |
| background: var(--surface-2) ; border: 1px solid var(--line-soft) ; | |
| border-radius: var(--r-md) ; padding: 7px 12px ; } | |
| .art-hud b { color: var(--text) ; font-family: var(--mono) ; } | |
| .art-hud .ok { color: var(--ok) ; } | |
| .art-hud .warn { color: var(--amber) ; } | |
| .art-hud-hint { margin-left: auto ; opacity: 0.6 ; font-family: var(--mono) ; } | |
| /* ---------- sticky action bar ---------- */ | |
| .art-actionbar { | |
| position: sticky ; bottom: 12px ; z-index: 50 ; | |
| display: flex ; gap: 10px ; align-items: center ; | |
| background: var(--bar) ; border: 1px solid var(--panel-border) ; | |
| border-radius: var(--pill) ; padding: 10px 12px ; | |
| backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) ; | |
| -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(var(--glass-sat)) ; | |
| box-shadow: 0 10px 32px var(--shadow), 0 0 24px var(--glow-1) ; | |
| } | |
| .art-actionbar .art-generate-btn { flex: 1 1 auto ; } | |
| .art-hotkeys { font-size: 11px ; color: var(--faint) ; white-space: nowrap ; padding-right: 6px ; } | |
| /* Status lines. | |
| The handlers used to inline #F87171 / #FBBF24 hexes into their HTML, which | |
| bypassed theming entirely (a warning stayed orange in the porcelain theme). | |
| These classes keep the tone in CSS so every theme re-colours them. */ | |
| .art-notice-error, .art-notice-warn { | |
| font-size: 12.5px ; line-height: 1.5 ; padding: 7px 12px ; | |
| border-radius: 0 var(--r-sm) var(--r-sm) 0 ; border-left: 2px solid transparent ; | |
| } | |
| .art-notice-error { color: var(--danger) ; background: var(--danger-soft) ; | |
| border-left-color: var(--danger-border) ; } | |
| .art-notice-warn { color: var(--amber) ; background: var(--inset-bg) ; | |
| border-left-color: var(--amber) ; } | |
| /* Informational and positive variants of the same notice shape: the cloud | |
| "where" switch uses all three to say what happens to the key. */ | |
| .art-notice-info { color: var(--muted) ; background: var(--inset-bg) ; | |
| border-left-color: var(--panel-border) ; } | |
| .art-notice-ok { color: var(--ok) ; background: var(--inset-bg) ; | |
| border-left-color: var(--ok) ; } | |
| .art-cloud-actions { margin-top: 6px ; } | |
| .art-notice-warn-inline { color: var(--amber) ; font-weight: 600 ; } | |
| .art-notice-error-inline { color: var(--danger) ; font-weight: 600 ; } | |
| /* Rating bars are semantic, not decorative: the safe/questionable/explicit | |
| tiers map onto the status tokens so they re-tint with the theme while | |
| keeping their relative seriousness. */ | |
| .art-rating-pg { background: var(--ok) ; } | |
| .art-rating-pg13 { background: var(--amber) ; } | |
| .art-rating-pg16 { background: color-mix(in srgb, var(--amber) 70%, var(--danger)) ; } | |
| .art-rating-r, .art-rating-r5 { background: var(--danger) ; } | |
| /* ---------- buttons ---------- */ | |
| /* Gradio stretches a Button that is a direct child of a Column to the full | |
| column width; a small secondary action then reads as a banner. */ | |
| .art-inline-btn, .art-inline-btn button { | |
| width: auto ; display: inline-flex ; align-items: center ; | |
| justify-content: center ; align-self: flex-start ; | |
| } | |
| #root button { font-weight: 600 ; border-radius: 12px ; transition: transform 0.15s, box-shadow 0.15s, background 0.15s, filter 0.15s ; } | |
| #root button:hover:not(:disabled) { transform: translateY(-1px) ; } | |
| #root button:active:not(:disabled) { transform: translateY(0) scale(0.98) ; } | |
| .art-generate-btn button { | |
| width: 100% ; border: none ; border-radius: var(--pill) ; | |
| background: linear-gradient(135deg, var(--grad-1), var(--accent) 52%, var(--grad-2)) ; | |
| color: var(--accent-ink) ; font-weight: 700 ; font-size: 14px ; letter-spacing: 0.2px ; | |
| padding: 11px 22px ; | |
| /* matte: a soft, tight shadow instead of the palette's 24px accent glow */ | |
| box-shadow: 0 2px 8px color-mix(in srgb, var(--btn-shadow) 55%, transparent) ; | |
| } | |
| .art-generate-btn button:hover:not(:disabled) { | |
| filter: brightness(1.04) ; | |
| box-shadow: 0 3px 12px color-mix(in srgb, var(--btn-shadow) 70%, transparent) ; | |
| } | |
| .art-copy-btn { min-width: 36px ; width: 36px ; height: 34px ; | |
| border-radius: var(--r-sm) ; border: 1px solid var(--panel-border) ; background: var(--surface-2) ; | |
| display: flex ; align-items: center ; justify-content: center ; } | |
| .art-copy-btn:hover { border-color: var(--chip-border) ; background: var(--accent-soft) ; } | |
| /* ---------- fields, radios, chips, pills ---------- */ | |
| .gradio-textbox textarea, .gradio-textbox input, .gradio-number input, input[type="text"] { | |
| background: var(--input) ; border: 1px solid var(--input-border) ; | |
| border-radius: var(--r-md) ; color: var(--text) ; | |
| } | |
| .gradio-textbox textarea:focus, .gradio-textbox input:focus { | |
| border-color: var(--accent) ; box-shadow: 0 0 0 3px var(--focus-ring) ; | |
| } | |
| input[type="radio"], input[type="checkbox"] { accent-color: var(--accent) ; } | |
| .art-radio-group label, .art-chip-group label { | |
| display: inline-flex ; align-items: center ; | |
| border: 1px solid var(--panel-border) ; background: var(--surface-2) ; | |
| border-radius: var(--pill) ; padding: 6px 14px ; | |
| font-size: 12px ; cursor: pointer ; transition: all 0.15s ; user-select: none ; | |
| } | |
| .art-radio-group label:hover, .art-chip-group label:hover { border-color: var(--chip-border) ; } | |
| .art-radio-group label.selected, .art-chip-group label:has(input:checked) { | |
| border-color: var(--chip-border) ; background: var(--accent-soft) ; color: var(--chip-ink) ; | |
| } | |
| .art-chip-group { display: flex ; flex-wrap: wrap ; gap: 8px ; } | |
| /* The tagger Mode picker is six chips whose labels run to 48 characters | |
| ("Krea Lite (fast, smart pose/emotion/composition)"). Left to flex-wrap in a | |
| 482px column they did not so much wrap as queue: five rows, ~200px of | |
| vertical space, with every chip a different width. A grid lines them up into | |
| equal columns instead. auto-fit + minmax collapses to a single full-width | |
| column below ~430px rather than overflowing the panel, and the labels wrap | |
| inside their cell so the text is never truncated. | |
| Scoped to #art-panel-tagger-mode on purpose: the other chip-groups (cloud | |
| provider, control kind) hold two-to-four short labels that read better as one | |
| ragged row than as a half-empty grid. */ | |
| #art-panel-tagger-mode > :has(> label) { | |
| display: grid ; | |
| grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)) ; | |
| gap: 8px ; width: 100% ; box-sizing: border-box ; | |
| } | |
| #art-panel-tagger-mode label { white-space: normal ; overflow-wrap: anywhere ; } | |
| /* ---------- categories grid ---------- | |
| Gradio wraps every gr.Column's children in a div.form, so the grid must be | |
| applied to that inner form. Targeting .art-cat-grid itself does nothing: | |
| it has a single child, and the 28 checkboxes were stacking as 28 rows. */ | |
| .art-cat-search { margin-bottom: 4px ; } | |
| .art-cat-search input { border-radius: var(--pill) ; font-size: 12px ; } | |
| .art-cat-grid { margin-top: 10px ; } | |
| .art-cat-grid > .form { | |
| display: grid ; | |
| grid-template-columns: repeat(auto-fill, minmax(198px, 1fr)) ; | |
| gap: 8px ; align-items: stretch ; | |
| } | |
| .art-cat-btn { margin: 0 ; min-width: 0 ; } | |
| /* Clamp the per-category description: unclamped it made each pill 126-179px | |
| tall and pushed the card past 4200px. Two lines, then an ellipsis. */ | |
| .art-cat-btn .info-text, .art-cat-btn .info { | |
| display: -webkit-box ; -webkit-box-orient: vertical ; | |
| -webkit-line-clamp: 2 ; line-clamp: 2 ; overflow: hidden ; | |
| font-size: 11px ; line-height: 1.4 ; color: var(--muted) ; | |
| text-align: left ; margin-top: 4px ; padding: 0 2px ; | |
| border: none ; background: none ; | |
| } | |
| .art-cat-btn:has(input:checked) .info-text { color: var(--muted) ; } | |
| .art-cat-btn label { | |
| display: flex ; align-items: center ; justify-content: center ; gap: 5px ; | |
| background: var(--surface-2) ; border: 1px solid var(--panel-border) ; | |
| border-radius: var(--pill) ; padding: 8px 10px ; | |
| font-size: 11.5px ; font-weight: 500 ; color: var(--muted) ; | |
| cursor: pointer ; transition: all 0.15s ; text-align: center ; user-select: none ; | |
| min-height: 36px ; | |
| } | |
| .art-cat-btn label:hover { border-color: var(--chip-border) ; color: var(--text) ; } | |
| .art-cat-btn input[type="checkbox"] { display: none ; } | |
| .art-cat-btn label > span:first-child { display: none ; } | |
| .art-cat-btn:has(input:checked) label { | |
| background: var(--accent-soft) ; border-color: var(--chip-border) ; | |
| color: var(--chip-ink) ; box-shadow: 0 0 12px var(--focus-ring) ; | |
| } | |
| /* ---------- presets ---------- */ | |
| .art-preset-group-label { font-size: 12px ; font-weight: 600 ; color: var(--text) ; margin: 12px 0 8px ; } | |
| .art-preset-toggle .gr-block { display: flex ; flex-wrap: wrap ; gap: 8px ; } | |
| .art-preset-toggle label { | |
| display: inline-flex ; align-items: center ; | |
| border: 1px solid var(--panel-border) ; background: var(--surface-2) ; | |
| border-radius: var(--pill) ; padding: 6px 14px ; | |
| font-size: 12px ; cursor: pointer ; transition: all 0.15s ; user-select: none ; white-space: nowrap ; | |
| } | |
| .art-preset-toggle label:hover { border-color: var(--chip-border) ; } | |
| .art-preset-toggle label:has(input:checked) { | |
| border-color: var(--chip-border) ; background: var(--accent-soft) ; color: var(--chip-ink) ; | |
| } | |
| /* ---------- accordions ---------- */ | |
| .gradio-accordion { border: 1px solid var(--panel-border) ; border-radius: var(--r-md) ; background: transparent ; } | |
| .gradio-accordion > .label-wrap, .gradio-accordion > button, .gradio-accordion > summary { | |
| color: var(--text) ; font-weight: 600 ; font-size: 12.5px ; padding: 10px 14px ; cursor: pointer ; | |
| } | |
| .gradio-accordion > div:not(.label-wrap) { padding: 4px 14px 14px ; } | |
| /* ---------- slide-down panels ---------- */ | |
| .art-slide-panel { overflow: hidden ; max-height: 0 ; opacity: 0 ; padding: 0 0 0 12px ; margin: 0 ; | |
| border-left: 2px solid var(--chip-border) ; | |
| transition: max-height 0.25s ease-out, opacity 0.2s ease-out, padding 0.2s ; } | |
| .art-slide-open { max-height: 2200px ; opacity: 1 ; padding: 8px 0 8px 12px ; margin-top: 8px ; } | |
| .art-slide-trigger { | |
| display: flex ; align-items: center ; justify-content: space-between ; gap: 8px ; | |
| width: 100% ; min-height: 34px ; box-sizing: border-box ; | |
| padding: 8px 14px ; border: 1px solid var(--panel-border) ; | |
| background: var(--surface-2) ; border-radius: var(--r-md) ; | |
| color: var(--muted) ; font-size: 12.5px ; font-weight: 600 ; | |
| line-height: 1.2 ; cursor: pointer ; text-align: left ; | |
| transition: all 0.15s ; | |
| } | |
| .art-slide-trigger:hover { border-color: var(--chip-border) ; color: var(--text) ; } | |
| .art-slide-trigger.art-slide-active { | |
| border-color: var(--chip-border) ; color: var(--chip-ink) ; | |
| background: var(--accent-soft) ; | |
| } | |
| /* ---------- results ---------- */ | |
| .art-result-toolbar { display: flex ; gap: 8px ; flex-wrap: wrap ; margin: 4px 0 12px ; } | |
| .art-result-toolbar button { border-radius: var(--pill) ; font-size: 11.5px ; font-weight: 700 ; } | |
| .art-result-card { padding: 12px 14px ; margin: 8px 0 ; } | |
| .art-result-card:hover { border-color: var(--chip-border) ; } | |
| .art-result-num { background: var(--accent-soft) ; color: var(--chip-ink) ; font-weight: 700 ; | |
| font-size: 11px ; padding: 4px 10px ; border-radius: var(--r-sm) ; | |
| border: 1px solid var(--chip-border) ; text-align: center ; } | |
| .art-result-text, .art-neg-text { background: transparent ; border: none ; | |
| font-family: var(--mono) ; color: var(--text) ; padding: 4px 0 ; } | |
| .art-result-text { font-size: 12.5px ; line-height: 1.6 ; } | |
| .art-neg-text { font-size: 11px ; line-height: 1.5 ; color: var(--muted) ; } | |
| .art-history-select label { border-radius: var(--r-md) ; } | |
| .art-strip-row { gap: 8px 16px ; flex-wrap: wrap ; } | |
| /* ---------- info, suggest, tooltips ---------- */ | |
| .art-info-text, .gradio-container .info { | |
| color: var(--muted) ; font-size: 11.5px ; line-height: 1.5 ; | |
| border-left: 2px solid var(--chip-border) ; background: var(--surface-2) ; | |
| padding: 7px 12px ; border-radius: 0 var(--r-sm) var(--r-sm) 0 ; margin-top: 8px ; | |
| } | |
| /* Per-field help text. Gradio renders it in its own colour/size, which made | |
| field descriptions read as a different kind of text from the panel notes | |
| above them. One line-height and colour for all of them. */ | |
| .gradio-container label + .info, .gradio-container .info:not(.art-info-text) { | |
| color: var(--muted) ; font-size: 11px ; line-height: 1.45 ; | |
| text-align: left ; margin-top: 4px ; padding: 0 2px ; | |
| background: none ; border: none ; | |
| } | |
| .art-suggest-box { padding: 12px 16px ; } | |
| .art-suggest-title { font-size: 12.5px ; font-weight: 700 ; color: var(--text) ; margin-bottom: 8px ; } | |
| .art-suggest-box ul { margin: 0 ; padding-left: 18px ; } | |
| .art-suggest-item { font-size: 12.5px ; color: var(--muted) ; line-height: 1.6 ; padding: 2px 0 ; } | |
| .art-tooltip { position: relative ; } | |
| .art-tooltip-text { | |
| visibility: hidden ; opacity: 0 ; position: absolute ; z-index: 9999 ; | |
| padding: 4px 10px ; background: var(--panel) ; color: var(--text) ; | |
| font-size: 10.5px ; font-weight: 600 ; border-radius: var(--r-sm) ; | |
| border: 1px solid var(--panel-border) ; white-space: nowrap ; pointer-events: none ; | |
| bottom: 110% ; left: 50% ; transform: translateX(-50%) ; | |
| } | |
| .art-tooltip:hover .art-tooltip-text { visibility: visible ; opacity: 1 ; } | |
| /* ---------- tabs ---------- | |
| .tabs is a block box; the buttons live in an inner .tab-container. Setting | |
| `gap` on .tabs did nothing (gap needs flex/grid), which is why the tab row | |
| came out cramped. | |
| The button rule is scoped to that container on purpose: every component in | |
| the app is nested somewhere inside .tabs, so a bare `.tabs button` also hit | |
| accordions, slide triggers and the action bar and left them at mismatched | |
| heights. The visually-hidden container is an a11y duplicate of the same | |
| buttons and is excluded so only the real row is styled. */ | |
| .tabs { border-bottom: 1px solid var(--line-soft) ; padding-bottom: 8px ; margin-bottom: 4px ; } | |
| /* Gradio sizes .tab-container at 32px while the pill rule below gives the | |
| buttons 34px, and `overflow-x: hidden` forces `overflow-y` off `visible`, so | |
| the two-pixel excess was simply cut: the selected pill lost a pixel off its | |
| top and its bottom, flattening the radius. Give the row its own height and | |
| stop it clipping. The min-height, not height:auto, so a future Gradio value | |
| can still grow the row but never shrink it under its own buttons. */ | |
| .tabs .tab-container { display: flex ; flex-wrap: wrap ; gap: 6px ; | |
| align-items: center ; min-height: 34px ; overflow: visible ; } | |
| .tabs .tab-container:not(.visually-hidden) > button { | |
| display: inline-flex ; align-items: center ; justify-content: center ; | |
| height: 34px ; min-height: 34px ; box-sizing: border-box ; | |
| border-radius: var(--pill) ; padding: 0 18px ; | |
| font-size: 12.5px ; font-weight: 600 ; line-height: 1 ; | |
| border: 1px solid transparent ; background: transparent ; | |
| color: var(--muted) ; white-space: nowrap ; | |
| } | |
| .tabs .tab-container:not(.visually-hidden) > button:hover { color: var(--text) ; background: var(--surface-2) ; } | |
| .tabs .tab-container:not(.visually-hidden) > button.selected { background: var(--accent-soft) ; border-color: var(--chip-border) ; color: var(--chip-ink) ; } | |
| .tabitem { border: none ; padding: 4px 0 0 ; } | |
| /* ---------- tagger ---------- */ | |
| .art-tagger-split { gap: 12px ; align-items: flex-start ; } | |
| .art-tagger-image { | |
| border-radius: var(--radius) ; overflow: hidden ; | |
| border: 1px solid var(--panel-border) ; background: var(--preview-bg) ; | |
| } | |
| .art-tagger-run-btn button, .art-tagger-apply-btn button { | |
| background: linear-gradient(135deg, var(--grad-1), var(--accent) 52%, var(--grad-2)) ; | |
| border: none ; color: var(--accent-ink) ; font-weight: 600 ; border-radius: var(--pill) ; | |
| box-shadow: 0 2px 8px color-mix(in srgb, var(--btn-shadow) 50%, transparent) ; | |
| } | |
| .art-tagger-copy-btn button { background: var(--surface-2) ; border: 1px solid var(--panel-border) ; color: var(--text) ; border-radius: var(--pill) ; } | |
| .art-tagger-tags-box textarea { font-family: var(--mono) ; font-size: 12px ; line-height: 1.55 ; } | |
| .art-tagger-chips .wrap, .art-tagger-chips .grid { display: flex ; flex-wrap: wrap ; gap: 8px ; max-height: 260px ; overflow-y: auto ; padding: 4px 2px 8px ; } | |
| .art-tagger-chips label { | |
| display: inline-flex ; align-items: center ; gap: 6px ; | |
| border: 1px solid var(--panel-border) ; background: var(--surface-2) ; | |
| border-radius: var(--pill) ; padding: 5px 12px ; font-size: 12px ; cursor: pointer ; | |
| } | |
| .art-tagger-chips label:hover { border-color: var(--chip-border) ; } | |
| .art-tagger-chips label:has(input:checked) { border-color: var(--chip-border) ; background: var(--accent-soft) ; color: var(--chip-ink) ; } | |
| /* A header is a colour tick, the label, and the number of rows below it -- | |
| with eighteen sections stacked in one column the count is what makes the | |
| panel scannable; display:flex is what lets the badge park on the right. */ | |
| .art-tagger-panel-title { | |
| display: flex ; align-items: center ; gap: 8px ; | |
| font-size: 12px ; font-weight: 700 ; color: var(--text) ; | |
| margin: 14px 0 7px ; | |
| } | |
| .art-tagger-panel-title::before { | |
| content: "" ; flex: none ; width: 3px ; height: 12px ; | |
| border-radius: var(--pill) ; background: var(--accent) ; | |
| } | |
| .art-tagger-count { | |
| margin-left: auto ; flex: none ; line-height: 1.5 ; | |
| font-family: var(--mono) ; font-size: 10px ; font-weight: 600 ; | |
| color: var(--muted) ; background: var(--surface-2) ; | |
| border: 1px solid var(--line-soft) ; border-radius: var(--pill) ; | |
| padding: 0 7px ; | |
| } | |
| /* The panel already pads itself by 16px, so the first header of the first | |
| zone must not open with a top margin on top of that. */ | |
| .art-tagger-zone:first-child .art-tagger-panel-title:first-child { margin-top: 0 ; } | |
| /* Zones are split off by a hairline so a long result reads as three panels | |
| -- caption, tags, analysis -- instead of one undifferentiated column. */ | |
| .art-tagger-zone + .art-tagger-zone { | |
| border-top: 1px solid var(--line-soft) ; | |
| margin-top: 14px ; padding-top: 2px ; | |
| } | |
| .art-tagger-rating-row { display: flex ; align-items: center ; gap: 8px ; font-size: 11px ; color: var(--muted) ; margin-bottom: 5px ; } | |
| /* The tier chip used to paint --muted straight onto --ok/--amber/--danger: | |
| 1.01-1.80:1 across all five themes (AA wants 4.5), so the PG/PG13/R labels | |
| were illegible in every one of them. The tone class stays on the element | |
| but is painted as a decorative dot instead of a background; the label sits | |
| on the neutral row, where --text measures 12-17:1. The `.art-tagger-panel` | |
| prefix is deliberate: it raises specificity above the single-class | |
| .art-rating-* fill rule, so the override holds whatever the source order | |
| and does not depend on `!important` tie-breaking. */ | |
| .art-tagger-panel .art-tagger-rating-name { | |
| width: 92px ; flex: none ; box-sizing: border-box ; | |
| font-weight: 700 ; display: inline-flex ; | |
| align-items: center ; gap: 6px ; | |
| color: var(--text) ; background: var(--surface-2) ; | |
| border: 1px solid var(--line-soft) ; border-radius: var(--pill) ; | |
| padding: 1px 7px ; | |
| } | |
| .art-tagger-panel .art-tagger-rating-name[class*="art-rating-"]::before { | |
| content: "" ; flex: none ; width: 8px ; height: 8px ; | |
| border-radius: 50% ; | |
| } | |
| .art-tagger-panel .art-tagger-rating-name.art-rating-pg::before { background: var(--ok) ; } | |
| .art-tagger-panel .art-tagger-rating-name.art-rating-pg13::before { background: var(--amber) ; } | |
| .art-tagger-panel .art-tagger-rating-name.art-rating-pg16::before { background: color-mix(in srgb, var(--amber) 70%, var(--danger)) ; } | |
| .art-tagger-panel .art-tagger-rating-name.art-rating-r::before, | |
| .art-tagger-panel .art-tagger-rating-name.art-rating-r5::before { background: var(--danger) ; } | |
| .art-tagger-rating-track { flex: 1 ; height: 6px ; background: var(--surface-2) ; border: 1px solid var(--panel-border) ; border-radius: var(--pill) ; overflow: hidden ; } | |
| .art-tagger-rating-fill { display: block ; height: 100% ; border-radius: var(--pill) ; } | |
| .art-tagger-rating-pct { color: var(--faint) ; font-family: var(--mono) ; } | |
| .art-tagger-badges { display: flex ; flex-wrap: wrap ; gap: 6px ; } | |
| .art-tagger-char-badge { background: var(--accent-soft) ; border: 1px solid var(--chip-border) ; | |
| border-radius: var(--pill) ; padding: 3px 12px ; font-size: 12px ; color: var(--text) ; } | |
| .art-tagger-badge-pct { color: var(--accent-2) ; font-size: 10px ; font-family: var(--mono) ; } | |
| .art-tagger-gen-list { display: flex ; flex-direction: column ; gap: 3px ; } | |
| .art-tagger-gen-row { display: flex ; align-items: center ; gap: 8px ; font-size: 11.5px ; color: var(--muted) ; } | |
| /* The tag name is the payload of the row and the bar already carries the | |
| rank, so the name can afford to be primary rather than the muted grey it | |
| used to inherit from the row. */ | |
| .art-tagger-gen-name { color: var(--text) ; font-weight: 500 ; } | |
| .art-tagger-gen-track { width: 90px ; height: 5px ; background: var(--surface-2) ; border: 1px solid var(--panel-border) ; border-radius: var(--pill) ; overflow: hidden ; flex: none ; } | |
| .art-tagger-gen-fill { display: block ; height: 100% ; background: linear-gradient(90deg, var(--grad-1), var(--accent)) ; } | |
| /* The caption is the panel's only prose and the only output meant to be | |
| pasted as-is, so it is the one card with an accent edge and body-sized | |
| text. It used to share this very rule with the tag metadata below it -- | |
| which is exactly how the panel ended up reading as eighteen interchangeable | |
| grey blocks. */ | |
| .art-tagger-caption { | |
| background: var(--surface-2) ; border: 1px solid var(--line-soft) ; | |
| border-left: 3px solid var(--accent) ; border-radius: var(--r-md) ; | |
| padding: 11px 13px 12px ; | |
| } | |
| .art-tagger-caption-label { | |
| font-size: 10.5px ; font-weight: 700 ; letter-spacing: 0.7px ; | |
| text-transform: uppercase ; color: var(--muted) ; margin-bottom: 5px ; | |
| } | |
| .art-tagger-nl-caption { | |
| font-size: 13.5px ; line-height: 1.62 ; color: var(--text) ; | |
| white-space: pre-wrap ; overflow-wrap: anywhere ; | |
| } | |
| /* One rule styles every tag section. Four of them (camera, action, | |
| face-retag, caption-bridge) previously had no CSS at all and rendered as | |
| bare text beside their six boxed neighbours. */ | |
| .art-tagger-taglist { | |
| display: flex ; flex-wrap: wrap ; gap: 5px ; margin-top: 6px ; | |
| } | |
| .art-tagger-tag { | |
| display: inline-flex ; align-items: center ; max-width: 100% ; | |
| background: var(--surface-2) ; border: 1px solid var(--line-soft) ; | |
| border-radius: var(--pill) ; padding: 3px 9px ; | |
| font-size: 11.5px ; line-height: 1.45 ; color: var(--muted) ; | |
| } | |
| .art-tagger-note { margin-top: 8px ; font-size: 11.5px ; } | |
| .art-tagger-pose-people { font-size: 11.5px ; color: var(--text) ; font-weight: 600 ; } | |
| .art-tagger-control-img { border-radius: var(--r-md) ; overflow: hidden ; border: 1px solid var(--panel-border) ; } | |
| .art-batch-wrap { max-height: 520px ; overflow-y: auto ; padding: 4px 2px ; } | |
| .art-batch-actions { display: flex ; align-items: center ; justify-content: space-between ; gap: 8px ; margin-bottom: 10px ; } | |
| .art-batch-count { font-size: 11.5px ; color: var(--muted) ; } | |
| .art-batch-copyall, .art-batch-copy { border: 1px solid var(--panel-border) ; background: var(--surface-2) ; | |
| color: var(--muted) ; border-radius: var(--pill) ; cursor: pointer ; font-size: 11px ; padding: 4px 12px ; } | |
| .art-batch-copyall:hover, .art-batch-copy:hover { border-color: var(--chip-border) ; color: var(--chip-ink) ; } | |
| .art-batch-grid { display: grid ; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)) ; gap: 12px ; } | |
| .art-batch-card { background: var(--panel) ; border: 1px solid var(--panel-border) ; border-radius: var(--r-md) ; padding: 10px ; display: flex ; flex-direction: column ; gap: 8px ; } | |
| .art-batch-thumb { width: 100% ; height: 160px ; object-fit: cover ; border-radius: var(--r-sm) ; } | |
| .art-batch-card-head { display: flex ; align-items: center ; justify-content: space-between ; gap: 8px ; } | |
| .art-batch-card-name { font-size: 11.5px ; font-weight: 600 ; color: var(--text) ; overflow: hidden ; text-overflow: ellipsis ; white-space: nowrap ; } | |
| .art-batch-card-tags { font-size: 11px ; color: var(--muted) ; max-height: 90px ; overflow-y: auto ; | |
| background: var(--surface-2) ; border: 1px solid var(--line-soft) ; border-radius: var(--r-sm) ; padding: 7px 9px ; line-height: 1.5 ; } | |
| .art-batch-fullrow, .art-dataset-fullrow { margin-top: 12px ; } | |
| /* ---------- artist / misc ---------- */ | |
| .art-artist-preview { background: var(--panel) ; border: 1px solid var(--panel-border) ; border-radius: var(--r-md) ; padding: 12px 16px ; margin-top: 12px ; } | |
| .art-artist-preview-title { font-weight: 600 ; font-size: 13px ; color: var(--text) ; margin-bottom: 6px ; } | |
| .art-artist-preview-empty { color: var(--muted) ; font-size: 12px ; font-style: italic ; } | |
| .art-artist-link { display: inline-flex ; align-items: center ; gap: 4px ; border: 1px solid var(--panel-border) ; | |
| border-radius: var(--r-sm) ; padding: 4px 10px ; background: var(--surface-2) ; | |
| color: var(--text) ; text-decoration: none ; font-size: 11px ; font-weight: 600 ; } | |
| [role="listbox"], ul.options, [data-testid="dropdown-options"] { | |
| z-index: 999999 ; background: var(--input) ; | |
| border: 1px solid var(--chip-border) ; border-radius: 10px ; | |
| box-shadow: 0 8px 32px var(--shadow) ; | |
| } | |
| [role="option"] { padding: 8px 14px ; font-size: 13px ; color: var(--text) ; cursor: pointer ; } | |
| [role="option"]:hover { background: var(--accent-soft) ; color: var(--chip-ink) ; } | |
| .gradio-dropdown, .gradio-slider, .gradio-radio, .gradio-checkboxgroup, .gradio-accordion { border-radius: var(--r-md) ; } | |
| input[type="range"]::-webkit-slider-runnable-track { height: 4px ; background: var(--track) ; border-radius: 2px ; } | |
| input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none ; width: 16px ; height: 16px ; border-radius: 50% ; | |
| background: linear-gradient(135deg, var(--grad-1), var(--grad-2)) ; cursor: pointer ; margin-top: -6px ; } | |
| /* ---------- Gradio variable bridge ---------- | |
| Gradio bakes its own palette into ~40 CSS variables (see theme/art_theme.py) | |
| and paints most framework widgets from those, not from our classes. Left | |
| alone, a theme switch only restyles our own elements and the app turns into | |
| a mix -- e.g. card titles stayed #CBD5E1 (light slate), which is unreadable | |
| on the cream theme's light panel. | |
| Declared on body and the theme wrapper, NOT on :root, so that var(--text) | |
| resolves against the *active* theme instead of the :root default. | |
| The `color` property itself is set too: Gradio 6 paints it as a literal on | |
| body.dark (#E2E8F0) and .gradio-container (#CBD5E1), so overriding only the | |
| variables leaves those two hard-coded values in place. */ | |
| body, body.dark, body .dark, body .gradio-container { | |
| color: var(--text) ; | |
| --body-background-fill: var(--bg) ; | |
| --body-background-fill-dark: var(--bg) ; | |
| --body-text-color: var(--text) ; | |
| --body-text-color-dark: var(--text) ; | |
| --body-text-color-subdued: var(--muted) ; | |
| --body-text-color-subdued-dark: var(--muted) ; | |
| --block-title-text-color: var(--text) ; | |
| --block-title-text-color-dark: var(--text) ; | |
| --block-label-text-color: var(--muted) ; | |
| --block-label-text-color-dark: var(--muted) ; | |
| /* ...and their background counterparts. Gradio's default sets these to a | |
| solid cyan (#0891B2), which painted a bright teal pill behind every | |
| field label that has an info tooltip -- "General Threshold", | |
| "Tag format", "Max images" and so on. Labels stay flat. */ | |
| --block-title-background-fill: transparent ; | |
| --block-title-background-fill-dark: transparent ; | |
| --block-label-background-fill: transparent ; | |
| --block-label-background-fill-dark: transparent ; | |
| --checkbox-label-background-fill-selected: var(--accent-soft) ; | |
| --stat-background-fill: var(--surface-2) ; | |
| --button-primary-background-fill: var(--accent) ; | |
| --button-primary-background-fill-hover: var(--accent) ; | |
| --button-primary-text-color: var(--accent-ink) ; | |
| --button-primary-text-color-dark: var(--accent-ink) ; | |
| --block-background-fill: var(--panel) ; | |
| --block-background-fill-dark: var(--panel) ; | |
| --block-border-color: var(--panel-border) ; | |
| --block-border-color-dark: var(--panel-border) ; | |
| --input-background-fill: var(--input) ; | |
| --input-background-fill-dark: var(--input) ; | |
| --input-border-color: var(--input-border) ; | |
| --input-border-color-dark: var(--input-border) ; | |
| --background-fill-primary: var(--surface-2) ; | |
| --background-fill-primary-dark: var(--surface-2) ; | |
| --background-fill-secondary: var(--surface-2) ; | |
| --background-fill-secondary-dark: var(--surface-2) ; | |
| --border-color-primary: var(--panel-border) ; | |
| --border-color-primary-dark: var(--panel-border) ; | |
| --color-accent: var(--accent) ; | |
| --color-accent-dark: var(--accent) ; | |
| --color-accent-soft: var(--accent-soft) ; | |
| --table-border-color: var(--panel-border) ; | |
| --table-even-background-fill: var(--panel) ; | |
| --table-odd-background-fill: var(--surface-2) ; | |
| } | |
| /* ---------- responsive ---------- */ | |
| @media (max-width: 1023px) { | |
| .gradio-container { padding: 12px 12px 40px ; } | |
| .art-title { font-size: 22px ; } | |
| .art-actionbar { bottom: 8px ; padding: 8px 10px ; } | |
| .art-hotkeys { display: none ; } | |
| #root .art-result-card { flex-wrap: wrap ; } | |
| .art-cat-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)) ; } | |
| .art-cat-btn label { font-size: 10px ; min-height: 34px ; } | |
| } | |
| /* Header wrapping. Below 860px the brand (313px floor) and the switcher column | |
| (640px) genuinely cannot share the non-wrapping row, so the switchers drop to | |
| their own full-width row. The breakpoint is 860 rather than 1023 on purpose: | |
| at 1000px both still fit on one line, and wrapping there cost 100px of | |
| sticky header for nothing. */ | |
| @media (max-width: 860px) { | |
| .art-topbar { flex-wrap: wrap ; row-gap: 8px ; } | |
| .art-head-side { min-width: 100% ; width: 100% ; } | |
| /* The two clusters are flex siblings sharing the row equally (1 1 0), which | |
| left the theme picker 317px -- too narrow for five chips. Give it the | |
| full row and let the language picker share the line below it. */ | |
| .art-controls { flex-wrap: wrap ; } | |
| .art-theme-btn { flex: 1 1 100% ; } | |
| .art-lang-btn { flex: 0 1 auto ; } | |
| .art-theme-btn label, .art-lang-btn label { padding: 0 8px ; font-size: 10.5px ; } | |
| } | |