feat: add Fishbowl phosphor theme + CSS/head assets
Browse filesPort the ui/raw aquatic-CRT prototype styling into a modular Gradio
styling surface under src/ui/fishbowl/:
- assets/styles.css: full ported stylesheet (styles.css + show.css),
scoped under a .fishbowl root so it survives Gradio's cascade, with
CRT overlay layers left full-bleed; hides the default footer and
paints .gradio-container phosphor-dark.
- assets/head.html: Google Fonts links for Martian Mono (display) +
IBM Plex Mono (body), passed to demo.launch(head=...).
- theme.py: FishbowlTheme(gr.themes.Base) mapping the dark phosphor
palette onto Gradio theme vars, plus import-safe load_css() and
FISHBOWL_HEAD helpers.
- tests/test_fishbowl_theme.py: mock-free assertions on classes, CSS
vars, fonts, and construct-without-launch.
Co-Authored-By: Codex <codex@openai.com>
- src/ui/fishbowl/assets/head.html +25 -0
- src/ui/fishbowl/assets/styles.css +655 -0
- src/ui/fishbowl/theme.py +120 -0
- tests/test_fishbowl_theme.py +105 -0
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- FISHBOWL — <head> injection for demo.launch(head=...)
|
| 2 |
+
|
| 3 |
+
Loads the two display/body fonts the phosphor theme relies on:
|
| 4 |
+
* Martian Mono — display (logos, eyebrows, headings, tags)
|
| 5 |
+
* IBM Plex Mono — body (everything else, incl. italic thoughts)
|
| 6 |
+
|
| 7 |
+
The CRT atmosphere is NOT defined here. The app shell (Unit 9)
|
| 8 |
+
emits the overlay layers as raw HTML inside the gr.Blocks body so
|
| 9 |
+
they live in the same DOM as the theater, e.g.:
|
| 10 |
+
|
| 11 |
+
<div class="crt-bg"></div>
|
| 12 |
+
<div class="crt-grid"></div>
|
| 13 |
+
<div class="crt-scan"></div>
|
| 14 |
+
<div class="crt-vignette"></div>
|
| 15 |
+
|
| 16 |
+
and wraps the live stage in <div class="crt-flicker">…</div>.
|
| 17 |
+
Styling for all of those lives in assets/styles.css. -->
|
| 18 |
+
<meta name="color-scheme" content="dark" />
|
| 19 |
+
<meta name="theme-color" content="#05121a" />
|
| 20 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 21 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 22 |
+
<link
|
| 23 |
+
href="https://fonts.googleapis.com/css2?family=Martian+Mono:wght@400;500;600;700;800&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400;1,500&display=swap"
|
| 24 |
+
rel="stylesheet"
|
| 25 |
+
/>
|
|
@@ -0,0 +1,655 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================
|
| 2 |
+
FISHBOWL — aquatic CRT lab readout
|
| 3 |
+
monospace, phosphor, glassy, scientific
|
| 4 |
+
|
| 5 |
+
Ported from ui/raw/styles.css + ui/raw/show.css for injection
|
| 6 |
+
into a gr.Blocks app via `css=`. Gradio wraps everything in
|
| 7 |
+
`.gradio-container`, so the theater markup is wrapped in a
|
| 8 |
+
`.fishbowl` root and the component rules are scoped under it to
|
| 9 |
+
survive Gradio's own cascade. The CSS custom properties live on
|
| 10 |
+
`:root` so they remain available everywhere (incl. shadow-free
|
| 11 |
+
gr.HTML islands), and the CRT overlay layers are fixed full-bleed
|
| 12 |
+
so they sit behind / above the app shell regardless of scope.
|
| 13 |
+
============================================================ */
|
| 14 |
+
|
| 15 |
+
:root {
|
| 16 |
+
/* aquatic darks */
|
| 17 |
+
--bg-0: #05121a; /* deepest tank */
|
| 18 |
+
--bg-1: #07181f;
|
| 19 |
+
--bg-2: #0a222b;
|
| 20 |
+
--panel: rgba(13, 39, 47, 0.55);
|
| 21 |
+
--panel-solid: #0c2730;
|
| 22 |
+
--glass: rgba(120, 222, 214, 0.04);
|
| 23 |
+
|
| 24 |
+
/* phosphor inks */
|
| 25 |
+
--ink: #c4f6ee;
|
| 26 |
+
--ink-mid: #79c3bb;
|
| 27 |
+
--ink-dim: #4d8983;
|
| 28 |
+
--ink-faint: #2f6962;
|
| 29 |
+
|
| 30 |
+
/* lines + grid */
|
| 31 |
+
--line: rgba(120, 222, 214, 0.16);
|
| 32 |
+
--line-soft: rgba(120, 222, 214, 0.08);
|
| 33 |
+
--line-hot: rgba(120, 222, 214, 0.42);
|
| 34 |
+
|
| 35 |
+
/* accents — shared chroma/lightness, varied hue */
|
| 36 |
+
--cyan: #4fe6d2;
|
| 37 |
+
--teal: #2bc4b4;
|
| 38 |
+
--blue: #5bb6ff;
|
| 39 |
+
--violet: #9b8cff;
|
| 40 |
+
|
| 41 |
+
/* signal colors (alarms / states) */
|
| 42 |
+
--amber: #ffcf6b; /* tension */
|
| 43 |
+
--coral: #ff8f7d; /* lie / danger */
|
| 44 |
+
--lime: #9be86b; /* truth / win */
|
| 45 |
+
|
| 46 |
+
--glow: 0 0 12px rgba(79, 230, 210, 0.35);
|
| 47 |
+
--glow-hot: 0 0 18px rgba(79, 230, 210, 0.6);
|
| 48 |
+
|
| 49 |
+
--font-display: "Martian Mono", ui-monospace, monospace;
|
| 50 |
+
--font-body: "IBM Plex Mono", ui-monospace, monospace;
|
| 51 |
+
|
| 52 |
+
--r: 4px;
|
| 53 |
+
--r-lg: 8px;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* ============================================================
|
| 57 |
+
Gradio container integration — paint the app phosphor-dark,
|
| 58 |
+
kill the default footer, and let the CRT layers show through.
|
| 59 |
+
============================================================ */
|
| 60 |
+
.gradio-container {
|
| 61 |
+
background: var(--bg-0) !important;
|
| 62 |
+
color: var(--ink) !important;
|
| 63 |
+
font-family: var(--font-body) !important;
|
| 64 |
+
max-width: 100% !important;
|
| 65 |
+
}
|
| 66 |
+
.gradio-container ::selection { background: rgba(79, 230, 210, 0.28); color: #fff; }
|
| 67 |
+
|
| 68 |
+
/* hide the default Gradio footer ("Use via API", settings, etc.) */
|
| 69 |
+
footer { display: none !important; }
|
| 70 |
+
.gradio-container footer { display: none !important; }
|
| 71 |
+
|
| 72 |
+
/* phosphor scrollbars (global — harmless outside the app) */
|
| 73 |
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
| 74 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 75 |
+
::-webkit-scrollbar-thumb {
|
| 76 |
+
background: rgba(79, 230, 210, 0.18);
|
| 77 |
+
border-radius: 10px;
|
| 78 |
+
border: 2px solid transparent;
|
| 79 |
+
background-clip: padding-box;
|
| 80 |
+
}
|
| 81 |
+
::-webkit-scrollbar-thumb:hover { background: rgba(79, 230, 210, 0.35); background-clip: padding-box; }
|
| 82 |
+
|
| 83 |
+
/* ============================================================
|
| 84 |
+
CRT atmosphere — scanlines, vignette, flicker
|
| 85 |
+
applied as fixed overlays so they never interfere w/ layout.
|
| 86 |
+
The app shell (Unit 9) emits these overlay divs as raw HTML:
|
| 87 |
+
|
| 88 |
+
<div class="crt-bg"></div>
|
| 89 |
+
<div class="crt-grid"></div>
|
| 90 |
+
<div class="crt-scan"></div>
|
| 91 |
+
<div class="crt-vignette"></div>
|
| 92 |
+
|
| 93 |
+
and wraps the live theater in <div class="crt-flicker"> so the
|
| 94 |
+
subtle phosphor flicker animation plays over the whole stage.
|
| 95 |
+
These are intentionally NOT scoped under .fishbowl: they are
|
| 96 |
+
position:fixed full-bleed layers that frame the entire viewport.
|
| 97 |
+
============================================================ */
|
| 98 |
+
.crt-bg {
|
| 99 |
+
position: fixed;
|
| 100 |
+
inset: 0;
|
| 101 |
+
z-index: 0;
|
| 102 |
+
pointer-events: none;
|
| 103 |
+
background:
|
| 104 |
+
radial-gradient(120% 90% at 50% -10%, rgba(43, 196, 180, 0.10), transparent 60%),
|
| 105 |
+
radial-gradient(100% 80% at 50% 120%, rgba(91, 182, 255, 0.07), transparent 55%),
|
| 106 |
+
var(--bg-0);
|
| 107 |
+
}
|
| 108 |
+
.crt-grid {
|
| 109 |
+
position: fixed;
|
| 110 |
+
inset: 0;
|
| 111 |
+
z-index: 0;
|
| 112 |
+
pointer-events: none;
|
| 113 |
+
background-image:
|
| 114 |
+
linear-gradient(var(--line-soft) 1px, transparent 1px),
|
| 115 |
+
linear-gradient(90deg, var(--line-soft) 1px, transparent 1px);
|
| 116 |
+
background-size: 44px 44px;
|
| 117 |
+
mask-image: radial-gradient(120% 100% at 50% 40%, #000 35%, transparent 92%);
|
| 118 |
+
opacity: 0.6;
|
| 119 |
+
}
|
| 120 |
+
.crt-scan {
|
| 121 |
+
position: fixed;
|
| 122 |
+
inset: 0;
|
| 123 |
+
z-index: 60;
|
| 124 |
+
pointer-events: none;
|
| 125 |
+
background: repeating-linear-gradient(
|
| 126 |
+
to bottom,
|
| 127 |
+
rgba(0, 0, 0, 0) 0px,
|
| 128 |
+
rgba(0, 0, 0, 0) 2px,
|
| 129 |
+
rgba(0, 0, 0, 0.16) 3px,
|
| 130 |
+
rgba(0, 0, 0, 0.16) 3px
|
| 131 |
+
);
|
| 132 |
+
mix-blend-mode: multiply;
|
| 133 |
+
opacity: 0.5;
|
| 134 |
+
}
|
| 135 |
+
.crt-vignette {
|
| 136 |
+
position: fixed;
|
| 137 |
+
inset: 0;
|
| 138 |
+
z-index: 61;
|
| 139 |
+
pointer-events: none;
|
| 140 |
+
box-shadow: inset 0 0 220px rgba(0,0,0,0.7), inset 0 0 60px rgba(0,0,0,0.4);
|
| 141 |
+
}
|
| 142 |
+
@media (prefers-reduced-motion: no-preference) {
|
| 143 |
+
.crt-flicker { animation: flicker 7s infinite steps(60); }
|
| 144 |
+
}
|
| 145 |
+
@keyframes flicker {
|
| 146 |
+
0%, 97%, 100% { opacity: 1; }
|
| 147 |
+
97.5% { opacity: 0.92; }
|
| 148 |
+
98% { opacity: 0.98; }
|
| 149 |
+
98.5% { opacity: 0.9; }
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* ============================================================
|
| 153 |
+
.fishbowl — scoped theater root. Everything the renderers emit
|
| 154 |
+
lives inside this wrapper so the rules win over Gradio defaults.
|
| 155 |
+
============================================================ */
|
| 156 |
+
.fishbowl {
|
| 157 |
+
position: relative;
|
| 158 |
+
z-index: 1;
|
| 159 |
+
font-family: var(--font-body);
|
| 160 |
+
font-size: 14px;
|
| 161 |
+
line-height: 1.5;
|
| 162 |
+
color: var(--ink);
|
| 163 |
+
-webkit-font-smoothing: antialiased;
|
| 164 |
+
}
|
| 165 |
+
.fishbowl * { box-sizing: border-box; }
|
| 166 |
+
|
| 167 |
+
/* ============================================================
|
| 168 |
+
typography helpers
|
| 169 |
+
============================================================ */
|
| 170 |
+
.fishbowl .mono { font-family: var(--font-body); }
|
| 171 |
+
.fishbowl .disp { font-family: var(--font-display); }
|
| 172 |
+
.fishbowl .eyebrow {
|
| 173 |
+
font-family: var(--font-display);
|
| 174 |
+
font-size: 9.5px;
|
| 175 |
+
letter-spacing: 0.32em;
|
| 176 |
+
text-transform: uppercase;
|
| 177 |
+
color: var(--ink-dim);
|
| 178 |
+
font-weight: 600;
|
| 179 |
+
}
|
| 180 |
+
.fishbowl .glow { text-shadow: var(--glow); }
|
| 181 |
+
.fishbowl .tnum { font-variant-numeric: tabular-nums; }
|
| 182 |
+
|
| 183 |
+
.fishbowl h1, .fishbowl h2, .fishbowl h3, .fishbowl h4 {
|
| 184 |
+
margin: 0; font-family: var(--font-display); font-weight: 700;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/* ============================================================
|
| 188 |
+
primitives
|
| 189 |
+
============================================================ */
|
| 190 |
+
.fishbowl .panel {
|
| 191 |
+
background: var(--panel);
|
| 192 |
+
border: 1px solid var(--line);
|
| 193 |
+
border-radius: var(--r-lg);
|
| 194 |
+
backdrop-filter: blur(8px);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.fishbowl .chip {
|
| 198 |
+
display: inline-flex;
|
| 199 |
+
align-items: center;
|
| 200 |
+
gap: 6px;
|
| 201 |
+
font-family: var(--font-display);
|
| 202 |
+
font-size: 9px;
|
| 203 |
+
letter-spacing: 0.14em;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
padding: 3px 8px;
|
| 206 |
+
border-radius: 999px;
|
| 207 |
+
border: 1px solid var(--line);
|
| 208 |
+
color: var(--ink-mid);
|
| 209 |
+
background: var(--glass);
|
| 210 |
+
white-space: nowrap;
|
| 211 |
+
}
|
| 212 |
+
.fishbowl .chip .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
| 213 |
+
|
| 214 |
+
.fishbowl .btn {
|
| 215 |
+
font-family: var(--font-display);
|
| 216 |
+
font-size: 11px;
|
| 217 |
+
letter-spacing: 0.14em;
|
| 218 |
+
text-transform: uppercase;
|
| 219 |
+
font-weight: 600;
|
| 220 |
+
color: var(--ink);
|
| 221 |
+
background: var(--glass);
|
| 222 |
+
border: 1px solid var(--line);
|
| 223 |
+
border-radius: var(--r);
|
| 224 |
+
padding: 9px 16px;
|
| 225 |
+
cursor: pointer;
|
| 226 |
+
transition: all 0.16s ease;
|
| 227 |
+
display: inline-flex;
|
| 228 |
+
align-items: center;
|
| 229 |
+
gap: 8px;
|
| 230 |
+
}
|
| 231 |
+
.fishbowl .btn:hover { border-color: var(--line-hot); color: #fff; box-shadow: var(--glow); }
|
| 232 |
+
.fishbowl .btn:active { transform: translateY(1px); }
|
| 233 |
+
.fishbowl .btn.primary {
|
| 234 |
+
background: linear-gradient(180deg, rgba(79,230,210,0.22), rgba(43,196,180,0.12));
|
| 235 |
+
border-color: var(--cyan);
|
| 236 |
+
color: var(--cyan);
|
| 237 |
+
text-shadow: var(--glow);
|
| 238 |
+
}
|
| 239 |
+
.fishbowl .btn.primary:hover { background: linear-gradient(180deg, rgba(79,230,210,0.34), rgba(43,196,180,0.2)); box-shadow: var(--glow-hot); }
|
| 240 |
+
.fishbowl .btn.ghost { background: transparent; }
|
| 241 |
+
.fishbowl .btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 242 |
+
|
| 243 |
+
.fishbowl .icon-btn {
|
| 244 |
+
width: 38px; height: 38px;
|
| 245 |
+
display: grid; place-items: center;
|
| 246 |
+
border: 1px solid var(--line);
|
| 247 |
+
background: var(--glass);
|
| 248 |
+
border-radius: var(--r);
|
| 249 |
+
color: var(--ink-mid);
|
| 250 |
+
cursor: pointer;
|
| 251 |
+
transition: all 0.16s ease;
|
| 252 |
+
}
|
| 253 |
+
.fishbowl .icon-btn:hover { border-color: var(--line-hot); color: var(--cyan); box-shadow: var(--glow); }
|
| 254 |
+
|
| 255 |
+
/* form controls — styled to look like crisp lab instruments */
|
| 256 |
+
.fishbowl .field { display: flex; flex-direction: column; gap: 6px; }
|
| 257 |
+
.fishbowl .field > label { font-family: var(--font-display); font-size: 9.5px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--ink-dim); }
|
| 258 |
+
.fishbowl input[type="text"], .fishbowl textarea, .fishbowl select {
|
| 259 |
+
font-family: var(--font-body);
|
| 260 |
+
font-size: 13px;
|
| 261 |
+
color: var(--ink);
|
| 262 |
+
background: rgba(5, 18, 26, 0.6);
|
| 263 |
+
border: 1px solid var(--line);
|
| 264 |
+
border-radius: var(--r);
|
| 265 |
+
padding: 9px 11px;
|
| 266 |
+
outline: none;
|
| 267 |
+
transition: all 0.16s ease;
|
| 268 |
+
width: 100%;
|
| 269 |
+
}
|
| 270 |
+
.fishbowl input:focus, .fishbowl textarea:focus, .fishbowl select:focus { border-color: var(--cyan); box-shadow: var(--glow); }
|
| 271 |
+
.fishbowl textarea { resize: vertical; line-height: 1.55; }
|
| 272 |
+
.fishbowl select { appearance: none; cursor: pointer;
|
| 273 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2379c3bb' stroke-width='1.4' fill='none'/%3E%3C/svg%3E");
|
| 274 |
+
background-repeat: no-repeat; background-position: right 11px center; padding-right: 28px;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/* range slider */
|
| 278 |
+
.fishbowl input[type="range"] {
|
| 279 |
+
-webkit-appearance: none; appearance: none;
|
| 280 |
+
width: 100%; height: 3px; background: var(--line); border-radius: 3px; outline: none; cursor: pointer;
|
| 281 |
+
}
|
| 282 |
+
.fishbowl input[type="range"]::-webkit-slider-thumb {
|
| 283 |
+
-webkit-appearance: none; appearance: none;
|
| 284 |
+
width: 14px; height: 14px; border-radius: 50%;
|
| 285 |
+
background: var(--cyan); box-shadow: var(--glow); border: 2px solid var(--bg-1); cursor: pointer;
|
| 286 |
+
}
|
| 287 |
+
.fishbowl input[type="range"]::-moz-range-thumb {
|
| 288 |
+
width: 14px; height: 14px; border-radius: 50%;
|
| 289 |
+
background: var(--cyan); box-shadow: var(--glow); border: 2px solid var(--bg-1); cursor: pointer;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
/* toggle switch */
|
| 293 |
+
.fishbowl .switch { position: relative; width: 46px; height: 24px; flex: none; cursor: pointer; }
|
| 294 |
+
.fishbowl .switch input { display: none; }
|
| 295 |
+
.fishbowl .switch .track {
|
| 296 |
+
position: absolute; inset: 0; border-radius: 999px;
|
| 297 |
+
background: rgba(5,18,26,0.8); border: 1px solid var(--line); transition: all 0.2s ease;
|
| 298 |
+
}
|
| 299 |
+
.fishbowl .switch .knob {
|
| 300 |
+
position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%;
|
| 301 |
+
background: var(--ink-dim); transition: all 0.2s ease;
|
| 302 |
+
}
|
| 303 |
+
.fishbowl .switch input:checked + .track { border-color: var(--cyan); background: rgba(79,230,210,0.16); box-shadow: var(--glow); }
|
| 304 |
+
.fishbowl .switch input:checked + .track + .knob { left: 25px; background: var(--cyan); box-shadow: var(--glow); }
|
| 305 |
+
|
| 306 |
+
.fishbowl .divider { height: 1px; background: var(--line-soft); border: 0; margin: 0; }
|
| 307 |
+
|
| 308 |
+
/* scrollable region */
|
| 309 |
+
.fishbowl .scroll { overflow-y: auto; overflow-x: hidden; }
|
| 310 |
+
|
| 311 |
+
/* animation utility keyframes used across components */
|
| 312 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 313 |
+
@keyframes blink { 50% { opacity: 0; } }
|
| 314 |
+
.fishbowl .caret::after { content: "▌"; color: var(--cyan); animation: blink 1s steps(1) infinite; }
|
| 315 |
+
|
| 316 |
+
/* ============================================================
|
| 317 |
+
avatar life
|
| 318 |
+
============================================================ */
|
| 319 |
+
@media (prefers-reduced-motion: no-preference) {
|
| 320 |
+
.fishbowl .av-ring { animation: avRing 2.4s ease-out infinite; transform-origin: 50% 50%; }
|
| 321 |
+
.fishbowl .av-blink { animation: avBlink 3.2s infinite; }
|
| 322 |
+
.fishbowl .av-sweat { animation: avSweat 2.6s ease-in infinite; transform-origin: 74px 24px; }
|
| 323 |
+
.fishbowl .av-gasp { animation: avGasp 0.8s ease-in-out infinite; transform-origin: 50px 65px; }
|
| 324 |
+
.fishbowl .av-thinking { animation: avBob 2.8s ease-in-out infinite; }
|
| 325 |
+
.fishbowl .av-panic { animation: avShake 0.32s linear infinite; }
|
| 326 |
+
.fishbowl .av-smug, .fishbowl .av-gossip { animation: avTilt 4s ease-in-out infinite; }
|
| 327 |
+
.fishbowl .av-truth, .fishbowl .av-calm { animation: avFloat 5s ease-in-out infinite; }
|
| 328 |
+
}
|
| 329 |
+
@keyframes avRing { 0% { transform: scale(0.85); opacity: 0.6; } 100% { transform: scale(1.25); opacity: 0; } }
|
| 330 |
+
@keyframes avBlink { 0%, 94%, 100% { transform: scaleY(1); } 97% { transform: scaleY(0.1); } }
|
| 331 |
+
@keyframes avSweat { 0%, 55% { transform: translateY(0); opacity: 0; } 65% { opacity: 1; } 100% { transform: translateY(14px); opacity: 0; } }
|
| 332 |
+
@keyframes avGasp { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.25); } }
|
| 333 |
+
@keyframes avBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
|
| 334 |
+
@keyframes avShake { 0%,100% { transform: translateX(0) rotate(0); } 25% { transform: translateX(-1.5px) rotate(-1.5deg); } 75% { transform: translateX(1.5px) rotate(1.5deg); } }
|
| 335 |
+
@keyframes avTilt { 0%, 100% { transform: rotate(-2deg); } 50% { transform: rotate(2deg); } }
|
| 336 |
+
@keyframes avFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
|
| 337 |
+
|
| 338 |
+
/* gradio mapping annotation */
|
| 339 |
+
.fishbowl .gr-tag {
|
| 340 |
+
font-family: var(--font-body);
|
| 341 |
+
font-size: 9px;
|
| 342 |
+
letter-spacing: 0.04em;
|
| 343 |
+
color: var(--blue);
|
| 344 |
+
background: rgba(91, 182, 255, 0.1);
|
| 345 |
+
border: 1px solid rgba(91, 182, 255, 0.3);
|
| 346 |
+
border-radius: 3px;
|
| 347 |
+
padding: 1px 5px;
|
| 348 |
+
white-space: nowrap;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
/* ============================================================
|
| 352 |
+
app shell — gradio-style tab header
|
| 353 |
+
============================================================ */
|
| 354 |
+
.fishbowl .app { display: flex; flex-direction: column; min-height: 100vh; position: relative; z-index: 1; }
|
| 355 |
+
.fishbowl .topbar {
|
| 356 |
+
display: flex; align-items: center; gap: 18px;
|
| 357 |
+
padding: 12px 22px;
|
| 358 |
+
border-bottom: 1px solid var(--line);
|
| 359 |
+
background: rgba(6, 20, 27, 0.7);
|
| 360 |
+
backdrop-filter: blur(10px);
|
| 361 |
+
flex: none;
|
| 362 |
+
}
|
| 363 |
+
.fishbowl .brand { display: flex; align-items: baseline; gap: 10px; }
|
| 364 |
+
.fishbowl .brand .logo {
|
| 365 |
+
font-family: var(--font-display); font-weight: 800; font-size: 19px; letter-spacing: 0.12em;
|
| 366 |
+
color: var(--cyan); text-shadow: var(--glow-hot);
|
| 367 |
+
}
|
| 368 |
+
.fishbowl .brand .sub { font-size: 10px; letter-spacing: 0.06em; color: var(--ink-dim); }
|
| 369 |
+
|
| 370 |
+
.fishbowl .tabs { display: flex; gap: 4px; background: rgba(5,18,26,0.6); border: 1px solid var(--line); border-radius: var(--r); padding: 3px; }
|
| 371 |
+
.fishbowl .tab {
|
| 372 |
+
font-family: var(--font-display); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 600;
|
| 373 |
+
color: var(--ink-dim); padding: 7px 16px; border-radius: 3px; cursor: pointer; transition: all 0.16s ease;
|
| 374 |
+
display: flex; align-items: center; gap: 8px; border: 1px solid transparent;
|
| 375 |
+
}
|
| 376 |
+
.fishbowl .tab:hover { color: var(--ink-mid); }
|
| 377 |
+
.fishbowl .tab.active { color: var(--cyan); background: var(--glass); border-color: var(--line); text-shadow: var(--glow); }
|
| 378 |
+
|
| 379 |
+
.fishbowl .tab-badge {
|
| 380 |
+
font-size: 8.5px; padding: 1px 6px; border-radius: 999px; letter-spacing: 0.1em;
|
| 381 |
+
background: rgba(155,232,107,0.14); color: var(--lime); border: 1px solid rgba(155,232,107,0.3);
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.fishbowl .app-body { flex: 1; min-height: 0; position: relative; }
|
| 385 |
+
|
| 386 |
+
/* live status dot */
|
| 387 |
+
.fishbowl .live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--lime); box-shadow: 0 0 8px var(--lime); }
|
| 388 |
+
@media (prefers-reduced-motion: no-preference) { .fishbowl .live-dot { animation: livePulse 1.6s ease-in-out infinite; } }
|
| 389 |
+
@keyframes livePulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
| 390 |
+
|
| 391 |
+
/* ============================================================
|
| 392 |
+
THE LAB
|
| 393 |
+
============================================================ */
|
| 394 |
+
.fishbowl .lab { height: 100%; position: relative; }
|
| 395 |
+
.fishbowl .lab-inner { max-width: 1080px; margin: 0 auto; padding: 26px 26px 120px; display: flex; flex-direction: column; gap: 16px; }
|
| 396 |
+
|
| 397 |
+
.fishbowl .lab-hero { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; padding: 6px 2px 10px; }
|
| 398 |
+
.fishbowl .lab-hero-t { font-size: 26px; font-weight: 700; letter-spacing: -0.01em; margin: 8px 0 8px; color: var(--ink); text-shadow: var(--glow); max-width: 640px; line-height: 1.2; }
|
| 399 |
+
.fishbowl .lab-hero-p { font-size: 13px; color: var(--ink-mid); max-width: 600px; margin: 0; line-height: 1.6; }
|
| 400 |
+
.fishbowl .lab-hero-p b { color: var(--cyan); }
|
| 401 |
+
.fishbowl .btn.surprise { border-color: var(--violet); color: var(--violet); flex: none; }
|
| 402 |
+
.fishbowl .btn.surprise:hover { box-shadow: 0 0 16px rgba(155,140,255,0.4); }
|
| 403 |
+
|
| 404 |
+
.fishbowl .lab-sec { padding: 0; overflow: hidden; }
|
| 405 |
+
.fishbowl .lab-sec-h { display: flex; justify-content: space-between; align-items: center; padding: 14px 18px; border-bottom: 1px solid var(--line-soft); }
|
| 406 |
+
.fishbowl .lab-sec-n { font-size: 12px; font-weight: 700; color: var(--cyan); letter-spacing: 0.1em; opacity: 0.8; }
|
| 407 |
+
.fishbowl .lab-sec-t { font-size: 14px; letter-spacing: 0.06em; color: var(--ink); }
|
| 408 |
+
.fishbowl .lab-sec-body { padding: 18px; }
|
| 409 |
+
.fishbowl .lab-hint { font-size: 10.5px; color: var(--ink-faint); font-style: italic; max-width: 360px; text-align: right; }
|
| 410 |
+
.fishbowl .lab-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
| 411 |
+
|
| 412 |
+
/* scenario cards */
|
| 413 |
+
.fishbowl .scn-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
| 414 |
+
.fishbowl .scn-card {
|
| 415 |
+
display: flex; flex-direction: column; gap: 7px; align-items: flex-start;
|
| 416 |
+
padding: 16px 14px; border: 1px solid var(--line); border-radius: var(--r-lg);
|
| 417 |
+
background: var(--glass); cursor: pointer; transition: all 0.18s ease; text-align: left;
|
| 418 |
+
position: relative; min-height: 150px;
|
| 419 |
+
}
|
| 420 |
+
.fishbowl .scn-card:hover { border-color: var(--line-hot); box-shadow: var(--glow); transform: translateY(-2px); }
|
| 421 |
+
.fishbowl .scn-card.on { border-color: var(--cyan); background: rgba(79,230,210,0.08); box-shadow: var(--glow); }
|
| 422 |
+
.fishbowl .scn-card.on::after { content: "ACTIVE"; position: absolute; top: 10px; right: 10px; font-family: var(--font-display); font-size: 7.5px; letter-spacing: 0.18em; color: var(--cyan); }
|
| 423 |
+
.fishbowl .scn-card.custom { opacity: 0.45; cursor: default; border-style: dashed; }
|
| 424 |
+
.fishbowl .scn-card.custom:hover { transform: none; box-shadow: none; border-color: var(--line); }
|
| 425 |
+
.fishbowl .scn-glyph { font-size: 28px; color: var(--cyan); text-shadow: var(--glow); line-height: 1; }
|
| 426 |
+
.fishbowl .scn-title { font-size: 13px; font-weight: 700; letter-spacing: 0.04em; color: var(--ink); }
|
| 427 |
+
.fishbowl .scn-code { font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-dim); }
|
| 428 |
+
.fishbowl .scn-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: auto; }
|
| 429 |
+
.fishbowl .scn-tag { font-size: 8.5px; letter-spacing: 0.06em; color: var(--ink-mid); border: 1px solid var(--line-soft); border-radius: 999px; padding: 2px 7px; }
|
| 430 |
+
|
| 431 |
+
/* cast table */
|
| 432 |
+
.fishbowl .cast-headcount { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
| 433 |
+
.fishbowl .cast-table { display: flex; flex-direction: column; gap: 2px; }
|
| 434 |
+
.fishbowl .cast-row { display: grid; grid-template-columns: 48px 1.1fr 1.3fr 1.8fr 1.2fr 1fr; align-items: center; gap: 12px; padding: 8px 10px; border-radius: var(--r); }
|
| 435 |
+
.fishbowl .cast-row:not(.cast-head):hover { background: var(--glass); }
|
| 436 |
+
.fishbowl .cast-head { font-family: var(--font-display); font-size: 8.5px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--ink-faint); padding-bottom: 6px; border-bottom: 1px solid var(--line-soft); border-radius: 0; }
|
| 437 |
+
.fishbowl .cast-av { display: grid; place-items: center; }
|
| 438 |
+
.fishbowl .cast-name { font-family: var(--font-display); font-weight: 600; font-size: 13px; letter-spacing: 0.05em; padding: 6px 8px; }
|
| 439 |
+
.fishbowl .cast-arch { font-size: 12px; color: var(--ink-mid); }
|
| 440 |
+
.fishbowl .cast-model { display: flex; align-items: center; gap: 8px; }
|
| 441 |
+
.fishbowl .cast-model select { font-size: 11.5px; padding: 6px 8px; }
|
| 442 |
+
.fishbowl .tier-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; box-shadow: 0 0 6px currentColor; }
|
| 443 |
+
.fishbowl .cast-temp { display: flex; align-items: center; gap: 9px; }
|
| 444 |
+
.fishbowl .cast-temp .tnum { font-size: 12px; color: var(--cyan); min-width: 24px; }
|
| 445 |
+
.fishbowl .cast-role { justify-self: start; font-size: 8px; }
|
| 446 |
+
|
| 447 |
+
.fishbowl .range-ends { display: flex; justify-content: space-between; font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-faint); margin-top: 5px; }
|
| 448 |
+
|
| 449 |
+
/* tools */
|
| 450 |
+
.fishbowl .tool-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
| 451 |
+
.fishbowl .tool-chip { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; padding: 11px 12px; border: 1px solid var(--line); border-radius: var(--r); background: var(--glass); cursor: pointer; transition: all 0.16s ease; text-align: left; position: relative; }
|
| 452 |
+
.fishbowl .tool-chip:hover { border-color: var(--line-hot); }
|
| 453 |
+
.fishbowl .tool-chip.on { border-color: var(--cyan); background: rgba(79,230,210,0.07); }
|
| 454 |
+
.fishbowl .tool-box { position: absolute; top: 10px; right: 11px; width: 16px; height: 16px; border: 1px solid var(--line); border-radius: 3px; display: grid; place-items: center; font-size: 11px; color: var(--cyan); }
|
| 455 |
+
.fishbowl .tool-chip.on .tool-box { border-color: var(--cyan); box-shadow: var(--glow); }
|
| 456 |
+
.fishbowl .tool-name { font-size: 12px; color: var(--ink); }
|
| 457 |
+
.fishbowl .tool-desc { font-size: 9.5px; color: var(--ink-dim); }
|
| 458 |
+
|
| 459 |
+
/* budget */
|
| 460 |
+
.fishbowl .budget-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
| 461 |
+
.fishbowl .seed-note { font-size: 11px; color: var(--ink-faint); margin: 16px 0 0; line-height: 1.5; }
|
| 462 |
+
|
| 463 |
+
/* launch bar */
|
| 464 |
+
.fishbowl .launch-bar { position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 18px; padding: 14px 26px; background: rgba(6,20,27,0.92); backdrop-filter: blur(12px); border-top: 1px solid var(--line); }
|
| 465 |
+
.fishbowl .launch-info { display: flex; align-items: center; gap: 14px; flex: 1; }
|
| 466 |
+
.fishbowl .launch-meta { font-size: 11px; color: var(--ink-dim); letter-spacing: 0.04em; }
|
| 467 |
+
.fishbowl .launch-voice { flex-direction: row; align-items: center; gap: 10px; }
|
| 468 |
+
.fishbowl .launch-voice label { white-space: nowrap; }
|
| 469 |
+
.fishbowl .launch-voice select { min-width: 230px; font-size: 11.5px; }
|
| 470 |
+
.fishbowl .launch-go { padding: 12px 22px; font-size: 12px; }
|
| 471 |
+
|
| 472 |
+
/* hints toggle + summon flash */
|
| 473 |
+
.fishbowl .hints-toggle { display: flex; align-items: center; gap: 9px; cursor: pointer; }
|
| 474 |
+
.fishbowl .summon-flash { position: fixed; inset: 0; z-index: 200; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: rgba(5,18,26,0.86); backdrop-filter: blur(10px); animation: sfIn 0.3s ease; }
|
| 475 |
+
@keyframes sfIn { from { opacity: 0; } to { opacity: 1; } }
|
| 476 |
+
.fishbowl .sf-glyph { font-size: 90px; color: var(--cyan); text-shadow: var(--glow-hot); animation: sfPulse 0.85s ease-in-out infinite; }
|
| 477 |
+
@keyframes sfPulse { 0%,100% { transform: scale(1); opacity: 0.8; } 50% { transform: scale(1.12); opacity: 1; } }
|
| 478 |
+
.fishbowl .sf-text { font-size: 18px; letter-spacing: 0.22em; color: var(--ink); text-shadow: var(--glow); }
|
| 479 |
+
.fishbowl .sf-sub { font-size: 11px; letter-spacing: 0.08em; color: var(--ink-dim); }
|
| 480 |
+
.fishbowl .app.surprising .lab-inner { animation: surpriseShimmer 0.6s ease; }
|
| 481 |
+
@keyframes surpriseShimmer { 0% { filter: brightness(1.4) saturate(1.3); } 100% { filter: none; } }
|
| 482 |
+
|
| 483 |
+
/* ============================================================
|
| 484 |
+
THE SHOW
|
| 485 |
+
============================================================ */
|
| 486 |
+
.fishbowl .show { display: flex; flex-direction: column; height: 100%; position: relative; }
|
| 487 |
+
|
| 488 |
+
/* ---- show bar ---- */
|
| 489 |
+
.fishbowl .showbar { display: flex; align-items: center; gap: 16px; padding: 11px 20px; border-bottom: 1px solid var(--line); background: rgba(6,20,27,0.6); backdrop-filter: blur(10px); flex: none; }
|
| 490 |
+
.fishbowl .show-title { display: flex; align-items: center; gap: 11px; flex: 1; min-width: 0; }
|
| 491 |
+
.fishbowl .show-title .disp { font-size: 15px; letter-spacing: 0.08em; color: var(--ink); text-shadow: var(--glow); }
|
| 492 |
+
.fishbowl .show-code { font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-dim); }
|
| 493 |
+
.fishbowl .show-controls { display: flex; align-items: center; gap: 14px; }
|
| 494 |
+
|
| 495 |
+
/* the star — mind reader toggle */
|
| 496 |
+
.fishbowl .mindreader { display: flex; align-items: center; gap: 10px; padding: 6px 12px 6px 14px; border: 1px solid var(--line); border-radius: 999px; cursor: pointer; transition: all 0.2s ease; background: var(--glass); }
|
| 497 |
+
.fishbowl .mindreader.on { border-color: var(--cyan); box-shadow: var(--glow); background: rgba(79,230,210,0.08); }
|
| 498 |
+
.fishbowl .mr-icon { color: var(--ink-dim); display: grid; place-items: center; }
|
| 499 |
+
.fishbowl .mindreader.on .mr-icon { color: var(--cyan); }
|
| 500 |
+
.fishbowl .mr-label { font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-mid); }
|
| 501 |
+
.fishbowl .mindreader.on .mr-label { color: var(--cyan); }
|
| 502 |
+
|
| 503 |
+
/* segmented control */
|
| 504 |
+
.fishbowl .seg { display: flex; gap: 2px; background: rgba(5,18,26,0.7); border: 1px solid var(--line); border-radius: var(--r); padding: 2px; }
|
| 505 |
+
.fishbowl .seg-b { width: 34px; height: 30px; display: grid; place-items: center; border: 0; background: transparent; color: var(--ink-dim); border-radius: 3px; cursor: pointer; transition: all 0.15s ease; }
|
| 506 |
+
.fishbowl .seg-b.sb { width: auto; padding: 0 11px; font-family: var(--font-display); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; }
|
| 507 |
+
.fishbowl .seg-b:hover { color: var(--ink-mid); }
|
| 508 |
+
.fishbowl .seg-b.on { background: var(--glass); color: var(--cyan); box-shadow: inset 0 0 0 1px var(--line-hot); }
|
| 509 |
+
.fishbowl .voice-field select { min-width: 180px; font-size: 11px; padding: 7px 9px; }
|
| 510 |
+
|
| 511 |
+
/* ---- body ---- */
|
| 512 |
+
.fishbowl .show-body { flex: 1; min-height: 0; position: relative; }
|
| 513 |
+
|
| 514 |
+
/* ============ CONSTELLATION ============ */
|
| 515 |
+
.fishbowl .show-constellation .show-body { display: grid; grid-template-columns: 1fr 384px; }
|
| 516 |
+
.fishbowl .constellation { position: relative; }
|
| 517 |
+
.fishbowl .stage { position: relative; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); padding: 22px; gap: 10px; overflow: auto; min-height: 0; place-items: center; }
|
| 518 |
+
.fishbowl .core { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; pointer-events: none; z-index: 1; }
|
| 519 |
+
.fishbowl .core-glyph { font-size: 64px; color: var(--cyan); text-shadow: var(--glow-hot); line-height: 1; opacity: 0.85; animation: coreFloat 6s ease-in-out infinite; }
|
| 520 |
+
@keyframes coreFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
|
| 521 |
+
.fishbowl .core-title { font-size: 16px; letter-spacing: 0.1em; color: var(--ink); }
|
| 522 |
+
.fishbowl .core-round { font-size: 9px; color: var(--ink-dim); }
|
| 523 |
+
.fishbowl .ring { display: contents; }
|
| 524 |
+
.fishbowl .ring-slot { display: grid; place-items: center; z-index: 2; }
|
| 525 |
+
.fishbowl .show-constellation .ring-slot:nth-child(1) { grid-area: 1 / 2; }
|
| 526 |
+
.fishbowl .show-constellation .ring-slot:nth-child(2) { grid-area: 2 / 3; }
|
| 527 |
+
.fishbowl .show-constellation .ring-slot:nth-child(3) { grid-area: 3 / 2; }
|
| 528 |
+
.fishbowl .show-constellation .ring-slot:nth-child(4) { grid-area: 2 / 1; }
|
| 529 |
+
.fishbowl .show-constellation .ring-slot:nth-child(5) { grid-area: 1 / 1; }
|
| 530 |
+
.fishbowl .show-constellation .ring-slot:nth-child(6) { grid-area: 3 / 3; }
|
| 531 |
+
|
| 532 |
+
/* ---- the mind card ---- */
|
| 533 |
+
.fishbowl .mind { perspective: 1200px; width: 300px; }
|
| 534 |
+
.fishbowl .mind-stage { width: 300px; }
|
| 535 |
+
.fishbowl .mind-inner { position: relative; transition: transform 0.7s cubic-bezier(0.4,0.1,0.2,1); transform-style: preserve-3d; }
|
| 536 |
+
.fishbowl .mind.flipped .mind-inner { transform: rotateY(180deg); }
|
| 537 |
+
.fishbowl .mind-face { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel); backdrop-filter: blur(8px); padding: 13px; backface-visibility: hidden; }
|
| 538 |
+
.fishbowl .mind-back { position: absolute; inset: 0; transform: rotateY(180deg); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; border-color: var(--lime); background: rgba(155,232,107,0.08); box-shadow: 0 0 24px rgba(155,232,107,0.3); }
|
| 539 |
+
.fishbowl .reveal-glyph { font-size: 40px; color: var(--lime); text-shadow: 0 0 16px var(--lime); }
|
| 540 |
+
.fishbowl .reveal-secret { font-size: 14px; color: var(--lime); text-align: center; padding: 0 14px; line-height: 1.4; }
|
| 541 |
+
|
| 542 |
+
.fishbowl .mind.speaking .mind-front { border-color: var(--ac); box-shadow: 0 0 22px color-mix(in oklab, var(--ac) 45%, transparent); }
|
| 543 |
+
.fishbowl .mind.rattled .mind-front { border-color: var(--coral); animation: cardShake 0.4s ease-in-out infinite; }
|
| 544 |
+
@keyframes cardShake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } }
|
| 545 |
+
|
| 546 |
+
.fishbowl .mind-head { display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center; padding-bottom: 11px; border-bottom: 1px solid var(--line-soft); }
|
| 547 |
+
.fishbowl .mind-id { min-width: 0; }
|
| 548 |
+
.fishbowl .mind-name { font-size: 14px; font-weight: 700; letter-spacing: 0.06em; color: var(--ink); display: flex; align-items: center; gap: 7px; }
|
| 549 |
+
.fishbowl .mic { color: var(--coral); font-size: 9px; animation: livePulse 0.9s ease-in-out infinite; }
|
| 550 |
+
.fishbowl .mind-arch { font-size: 10.5px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 551 |
+
.fishbowl .mind-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
|
| 552 |
+
.fishbowl .mind-model { display: flex; align-items: center; gap: 5px; font-size: 9.5px; letter-spacing: 0.04em; color: var(--ink-mid); }
|
| 553 |
+
.fishbowl .mind-mood { font-family: var(--font-display); font-size: 8.5px; letter-spacing: 0.12em; text-transform: uppercase; }
|
| 554 |
+
|
| 555 |
+
.fishbowl .bubbles { display: flex; flex-direction: column; gap: 8px; padding-top: 11px; }
|
| 556 |
+
.fishbowl .bubble { border-radius: var(--r); padding: 9px 11px 10px; position: relative; }
|
| 557 |
+
.fishbowl .bub-tag { font-family: var(--font-display); font-size: 7.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-faint); display: block; margin-bottom: 5px; }
|
| 558 |
+
.fishbowl .bubble p { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--ink); }
|
| 559 |
+
.fishbowl .bubble .muted { color: var(--ink-faint); font-style: italic; }
|
| 560 |
+
.fishbowl .bubble.said { background: rgba(5,18,26,0.55); border: 1px solid var(--line); }
|
| 561 |
+
.fishbowl .bubble.thought.open { background: color-mix(in oklab, var(--ac) 9%, rgba(5,18,26,0.4)); border: 1px dashed var(--ac); }
|
| 562 |
+
.fishbowl .bubble.thought.open .bub-tag { color: var(--ac); }
|
| 563 |
+
.fishbowl .think-text { font-style: italic; color: color-mix(in oklab, var(--ac) 70%, var(--ink)) !important; text-shadow: 0 0 8px color-mix(in oklab, var(--ac) 40%, transparent); }
|
| 564 |
+
.fishbowl .bubble.thought.sealed { background: rgba(5,18,26,0.35); border: 1px dashed var(--line-soft); }
|
| 565 |
+
.fishbowl .seal { font-size: 10.5px; color: var(--ink-faint); display: flex; align-items: center; gap: 7px; font-style: italic; }
|
| 566 |
+
.fishbowl .bubble.thought.leak.open { border-color: var(--coral); background: color-mix(in oklab, var(--coral) 10%, rgba(5,18,26,0.4)); animation: leakPulse 0.7s ease-in-out infinite; }
|
| 567 |
+
.fishbowl .bubble.thought.leak .bub-tag, .fishbowl .bubble.thought.leak .think-text { color: var(--coral) !important; text-shadow: 0 0 8px rgba(255,143,125,0.5); }
|
| 568 |
+
@keyframes leakPulse { 0%,100% { box-shadow: 0 0 0 transparent; } 50% { box-shadow: 0 0 16px rgba(255,143,125,0.35); } }
|
| 569 |
+
|
| 570 |
+
/* ---- rail (narrator feed + meters) ---- */
|
| 571 |
+
.fishbowl .rail { display: flex; flex-direction: column; border-left: 1px solid var(--line); background: rgba(6,20,27,0.4); min-height: 0; }
|
| 572 |
+
.fishbowl .rail-h { display: flex; justify-content: space-between; align-items: center; padding: 13px 16px; border-bottom: 1px solid var(--line-soft); flex: none; }
|
| 573 |
+
.fishbowl .feed { flex: 1; padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; min-height: 0; }
|
| 574 |
+
.fishbowl .feed.dense { gap: 9px; }
|
| 575 |
+
.fishbowl .fe { animation: feIn 0.3s ease; }
|
| 576 |
+
@keyframes feIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
| 577 |
+
.fishbowl .fe.narr { border-left: 2px solid var(--violet); padding-left: 12px; }
|
| 578 |
+
.fishbowl .narr-voice { font-family: var(--font-display); font-size: 8px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--violet); display: block; margin-bottom: 4px; }
|
| 579 |
+
.fishbowl .fe.narr p { margin: 0; font-size: 12.5px; line-height: 1.55; color: var(--ink-mid); font-style: italic; }
|
| 580 |
+
.fishbowl .fe.say { background: rgba(5,18,26,0.4); border-left: 2px solid var(--ac); border-radius: 0 var(--r) var(--r) 0; padding: 8px 11px; }
|
| 581 |
+
.fishbowl .say-line { font-size: 12.5px; line-height: 1.45; }
|
| 582 |
+
.fishbowl .say-line b { color: var(--ac); margin-right: 7px; letter-spacing: 0.04em; }
|
| 583 |
+
.fishbowl .say-line span { color: var(--ink); }
|
| 584 |
+
.fishbowl .say-think { margin-top: 5px; font-size: 11px; color: var(--ac); opacity: 0.85; }
|
| 585 |
+
.fishbowl .say-think i { color: color-mix(in oklab, var(--ac) 75%, var(--ink)); }
|
| 586 |
+
.fishbowl .fe.poke, .fishbowl .fe.verdict-fe { border: 1px solid var(--amber); border-radius: var(--r); padding: 9px 11px; background: rgba(255,207,107,0.07); }
|
| 587 |
+
.fishbowl .poke-tag { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-display); font-size: 8.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); border: 1px solid rgba(255,207,107,0.4); border-radius: 999px; padding: 2px 8px; margin-bottom: 6px; }
|
| 588 |
+
.fishbowl .fe.poke p, .fishbowl .fe.verdict-fe p { margin: 0; font-size: 12px; line-height: 1.5; color: var(--ink); }
|
| 589 |
+
|
| 590 |
+
/* ---- meters ---- */
|
| 591 |
+
.fishbowl .meters { margin: 0 14px 14px; padding: 13px 15px; flex: none; display: flex; flex-direction: column; gap: 12px; }
|
| 592 |
+
.fishbowl .stat { font-size: 10px; letter-spacing: 0.08em; color: var(--ink-mid); }
|
| 593 |
+
.fishbowl .meter-h { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 7px; }
|
| 594 |
+
.fishbowl .meter-h .tnum { font-size: 11px; font-family: var(--font-display); }
|
| 595 |
+
.fishbowl .bar { height: 5px; background: rgba(5,18,26,0.8); border-radius: 3px; overflow: hidden; }
|
| 596 |
+
.fishbowl .bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s ease; box-shadow: 0 0 8px currentColor; }
|
| 597 |
+
.fishbowl .meter-stats { display: flex; justify-content: space-between; gap: 10px; }
|
| 598 |
+
|
| 599 |
+
/* ============ FEED VIEW ============ */
|
| 600 |
+
.fishbowl .feedview { display: grid; grid-template-columns: 220px 1fr 300px; height: 100%; min-height: 0; }
|
| 601 |
+
.fishbowl .roster { border-right: 1px solid var(--line); padding: 16px 14px; display: flex; flex-direction: column; gap: 10px; background: rgba(6,20,27,0.4); overflow: auto; }
|
| 602 |
+
.fishbowl .roster-chip { display: flex; align-items: center; gap: 11px; padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--r); transition: all 0.2s ease; }
|
| 603 |
+
.fishbowl .roster-chip.on { border-color: var(--ac); box-shadow: 0 0 14px color-mix(in oklab, var(--ac) 35%, transparent); background: color-mix(in oklab, var(--ac) 6%, transparent); }
|
| 604 |
+
.fishbowl .rc-name { font-size: 12.5px; letter-spacing: 0.05em; color: var(--ink); }
|
| 605 |
+
.fishbowl .rc-mood { font-family: var(--font-display); font-size: 8px; letter-spacing: 0.1em; text-transform: uppercase; }
|
| 606 |
+
.fishbowl .feedview-main { min-height: 0; display: flex; }
|
| 607 |
+
.fishbowl .feedview-main .feed { max-width: 720px; margin: 0 auto; width: 100%; }
|
| 608 |
+
.fishbowl .feedview-side { border-left: 1px solid var(--line); padding: 16px 0; background: rgba(6,20,27,0.4); }
|
| 609 |
+
.fishbowl .feedview-side .meters { background: transparent; border: 0; }
|
| 610 |
+
|
| 611 |
+
/* ============ SPLIT VIEW ============ */
|
| 612 |
+
.fishbowl .splitview { height: 100%; padding: 20px 26px 26px; max-width: 1180px; margin: 0 auto; width: 100%; }
|
| 613 |
+
.fishbowl .split-head { display: grid; grid-template-columns: 230px 1fr 1fr; gap: 16px; padding: 0 14px 10px; position: sticky; top: 0; }
|
| 614 |
+
.fishbowl .split-row { display: grid; grid-template-columns: 230px 1fr 1fr; gap: 16px; align-items: stretch; padding: 14px; border: 1px solid var(--line); border-radius: var(--r-lg); margin-bottom: 12px; background: var(--panel); transition: all 0.2s ease; }
|
| 615 |
+
.fishbowl .split-row.on { border-color: var(--ac); box-shadow: 0 0 18px color-mix(in oklab, var(--ac) 30%, transparent); }
|
| 616 |
+
.fishbowl .split-id { display: flex; gap: 12px; align-items: center; }
|
| 617 |
+
.fishbowl .split-name { font-size: 14px; letter-spacing: 0.06em; color: var(--ink); }
|
| 618 |
+
.fishbowl .split-arch { font-size: 10.5px; color: var(--ink-dim); }
|
| 619 |
+
.fishbowl .split-model { display: flex; align-items: center; gap: 5px; font-size: 9.5px; color: var(--ink-mid); margin-top: 3px; }
|
| 620 |
+
.fishbowl .split-said, .fishbowl .split-think { border-radius: var(--r); padding: 12px 14px; display: flex; align-items: center; }
|
| 621 |
+
.fishbowl .split-said { background: rgba(5,18,26,0.5); border: 1px solid var(--line); }
|
| 622 |
+
.fishbowl .split-think { background: color-mix(in oklab, var(--ac) 8%, rgba(5,18,26,0.4)); border: 1px dashed var(--ac); }
|
| 623 |
+
.fishbowl .split-said p, .fishbowl .split-think p { margin: 0; font-size: 13px; line-height: 1.5; }
|
| 624 |
+
.fishbowl .split-think.leak { border-color: var(--coral); background: rgba(255,143,125,0.08); }
|
| 625 |
+
.fishbowl .split-think.leak .think-text { color: var(--coral) !important; }
|
| 626 |
+
|
| 627 |
+
/* ---- verdict banner ---- */
|
| 628 |
+
.fishbowl .verdict-banner { position: absolute; top: 64px; left: 50%; transform: translateX(-50%); z-index: 40; display: flex; align-items: center; gap: 14px; padding: 11px 22px; background: rgba(10,34,27,0.95); border: 1px solid var(--lime); border-radius: 999px; box-shadow: 0 0 30px rgba(155,232,107,0.4); backdrop-filter: blur(8px); animation: vbDrop 0.5s cubic-bezier(0.2,1.2,0.3,1); }
|
| 629 |
+
@keyframes vbDrop { from { opacity: 0; transform: translateX(-50%) translateY(-20px); } to { opacity: 1; } }
|
| 630 |
+
.fishbowl .verdict-banner .disp { color: var(--lime); font-size: 13px; letter-spacing: 0.14em; text-shadow: 0 0 10px var(--lime); }
|
| 631 |
+
.fishbowl .vb-text { color: var(--ink); font-size: 12.5px; }
|
| 632 |
+
|
| 633 |
+
/* ---- poke strip ---- */
|
| 634 |
+
.fishbowl .poke-strip { display: flex; align-items: center; gap: 14px; padding: 10px 20px; border-top: 1px solid var(--line-soft); background: rgba(6,20,27,0.5); flex: none; }
|
| 635 |
+
.fishbowl .poke-btns { display: flex; gap: 8px; flex: 1; flex-wrap: wrap; }
|
| 636 |
+
.fishbowl .poke-b { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-display); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--amber); border: 1px solid rgba(255,207,107,0.35); background: rgba(255,207,107,0.06); border-radius: var(--r); padding: 7px 12px; cursor: pointer; transition: all 0.16s ease; }
|
| 637 |
+
.fishbowl .poke-b:hover:not(:disabled) { box-shadow: 0 0 14px rgba(255,207,107,0.35); border-color: var(--amber); }
|
| 638 |
+
.fishbowl .poke-b:disabled { opacity: 0.3; cursor: default; }
|
| 639 |
+
.fishbowl .poke-b.next { animation: pokeBeck 1.8s ease-in-out infinite; border-color: var(--amber); }
|
| 640 |
+
@keyframes pokeBeck { 0%,100% { box-shadow: 0 0 0 transparent; } 50% { box-shadow: 0 0 14px rgba(255,207,107,0.4); } }
|
| 641 |
+
.fishbowl .export-btn { color: var(--lime); border-color: rgba(155,232,107,0.35); }
|
| 642 |
+
.fishbowl .export-btn:hover:not(:disabled) { box-shadow: 0 0 14px rgba(155,232,107,0.3); }
|
| 643 |
+
.fishbowl .mini-spin { width: 12px; height: 12px; border: 2px solid var(--lime); border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; display: inline-block; }
|
| 644 |
+
|
| 645 |
+
/* ---- transport ---- */
|
| 646 |
+
.fishbowl .transport { display: flex; align-items: center; gap: 16px; padding: 13px 22px; border-top: 1px solid var(--line); background: rgba(6,20,27,0.85); backdrop-filter: blur(10px); flex: none; }
|
| 647 |
+
.fishbowl .tp-btns { display: flex; gap: 6px; }
|
| 648 |
+
.fishbowl .icon-btn.play { border-color: var(--cyan); color: var(--cyan); box-shadow: var(--glow); }
|
| 649 |
+
.fishbowl .scrub { flex: 1; }
|
| 650 |
+
.fishbowl .tp-count { font-size: 12px; color: var(--ink-mid); white-space: nowrap; }
|
| 651 |
+
.fishbowl .speed-seg .seg-b.on { color: var(--cyan); }
|
| 652 |
+
|
| 653 |
+
/* ---- saga toast ---- */
|
| 654 |
+
.fishbowl .saga-toast { position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%); z-index: 45; padding: 16px 22px; max-width: 520px; text-align: center; border-color: var(--lime); box-shadow: 0 0 30px rgba(155,232,107,0.25); animation: vbDrop 0.4s ease; }
|
| 655 |
+
.fishbowl .saga-head { font-size: 15px; color: var(--lime); margin: 8px 0 0; line-height: 1.4; }
|
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fishbowl theme + CSS/head assets for the Gradio shell.
|
| 2 |
+
|
| 3 |
+
This module is the styling surface of the Fishbowl UI. It is import-safe —
|
| 4 |
+
constructing :class:`FishbowlTheme` or reading the assets never launches Gradio
|
| 5 |
+
or touches the engine. The app shell (Unit 9) wires it up with::
|
| 6 |
+
|
| 7 |
+
from src.ui.fishbowl.theme import FISHBOWL_HEAD, FishbowlTheme, load_css
|
| 8 |
+
|
| 9 |
+
demo.launch(css=load_css(), head=FISHBOWL_HEAD, theme=FishbowlTheme())
|
| 10 |
+
|
| 11 |
+
The phosphor / aquatic-CRT look is ported from the ``ui/raw`` prototype:
|
| 12 |
+
|
| 13 |
+
* ``assets/styles.css`` — the full stylesheet, scoped under a ``.fishbowl``
|
| 14 |
+
root so it survives Gradio's own cascade, with the CRT overlay layers left
|
| 15 |
+
full-bleed.
|
| 16 |
+
* ``assets/head.html`` — the Google Fonts links (Martian Mono + IBM Plex Mono).
|
| 17 |
+
|
| 18 |
+
:class:`FishbowlTheme` maps the same dark phosphor palette onto Gradio's theme
|
| 19 |
+
variables so the built-in widgets (sliders, dropdowns, buttons rendered by
|
| 20 |
+
Gradio itself) sit comfortably inside the theater.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
import gradio as gr
|
| 28 |
+
|
| 29 |
+
__all__ = ["FISHBOWL_HEAD", "FishbowlTheme", "load_css"]
|
| 30 |
+
|
| 31 |
+
_ASSETS = Path(__file__).parent / "assets"
|
| 32 |
+
|
| 33 |
+
# ---- palette (mirrors the CSS custom properties in assets/styles.css) ----
|
| 34 |
+
_BG_0 = "#05121a" # deepest tank
|
| 35 |
+
_BG_1 = "#07181f"
|
| 36 |
+
_BG_2 = "#0a222b"
|
| 37 |
+
_PANEL_SOLID = "#0c2730"
|
| 38 |
+
_INK = "#c4f6ee"
|
| 39 |
+
_INK_MID = "#79c3bb"
|
| 40 |
+
_INK_DIM = "#4d8983"
|
| 41 |
+
_CYAN = "#4fe6d2"
|
| 42 |
+
_TEAL = "#2bc4b4"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def load_css() -> str:
|
| 46 |
+
"""Return the Fishbowl stylesheet as a string for ``gr.Blocks(css=...)``.
|
| 47 |
+
|
| 48 |
+
Reads ``assets/styles.css`` relative to this file, so it works regardless
|
| 49 |
+
of the process working directory.
|
| 50 |
+
"""
|
| 51 |
+
return (_ASSETS / "styles.css").read_text(encoding="utf-8")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _load_head() -> str:
|
| 55 |
+
"""Read ``assets/head.html`` for ``demo.launch(head=...)``."""
|
| 56 |
+
return (_ASSETS / "head.html").read_text(encoding="utf-8")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
#: The ``<head>`` markup (font links + meta) passed to ``demo.launch(head=...)``.
|
| 60 |
+
FISHBOWL_HEAD: str = _load_head()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class FishbowlTheme(gr.themes.Base):
|
| 64 |
+
"""A dark phosphor / aquatic-CRT Gradio theme for the Fishbowl app.
|
| 65 |
+
|
| 66 |
+
Subclasses :class:`gr.themes.Base` and overrides the colour, font, and
|
| 67 |
+
radius variables so Gradio's own widgets blend into the theater. The
|
| 68 |
+
bespoke theater markup is styled by :func:`load_css`; this theme covers the
|
| 69 |
+
surrounding Gradio chrome.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def __init__(self) -> None:
|
| 73 |
+
super().__init__(
|
| 74 |
+
primary_hue=gr.themes.colors.teal,
|
| 75 |
+
secondary_hue=gr.themes.colors.cyan,
|
| 76 |
+
neutral_hue=gr.themes.colors.slate,
|
| 77 |
+
font=gr.themes.GoogleFont("IBM Plex Mono"),
|
| 78 |
+
font_mono=gr.themes.GoogleFont("Martian Mono"),
|
| 79 |
+
)
|
| 80 |
+
# Map the aquatic phosphor palette onto Gradio's theme variables so the
|
| 81 |
+
# built-in components match the ported .fishbowl markup.
|
| 82 |
+
super().set(
|
| 83 |
+
# backgrounds
|
| 84 |
+
body_background_fill=_BG_0,
|
| 85 |
+
body_background_fill_dark=_BG_0,
|
| 86 |
+
background_fill_primary=_BG_1,
|
| 87 |
+
background_fill_primary_dark=_BG_1,
|
| 88 |
+
background_fill_secondary=_BG_2,
|
| 89 |
+
background_fill_secondary_dark=_BG_2,
|
| 90 |
+
block_background_fill=_PANEL_SOLID,
|
| 91 |
+
block_background_fill_dark=_PANEL_SOLID,
|
| 92 |
+
panel_background_fill=_PANEL_SOLID,
|
| 93 |
+
panel_background_fill_dark=_PANEL_SOLID,
|
| 94 |
+
# text
|
| 95 |
+
body_text_color=_INK,
|
| 96 |
+
body_text_color_dark=_INK,
|
| 97 |
+
body_text_color_subdued=_INK_MID,
|
| 98 |
+
body_text_color_subdued_dark=_INK_MID,
|
| 99 |
+
block_label_text_color=_INK_DIM,
|
| 100 |
+
block_label_text_color_dark=_INK_DIM,
|
| 101 |
+
block_title_text_color=_INK_MID,
|
| 102 |
+
block_title_text_color_dark=_INK_MID,
|
| 103 |
+
# borders
|
| 104 |
+
border_color_primary="*neutral_700",
|
| 105 |
+
block_border_color="*neutral_700",
|
| 106 |
+
# accents — cyan/teal phosphor
|
| 107 |
+
color_accent=_CYAN,
|
| 108 |
+
color_accent_soft=_TEAL,
|
| 109 |
+
button_primary_background_fill=_TEAL,
|
| 110 |
+
button_primary_background_fill_dark=_TEAL,
|
| 111 |
+
button_primary_text_color=_BG_0,
|
| 112 |
+
button_primary_text_color_dark=_BG_0,
|
| 113 |
+
slider_color=_CYAN,
|
| 114 |
+
slider_color_dark=_CYAN,
|
| 115 |
+
# radii to match --r / --r-lg
|
| 116 |
+
block_radius="8px",
|
| 117 |
+
button_large_radius="4px",
|
| 118 |
+
button_small_radius="4px",
|
| 119 |
+
input_radius="4px",
|
| 120 |
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mock-free tests for the Fishbowl theme + CSS/head assets (Unit 1).
|
| 2 |
+
|
| 3 |
+
These assert the styling surface is wired correctly: the stylesheet carries the
|
| 4 |
+
class names and custom properties the renderers rely on, the head markup loads
|
| 5 |
+
both fonts, and the theme is import-/construct-safe without launching Gradio.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from src.ui.fishbowl.theme import FISHBOWL_HEAD, FishbowlTheme, load_css
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_load_css_returns_nonempty_string() -> None:
|
| 14 |
+
css = load_css()
|
| 15 |
+
assert isinstance(css, str)
|
| 16 |
+
assert len(css) > 1000 # the full ported stylesheet, not a stub
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_css_contains_key_classes() -> None:
|
| 20 |
+
css = load_css()
|
| 21 |
+
# mind card + flip states
|
| 22 |
+
for token in ("mind", "mind-inner", "mind-face", "flipped", "speaking", "rattled", "mic"):
|
| 23 |
+
assert token in css, f"missing mind class: {token}"
|
| 24 |
+
# CRT atmosphere layers
|
| 25 |
+
for token in ("crt-bg", "crt-grid", "crt-scan", "crt-vignette", "crt-flicker"):
|
| 26 |
+
assert token in css, f"missing CRT layer: {token}"
|
| 27 |
+
# feed kinds
|
| 28 |
+
for token in ("narr", "say", "poke", "verdict-fe"):
|
| 29 |
+
assert token in css, f"missing feed class: {token}"
|
| 30 |
+
# stage / constellation (scoped under the .fishbowl root)
|
| 31 |
+
assert ".constellation" in css
|
| 32 |
+
assert ".fishbowl .stage" in css
|
| 33 |
+
assert ".fishbowl .core" in css
|
| 34 |
+
assert ".fishbowl .ring" in css
|
| 35 |
+
# transport + meters
|
| 36 |
+
for token in ("transport", "scrub", "seg", "seg-b", "meters", "stat"):
|
| 37 |
+
assert token in css, f"missing chrome class: {token}"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_css_contains_avatar_animations() -> None:
|
| 41 |
+
css = load_css()
|
| 42 |
+
for token in (
|
| 43 |
+
"av-ring",
|
| 44 |
+
"av-blink",
|
| 45 |
+
"av-sweat",
|
| 46 |
+
"av-gasp",
|
| 47 |
+
"av-thinking",
|
| 48 |
+
"av-panic",
|
| 49 |
+
"av-smug",
|
| 50 |
+
"av-gossip",
|
| 51 |
+
"av-truth",
|
| 52 |
+
"av-calm",
|
| 53 |
+
):
|
| 54 |
+
assert token in css, f"missing avatar anim class: {token}"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_css_contains_color_variables() -> None:
|
| 58 |
+
css = load_css()
|
| 59 |
+
for var in (
|
| 60 |
+
"--bg-0",
|
| 61 |
+
"--bg-1",
|
| 62 |
+
"--bg-2",
|
| 63 |
+
"--panel",
|
| 64 |
+
"--ink",
|
| 65 |
+
"--ink-mid",
|
| 66 |
+
"--ink-dim",
|
| 67 |
+
"--ink-faint",
|
| 68 |
+
"--cyan",
|
| 69 |
+
"--teal",
|
| 70 |
+
"--blue",
|
| 71 |
+
"--violet",
|
| 72 |
+
"--amber",
|
| 73 |
+
"--coral",
|
| 74 |
+
"--lime",
|
| 75 |
+
"--r",
|
| 76 |
+
"--r-lg",
|
| 77 |
+
"--glow",
|
| 78 |
+
):
|
| 79 |
+
assert var in css, f"missing CSS var: {var}"
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_css_scopes_under_fishbowl_and_hides_footer() -> None:
|
| 83 |
+
css = load_css()
|
| 84 |
+
assert ".fishbowl" in css
|
| 85 |
+
# default Gradio footer must be hidden
|
| 86 |
+
assert "footer" in css
|
| 87 |
+
assert "display: none" in css
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def test_head_references_both_fonts() -> None:
|
| 91 |
+
assert isinstance(FISHBOWL_HEAD, str)
|
| 92 |
+
assert "Martian+Mono" in FISHBOWL_HEAD or "Martian Mono" in FISHBOWL_HEAD
|
| 93 |
+
assert "IBM+Plex+Mono" in FISHBOWL_HEAD or "IBM Plex Mono" in FISHBOWL_HEAD
|
| 94 |
+
assert "fonts.googleapis.com" in FISHBOWL_HEAD
|
| 95 |
+
assert "preconnect" in FISHBOWL_HEAD
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def test_theme_constructs_without_launch() -> None:
|
| 99 |
+
theme = FishbowlTheme()
|
| 100 |
+
# font set to the body display fonts requested
|
| 101 |
+
assert theme is not None
|
| 102 |
+
# the theme is a gradio Base subclass
|
| 103 |
+
import gradio as gr
|
| 104 |
+
|
| 105 |
+
assert isinstance(theme, gr.themes.Base)
|