agharsallah Codex commited on
Commit
d1372c7
·
1 Parent(s): dfae9ee

feat: add Fishbowl phosphor theme + CSS/head assets

Browse files

Port 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 ADDED
@@ -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
+ />
src/ui/fishbowl/assets/styles.css ADDED
@@ -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; }
src/ui/fishbowl/theme.py ADDED
@@ -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
+ )
tests/test_fishbowl_theme.py ADDED
@@ -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)