hetchyy Claude Opus 4.6 commited on
Commit
410852a
·
1 Parent(s): 045ee7d

5A: Extract CSS into src/ui/styles.py

Browse files

Move ~420 lines of CSS f-string from build_interface() into a
dedicated build_css() function, reducing interface.py from 3016
to 2597 lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. src/ui/interface.py +2 -421
  2. src/ui/styles.py +431 -0
src/ui/interface.py CHANGED
@@ -9,7 +9,6 @@ from config import (
9
  MIN_SPEECH_MIN, MIN_SPEECH_MAX, MIN_SPEECH_STEP,
10
  PAD_MIN, PAD_MAX, PAD_STEP,
11
  PRESET_MUJAWWAD, PRESET_MURATTAL, PRESET_FAST,
12
- QURAN_TEXT_SIZE_PX, ARABIC_WORD_SPACING,
13
  DELETE_CACHE_FREQUENCY, DELETE_CACHE_AGE,
14
  ANIM_WORD_COLOR, ANIM_STYLE_ROW_SCALES,
15
  ANIM_DISPLAY_MODES, ANIM_DISPLAY_MODE_DEFAULT,
@@ -22,10 +21,9 @@ from config import (
22
  MEGA_WORD_SPACING_MIN, MEGA_WORD_SPACING_MAX, MEGA_WORD_SPACING_STEP, MEGA_WORD_SPACING_DEFAULT,
23
  MEGA_TEXT_SIZE_MIN, MEGA_TEXT_SIZE_MAX, MEGA_TEXT_SIZE_STEP, MEGA_TEXT_SIZE_DEFAULT,
24
  MEGA_LINE_SPACING_MIN, MEGA_LINE_SPACING_MAX, MEGA_LINE_SPACING_STEP, MEGA_LINE_SPACING_DEFAULT,
25
- MEGA_SURAH_LIGATURE_SIZE,
26
  LEFT_COLUMN_SCALE, RIGHT_COLUMN_SCALE,
27
  )
28
- from data.font_data import DIGITAL_KHATT_FONT_B64, SURAH_NAME_FONT_B64
29
  from src.pipeline.process import (
30
  process_audio, resegment_audio,
31
  _retranscribe_wrapper, process_audio_json, save_json_export,
@@ -40,424 +38,7 @@ with open(Path(__file__).parent.parent.parent / "data" / "ligatures.json") as _f
40
  def build_interface():
41
  """Build the Gradio interface."""
42
 
43
- css = f"""
44
- /* Font faces */
45
- @font-face {{
46
- font-family: 'DigitalKhatt';
47
- src: url(data:font/otf;base64,{DIGITAL_KHATT_FONT_B64}) format('opentype');
48
- font-weight: normal;
49
- font-style: normal;
50
- }}
51
- @font-face {{
52
- font-family: 'SurahName';
53
- src: url(data:font/truetype;base64,{SURAH_NAME_FONT_B64}) format('truetype');
54
- font-weight: normal;
55
- font-style: normal;
56
- }}
57
-
58
- .arabic-text {{
59
- font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
60
- direction: rtl;
61
- text-align: right;
62
- }}
63
-
64
- /* Prevent output area from being in a scrolling box */
65
- .gradio-container .prose {{
66
- max-height: none !important;
67
- }}
68
- .output-html {{
69
- max-height: none !important;
70
- overflow: visible !important;
71
- }}
72
-
73
- /* Segment cards - theme adaptive */
74
- .segment-card {{
75
- border-radius: 8px;
76
- padding: 12px 16px;
77
- margin-bottom: 12px;
78
- border: 2px solid;
79
- }}
80
- .segment-header {{
81
- display: flex;
82
- justify-content: space-between;
83
- align-items: center;
84
- margin-bottom: 8px;
85
- }}
86
- .segment-title {{
87
- font-size: 13px;
88
- opacity: 0.9;
89
- }}
90
- .segment-badges {{
91
- display: flex;
92
- gap: 6px;
93
- align-items: center;
94
- }}
95
- .segment-badge {{
96
- color: white;
97
- padding: 2px 8px;
98
- border-radius: 12px;
99
- font-size: 12px;
100
- font-weight: bold;
101
- }}
102
- .segment-audio {{
103
- margin: 8px 0;
104
- display: flex;
105
- align-items: center;
106
- gap: 8px;
107
- }}
108
- .segment-audio audio {{
109
- flex: 1;
110
- height: 32px;
111
- border-radius: 4px;
112
- }}
113
-
114
- /* Lazy play button (replaces <audio controls> until clicked) */
115
- .play-btn {{
116
- flex: 1;
117
- height: 32px;
118
- border-radius: 4px;
119
- border: 1px solid var(--border-color-primary, #ddd);
120
- background: var(--block-background-fill, #f7f7f7);
121
- cursor: pointer;
122
- font-size: 16px;
123
- display: flex;
124
- align-items: center;
125
- justify-content: center;
126
- }}
127
- .play-btn:hover {{ background: var(--block-background-fill-secondary, #eee); }}
128
-
129
- /* Make color picker popup overlay instead of pushing content down */
130
- .gradio-container .color-picker {{
131
- position: relative;
132
- overflow: visible !important;
133
- }}
134
- .gradio-container .color-picker .overflow-hidden,
135
- .gradio-container .color-picker > div:last-child:not(:first-child) {{
136
- position: absolute;
137
- z-index: 100;
138
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
139
- border-radius: 8px;
140
- overflow: visible !important;
141
- max-height: none !important;
142
- }}
143
- .gradio-container .color-picker .overflow-hidden {{
144
- overflow: visible !important;
145
- }}
146
- .gradio-container .color-picker *,
147
- .gradio-container .color-picker div {{
148
- overflow: visible !important;
149
- max-height: none !important;
150
- scrollbar-width: none !important;
151
- }}
152
- .gradio-container .color-picker *::-webkit-scrollbar {{
153
- display: none !important;
154
- }}
155
- /* Ensure all color-picker ancestors allow overflow for absolute popup */
156
- #anim-settings-accordion,
157
- #anim-settings-accordion > *,
158
- #anim-style-row,
159
- #anim-style-row > * {{
160
- overflow: visible !important;
161
- }}
162
-
163
-
164
- /* Animate button */
165
- .animate-btn {{
166
- background: #4a90d9 !important;
167
- color: white !important;
168
- border: none;
169
- padding: 6px 12px;
170
- border-radius: 4px;
171
- cursor: pointer;
172
- font-size: 12px;
173
- font-weight: bold;
174
- white-space: nowrap;
175
- }}
176
- .animate-btn:hover:not(:disabled) {{ background: #357abd !important; }}
177
- .animate-btn.active {{ background: #dc3545 !important; }}
178
- .animate-btn:disabled {{ background: #888 !important; cursor: not-allowed; opacity: 0.5; }}
179
-
180
- /* Make the HTML wrapper inside ts-row match the Gradio Button wrapper */
181
- #ts-row > .gr-html {{
182
- padding: 0;
183
- margin: 0;
184
- min-width: 0;
185
- flex: 1 1 0%;
186
- }}
187
- #ts-row > div:has(> .animate-all-btn) {{
188
- padding: 0;
189
- margin: 0;
190
- min-width: 0;
191
- flex: 1 1 0%;
192
- }}
193
-
194
- /* Animate All button — matches Gradio lg button sizing */
195
- .animate-all-btn {{
196
- display: block;
197
- width: 100%;
198
- background: var(--button-primary-background-fill, #f97316) !important;
199
- color: var(--button-primary-text-color, white) !important;
200
- border: var(--button-primary-border, none);
201
- padding: var(--size-2, 0.5rem) var(--size-4, 1rem);
202
- border-radius: var(--button-large-radius, var(--radius-lg, 8px));
203
- cursor: pointer;
204
- font-size: var(--button-large-text-size, var(--text-lg, 1.125rem));
205
- font-weight: var(--button-large-text-weight, 600);
206
- box-sizing: border-box;
207
- line-height: var(--line-md, 1.5);
208
- min-height: var(--size-10, 40px);
209
- }}
210
- .animate-all-btn:hover:not(:disabled) {{ background: var(--button-primary-background-fill-hover, #ea6c10) !important; }}
211
- .animate-all-btn.active {{ background: #dc3545 !important; }}
212
- .animate-all-btn:disabled {{ background: #888 !important; cursor: not-allowed; opacity: 0.5; }}
213
-
214
- /* Mega card for Animate All */
215
- .mega-card {{
216
- font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
217
- font-size: {MEGA_TEXT_SIZE_DEFAULT}px;
218
- direction: rtl;
219
- text-align: justify;
220
- line-height: {MEGA_LINE_SPACING_DEFAULT};
221
- word-spacing: {MEGA_WORD_SPACING_DEFAULT}em;
222
- padding: 16px;
223
- border-radius: 8px;
224
- background: var(--block-background-fill);
225
- max-height: 70vh;
226
- overflow-y: auto;
227
- scrollbar-color: rgba(255,255,255,0.2) transparent;
228
- }}
229
- .mega-card::-webkit-scrollbar {{ width: 8px; }}
230
- .mega-card::-webkit-scrollbar-track {{ background: transparent; }}
231
- .mega-card::-webkit-scrollbar-thumb {{ background: rgba(255,255,255,0.2); border-radius: 4px; }}
232
- .mega-card::-webkit-scrollbar-thumb:hover {{ background: rgba(255,255,255,0.35); }}
233
- .mega-text-flow {{
234
- display: inline;
235
- }}
236
- .mega-special-line {{
237
- display: block;
238
- text-align: center;
239
- margin: 8px 0;
240
- opacity: 0.7;
241
- font-size: 0.85em;
242
- }}
243
- .mega-surah-separator {{
244
- display: block;
245
- text-align: center;
246
- margin: 8px 0 2px;
247
- padding: 4px 0 0;
248
- border-top: 1px solid rgba(255,255,255,0.1);
249
- opacity: 0.8;
250
- font-family: 'SurahName', sans-serif;
251
- font-feature-settings: "liga" 1;
252
- font-size: {MEGA_SURAH_LIGATURE_SIZE}em;
253
- line-height: 1.2;
254
- letter-spacing: normal;
255
- }}
256
- .segment-card.hidden-for-mega {{ display: none; }}
257
- #mega-styling-row {{ display: none; }}
258
- .mega-top-bar {{
259
- display: flex; justify-content: center; gap: 8px;
260
- margin-top: 12px; margin-bottom: 8px;
261
- }}
262
- .mega-top-bar .animate-all-btn {{
263
- width: auto; min-width: 0; min-height: auto;
264
- padding: 4px 12px;
265
- font-size: 12px; font-weight: bold;
266
- border-radius: 4px;
267
- line-height: normal;
268
- box-sizing: border-box;
269
- border: none;
270
- height: 42px;
271
- display: inline-flex;
272
- align-items: center;
273
- justify-content: center;
274
- }}
275
- .mega-exit-btn {{
276
- background: #6c757d;
277
- color: white;
278
- border: none;
279
- padding: 4px 12px;
280
- border-radius: 4px;
281
- cursor: pointer;
282
- font-size: 12px;
283
- font-weight: bold;
284
- min-width: 0; min-height: auto;
285
- line-height: normal;
286
- box-sizing: border-box;
287
- height: 42px;
288
- display: inline-flex;
289
- align-items: center;
290
- justify-content: center;
291
- }}
292
- .mega-exit-btn:hover {{ background: #5a6268; }}
293
- .mega-speed-select {{
294
- /* Use Gradio's theme-aware variables */
295
- background: var(--input-background-fill) !important;
296
- color: var(--body-text-color) !important;
297
- border: var(--input-border-width) solid var(--input-border-color) !important;
298
- border-radius: var(--input-radius) !important;
299
- /* Typography from Gradio */
300
- font-size: var(--input-text-size) !important;
301
- font-family: var(--font) !important;
302
- /* Layout */
303
- padding: var(--input-padding) !important;
304
- height: 48px !important;
305
- min-width: 70px;
306
- box-sizing: border-box;
307
- /* Dropdown styling */
308
- cursor: pointer;
309
- -webkit-appearance: none;
310
- -moz-appearance: none;
311
- appearance: none;
312
- /* Custom dropdown arrow using theme color */
313
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='%236b7280' d='M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z'/%3E%3C/svg%3E") !important;
314
- background-repeat: no-repeat !important;
315
- background-position: right 6px center !important;
316
- background-size: 16px !important;
317
- padding-right: 28px !important;
318
- }}
319
- .mega-speed-select:hover {{
320
- border-color: var(--input-border-color-hover) !important;
321
- }}
322
- .mega-speed-select:focus {{
323
- border-color: var(--input-border-color-focus) !important;
324
- outline: none;
325
- box-shadow: var(--input-shadow-focus);
326
- }}
327
- .mega-speed-select option {{
328
- background: var(--input-background-fill);
329
- color: var(--body-text-color);
330
- }}
331
- .mega-tip {{
332
- text-align: center; color: #b0b0b0; font-size: 0.95em;
333
- padding: 8px 16px; margin-bottom: 6px;
334
- background: rgba(255,255,255,0.05); border-radius: 8px;
335
- border: 1px solid rgba(255,255,255,0.08);
336
- width: fit-content; margin-left: auto; margin-right: auto;
337
- }}
338
-
339
- /* Word/char animation coloring — all modes use the window engine (JS-driven inline opacity) */
340
- :root {{ --anim-word-color: {ANIM_WORD_COLOR}; }}
341
- .word, .char {{
342
- color: inherit;
343
- }}
344
- .word.active, .char.active, .word.active .char {{
345
- color: var(--anim-word-color);
346
- }}
347
- /* Window engine: all hidden by default; JS sets inline opacity for visible window */
348
- .anim-window .word {{ opacity: 0; }}
349
- .anim-window .word.active {{ opacity: 1; }}
350
- /* Character-level Window */
351
- .anim-chars.anim-window .word {{ opacity: 1 !important; }}
352
- .anim-chars.anim-window .char {{ opacity: 0; }}
353
- .anim-chars.anim-window .char.active {{ opacity: 1; }}
354
- /* Clickable words and verse markers in mega card */
355
- .mega-text-flow .word {{ cursor: pointer; }}
356
- .mega-text-flow .verse-marker {{ cursor: pointer; }}
357
-
358
- /* Allow "All" hint below slider track to be visible */
359
- #anim-window-prev, #anim-window-after {{ overflow: visible !important; padding-bottom: 1.2em; }}
360
- #anim-window-prev *, #anim-window-after * {{ overflow: visible !important; }}
361
- #anim-settings-accordion .block {{ border: none; }}
362
- #anim-settings-accordion .color-picker {{ border: none !important; }}
363
- #anim-settings-accordion .color-picker .block {{ border: none !important; }}
364
-
365
- /* Merge style/granularity/color into one unified row */
366
- #anim-style-row {{
367
- gap: 0 !important;
368
- border: 1px solid var(--border-color-primary, #ddd);
369
- border-radius: var(--radius-lg, 8px);
370
- overflow: visible;
371
- }}
372
- #anim-style-row > div {{
373
- border: none !important;
374
- box-shadow: none !important;
375
- background: transparent !important;
376
- }}
377
- /* Side-by-side label + controls for animation settings */
378
- #anim-style-row fieldset,
379
- #anim-style-row > div:has(> .dialog-button) {{
380
- display: flex !important;
381
- flex-direction: row !important;
382
- align-items: center !important;
383
- gap: 8px;
384
- }}
385
- #anim-style-row .block-title,
386
- #anim-style-row fieldset > span:first-child {{
387
- white-space: nowrap;
388
- min-width: fit-content;
389
- margin: 0 !important;
390
- font-size: 0.9em;
391
- }}
392
-
393
- .segment-text {{
394
- font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
395
- font-size: {QURAN_TEXT_SIZE_PX}px;
396
- direction: rtl;
397
- text-align: right;
398
- line-height: 1.8;
399
- word-spacing: {ARABIC_WORD_SPACING};
400
- padding: 8px;
401
- border-radius: 4px;
402
- background: var(--block-background-fill);
403
- }}
404
- .segment-error {{
405
- font-size: 12px;
406
- margin-top: 4px;
407
- color: var(--error-text-color, #dc3545);
408
- }}
409
- .no-match {{
410
- opacity: 0.5;
411
- }}
412
- .no-segments {{
413
- text-align: center;
414
- opacity: 0.6;
415
- padding: 40px;
416
- }}
417
- .segments-header {{
418
- font-weight: bold;
419
- margin-bottom: 16px;
420
- }}
421
-
422
-
423
- /* Confidence colors - light mode */
424
- .segment-high {{ background: #d4edda; border-color: #28a745; }}
425
- .segment-med {{ background: #fff3cd; border-color: #ffc107; }}
426
- .segment-low {{ background: #f8d7da; border-color: #dc3545; }}
427
- .segment-high-badge {{ background: #28a745; }}
428
- .segment-med-badge {{ background: #ffc107; color: #333 !important; }}
429
- .segment-low-badge {{ background: #dc3545; }}
430
- .segment-underseg {{ background: #ffe5cc; border-color: #ff8c00; }}
431
- .segment-underseg-badge {{ background: #ff8c00; }}
432
-
433
- /* Review summary text colors */
434
- .segments-review-summary {{ margin-bottom: 8px; font-size: 14px; }}
435
- .segment-med-text {{ color: #856404; }}
436
- .segment-low-text {{ color: #721c24; }}
437
- .segment-underseg-text {{ color: #b35900; }}
438
- @media (prefers-color-scheme: dark) {{
439
- .segment-med-text {{ color: #ffc107; }}
440
- .segment-low-text {{ color: #f8d7da; }}
441
- .segment-underseg-text {{ color: #ff8c00; }}
442
- }}
443
- .dark .segment-med-text {{ color: #ffc107; }}
444
- .dark .segment-low-text {{ color: #f8d7da; }}
445
- .dark .segment-underseg-text {{ color: #ff8c00; }}
446
-
447
- /* Confidence colors - dark mode */
448
- @media (prefers-color-scheme: dark) {{
449
- .segment-high {{ background: rgba(40, 167, 69, 0.2); border-color: #28a745; }}
450
- .segment-med {{ background: rgba(255, 193, 7, 0.2); border-color: #ffc107; }}
451
- .segment-low {{ background: rgba(220, 53, 69, 0.2); border-color: #dc3545; }}
452
- .segment-underseg {{ background: rgba(255, 140, 0, 0.2); border-color: #ff8c00; }}
453
- }}
454
- /* Also support Gradio's dark class */
455
- .dark .segment-high {{ background: rgba(40, 167, 69, 0.2); border-color: #28a745; }}
456
- .dark .segment-med {{ background: rgba(255, 193, 7, 0.2); border-color: #ffc107; }}
457
- .dark .segment-low {{ background: rgba(220, 53, 69, 0.2); border-color: #dc3545; }}
458
- .dark .segment-underseg {{ background: rgba(255, 140, 0, 0.2); border-color: #ff8c00; }}
459
-
460
- """
461
 
462
  js = """
463
  <script>
 
9
  MIN_SPEECH_MIN, MIN_SPEECH_MAX, MIN_SPEECH_STEP,
10
  PAD_MIN, PAD_MAX, PAD_STEP,
11
  PRESET_MUJAWWAD, PRESET_MURATTAL, PRESET_FAST,
 
12
  DELETE_CACHE_FREQUENCY, DELETE_CACHE_AGE,
13
  ANIM_WORD_COLOR, ANIM_STYLE_ROW_SCALES,
14
  ANIM_DISPLAY_MODES, ANIM_DISPLAY_MODE_DEFAULT,
 
21
  MEGA_WORD_SPACING_MIN, MEGA_WORD_SPACING_MAX, MEGA_WORD_SPACING_STEP, MEGA_WORD_SPACING_DEFAULT,
22
  MEGA_TEXT_SIZE_MIN, MEGA_TEXT_SIZE_MAX, MEGA_TEXT_SIZE_STEP, MEGA_TEXT_SIZE_DEFAULT,
23
  MEGA_LINE_SPACING_MIN, MEGA_LINE_SPACING_MAX, MEGA_LINE_SPACING_STEP, MEGA_LINE_SPACING_DEFAULT,
 
24
  LEFT_COLUMN_SCALE, RIGHT_COLUMN_SCALE,
25
  )
26
+ from src.ui.styles import build_css
27
  from src.pipeline.process import (
28
  process_audio, resegment_audio,
29
  _retranscribe_wrapper, process_audio_json, save_json_export,
 
38
  def build_interface():
39
  """Build the Gradio interface."""
40
 
41
+ css = build_css()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  js = """
44
  <script>
src/ui/styles.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CSS styles for the Quran Aligner Gradio interface."""
2
+
3
+ from config import (
4
+ ANIM_WORD_COLOR,
5
+ QURAN_TEXT_SIZE_PX, ARABIC_WORD_SPACING,
6
+ MEGA_TEXT_SIZE_DEFAULT, MEGA_LINE_SPACING_DEFAULT,
7
+ MEGA_WORD_SPACING_DEFAULT, MEGA_SURAH_LIGATURE_SIZE,
8
+ )
9
+ from data.font_data import DIGITAL_KHATT_FONT_B64, SURAH_NAME_FONT_B64
10
+
11
+
12
+ def build_css() -> str:
13
+ """Return the complete CSS string for the Gradio interface."""
14
+ return f"""
15
+ /* Font faces */
16
+ @font-face {{
17
+ font-family: 'DigitalKhatt';
18
+ src: url(data:font/otf;base64,{DIGITAL_KHATT_FONT_B64}) format('opentype');
19
+ font-weight: normal;
20
+ font-style: normal;
21
+ }}
22
+ @font-face {{
23
+ font-family: 'SurahName';
24
+ src: url(data:font/truetype;base64,{SURAH_NAME_FONT_B64}) format('truetype');
25
+ font-weight: normal;
26
+ font-style: normal;
27
+ }}
28
+
29
+ .arabic-text {{
30
+ font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
31
+ direction: rtl;
32
+ text-align: right;
33
+ }}
34
+
35
+ /* Prevent output area from being in a scrolling box */
36
+ .gradio-container .prose {{
37
+ max-height: none !important;
38
+ }}
39
+ .output-html {{
40
+ max-height: none !important;
41
+ overflow: visible !important;
42
+ }}
43
+
44
+ /* Segment cards - theme adaptive */
45
+ .segment-card {{
46
+ border-radius: 8px;
47
+ padding: 12px 16px;
48
+ margin-bottom: 12px;
49
+ border: 2px solid;
50
+ }}
51
+ .segment-header {{
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ margin-bottom: 8px;
56
+ }}
57
+ .segment-title {{
58
+ font-size: 13px;
59
+ opacity: 0.9;
60
+ }}
61
+ .segment-badges {{
62
+ display: flex;
63
+ gap: 6px;
64
+ align-items: center;
65
+ }}
66
+ .segment-badge {{
67
+ color: white;
68
+ padding: 2px 8px;
69
+ border-radius: 12px;
70
+ font-size: 12px;
71
+ font-weight: bold;
72
+ }}
73
+ .segment-audio {{
74
+ margin: 8px 0;
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 8px;
78
+ }}
79
+ .segment-audio audio {{
80
+ flex: 1;
81
+ height: 32px;
82
+ border-radius: 4px;
83
+ }}
84
+
85
+ /* Lazy play button (replaces <audio controls> until clicked) */
86
+ .play-btn {{
87
+ flex: 1;
88
+ height: 32px;
89
+ border-radius: 4px;
90
+ border: 1px solid var(--border-color-primary, #ddd);
91
+ background: var(--block-background-fill, #f7f7f7);
92
+ cursor: pointer;
93
+ font-size: 16px;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ }}
98
+ .play-btn:hover {{ background: var(--block-background-fill-secondary, #eee); }}
99
+
100
+ /* Make color picker popup overlay instead of pushing content down */
101
+ .gradio-container .color-picker {{
102
+ position: relative;
103
+ overflow: visible !important;
104
+ }}
105
+ .gradio-container .color-picker .overflow-hidden,
106
+ .gradio-container .color-picker > div:last-child:not(:first-child) {{
107
+ position: absolute;
108
+ z-index: 100;
109
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
110
+ border-radius: 8px;
111
+ overflow: visible !important;
112
+ max-height: none !important;
113
+ }}
114
+ .gradio-container .color-picker .overflow-hidden {{
115
+ overflow: visible !important;
116
+ }}
117
+ .gradio-container .color-picker *,
118
+ .gradio-container .color-picker div {{
119
+ overflow: visible !important;
120
+ max-height: none !important;
121
+ scrollbar-width: none !important;
122
+ }}
123
+ .gradio-container .color-picker *::-webkit-scrollbar {{
124
+ display: none !important;
125
+ }}
126
+ /* Ensure all color-picker ancestors allow overflow for absolute popup */
127
+ #anim-settings-accordion,
128
+ #anim-settings-accordion > *,
129
+ #anim-style-row,
130
+ #anim-style-row > * {{
131
+ overflow: visible !important;
132
+ }}
133
+
134
+
135
+ /* Animate button */
136
+ .animate-btn {{
137
+ background: #4a90d9 !important;
138
+ color: white !important;
139
+ border: none;
140
+ padding: 6px 12px;
141
+ border-radius: 4px;
142
+ cursor: pointer;
143
+ font-size: 12px;
144
+ font-weight: bold;
145
+ white-space: nowrap;
146
+ }}
147
+ .animate-btn:hover:not(:disabled) {{ background: #357abd !important; }}
148
+ .animate-btn.active {{ background: #dc3545 !important; }}
149
+ .animate-btn:disabled {{ background: #888 !important; cursor: not-allowed; opacity: 0.5; }}
150
+
151
+ /* Make the HTML wrapper inside ts-row match the Gradio Button wrapper */
152
+ #ts-row > .gr-html {{
153
+ padding: 0;
154
+ margin: 0;
155
+ min-width: 0;
156
+ flex: 1 1 0%;
157
+ }}
158
+ #ts-row > div:has(> .animate-all-btn) {{
159
+ padding: 0;
160
+ margin: 0;
161
+ min-width: 0;
162
+ flex: 1 1 0%;
163
+ }}
164
+
165
+ /* Animate All button — matches Gradio lg button sizing */
166
+ .animate-all-btn {{
167
+ display: block;
168
+ width: 100%;
169
+ background: var(--button-primary-background-fill, #f97316) !important;
170
+ color: var(--button-primary-text-color, white) !important;
171
+ border: var(--button-primary-border, none);
172
+ padding: var(--size-2, 0.5rem) var(--size-4, 1rem);
173
+ border-radius: var(--button-large-radius, var(--radius-lg, 8px));
174
+ cursor: pointer;
175
+ font-size: var(--button-large-text-size, var(--text-lg, 1.125rem));
176
+ font-weight: var(--button-large-text-weight, 600);
177
+ box-sizing: border-box;
178
+ line-height: var(--line-md, 1.5);
179
+ min-height: var(--size-10, 40px);
180
+ }}
181
+ .animate-all-btn:hover:not(:disabled) {{ background: var(--button-primary-background-fill-hover, #ea6c10) !important; }}
182
+ .animate-all-btn.active {{ background: #dc3545 !important; }}
183
+ .animate-all-btn:disabled {{ background: #888 !important; cursor: not-allowed; opacity: 0.5; }}
184
+
185
+ /* Mega card for Animate All */
186
+ .mega-card {{
187
+ font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
188
+ font-size: {MEGA_TEXT_SIZE_DEFAULT}px;
189
+ direction: rtl;
190
+ text-align: justify;
191
+ line-height: {MEGA_LINE_SPACING_DEFAULT};
192
+ word-spacing: {MEGA_WORD_SPACING_DEFAULT}em;
193
+ padding: 16px;
194
+ border-radius: 8px;
195
+ background: var(--block-background-fill);
196
+ max-height: 70vh;
197
+ overflow-y: auto;
198
+ scrollbar-color: rgba(255,255,255,0.2) transparent;
199
+ }}
200
+ .mega-card::-webkit-scrollbar {{ width: 8px; }}
201
+ .mega-card::-webkit-scrollbar-track {{ background: transparent; }}
202
+ .mega-card::-webkit-scrollbar-thumb {{ background: rgba(255,255,255,0.2); border-radius: 4px; }}
203
+ .mega-card::-webkit-scrollbar-thumb:hover {{ background: rgba(255,255,255,0.35); }}
204
+ .mega-text-flow {{
205
+ display: inline;
206
+ }}
207
+ .mega-special-line {{
208
+ display: block;
209
+ text-align: center;
210
+ margin: 8px 0;
211
+ opacity: 0.7;
212
+ font-size: 0.85em;
213
+ }}
214
+ .mega-surah-separator {{
215
+ display: block;
216
+ text-align: center;
217
+ margin: 8px 0 2px;
218
+ padding: 4px 0 0;
219
+ border-top: 1px solid rgba(255,255,255,0.1);
220
+ opacity: 0.8;
221
+ font-family: 'SurahName', sans-serif;
222
+ font-feature-settings: "liga" 1;
223
+ font-size: {MEGA_SURAH_LIGATURE_SIZE}em;
224
+ line-height: 1.2;
225
+ letter-spacing: normal;
226
+ }}
227
+ .segment-card.hidden-for-mega {{ display: none; }}
228
+ #mega-styling-row {{ display: none; }}
229
+ .mega-top-bar {{
230
+ display: flex; justify-content: center; gap: 8px;
231
+ margin-top: 12px; margin-bottom: 8px;
232
+ }}
233
+ .mega-top-bar .animate-all-btn {{
234
+ width: auto; min-width: 0; min-height: auto;
235
+ padding: 4px 12px;
236
+ font-size: 12px; font-weight: bold;
237
+ border-radius: 4px;
238
+ line-height: normal;
239
+ box-sizing: border-box;
240
+ border: none;
241
+ height: 42px;
242
+ display: inline-flex;
243
+ align-items: center;
244
+ justify-content: center;
245
+ }}
246
+ .mega-exit-btn {{
247
+ background: #6c757d;
248
+ color: white;
249
+ border: none;
250
+ padding: 4px 12px;
251
+ border-radius: 4px;
252
+ cursor: pointer;
253
+ font-size: 12px;
254
+ font-weight: bold;
255
+ min-width: 0; min-height: auto;
256
+ line-height: normal;
257
+ box-sizing: border-box;
258
+ height: 42px;
259
+ display: inline-flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ }}
263
+ .mega-exit-btn:hover {{ background: #5a6268; }}
264
+ .mega-speed-select {{
265
+ /* Use Gradio's theme-aware variables */
266
+ background: var(--input-background-fill) !important;
267
+ color: var(--body-text-color) !important;
268
+ border: var(--input-border-width) solid var(--input-border-color) !important;
269
+ border-radius: var(--input-radius) !important;
270
+ /* Typography from Gradio */
271
+ font-size: var(--input-text-size) !important;
272
+ font-family: var(--font) !important;
273
+ /* Layout */
274
+ padding: var(--input-padding) !important;
275
+ height: 48px !important;
276
+ min-width: 70px;
277
+ box-sizing: border-box;
278
+ /* Dropdown styling */
279
+ cursor: pointer;
280
+ -webkit-appearance: none;
281
+ -moz-appearance: none;
282
+ appearance: none;
283
+ /* Custom dropdown arrow using theme color */
284
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='%236b7280' d='M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z'/%3E%3C/svg%3E") !important;
285
+ background-repeat: no-repeat !important;
286
+ background-position: right 6px center !important;
287
+ background-size: 16px !important;
288
+ padding-right: 28px !important;
289
+ }}
290
+ .mega-speed-select:hover {{
291
+ border-color: var(--input-border-color-hover) !important;
292
+ }}
293
+ .mega-speed-select:focus {{
294
+ border-color: var(--input-border-color-focus) !important;
295
+ outline: none;
296
+ box-shadow: var(--input-shadow-focus);
297
+ }}
298
+ .mega-speed-select option {{
299
+ background: var(--input-background-fill);
300
+ color: var(--body-text-color);
301
+ }}
302
+ .mega-tip {{
303
+ text-align: center; color: #b0b0b0; font-size: 0.95em;
304
+ padding: 8px 16px; margin-bottom: 6px;
305
+ background: rgba(255,255,255,0.05); border-radius: 8px;
306
+ border: 1px solid rgba(255,255,255,0.08);
307
+ width: fit-content; margin-left: auto; margin-right: auto;
308
+ }}
309
+
310
+ /* Word/char animation coloring — all modes use the window engine (JS-driven inline opacity) */
311
+ :root {{ --anim-word-color: {ANIM_WORD_COLOR}; }}
312
+ .word, .char {{
313
+ color: inherit;
314
+ }}
315
+ .word.active, .char.active, .word.active .char {{
316
+ color: var(--anim-word-color);
317
+ }}
318
+ /* Window engine: all hidden by default; JS sets inline opacity for visible window */
319
+ .anim-window .word {{ opacity: 0; }}
320
+ .anim-window .word.active {{ opacity: 1; }}
321
+ /* Character-level Window */
322
+ .anim-chars.anim-window .word {{ opacity: 1 !important; }}
323
+ .anim-chars.anim-window .char {{ opacity: 0; }}
324
+ .anim-chars.anim-window .char.active {{ opacity: 1; }}
325
+ /* Clickable words and verse markers in mega card */
326
+ .mega-text-flow .word {{ cursor: pointer; }}
327
+ .mega-text-flow .verse-marker {{ cursor: pointer; }}
328
+
329
+ /* Allow "All" hint below slider track to be visible */
330
+ #anim-window-prev, #anim-window-after {{ overflow: visible !important; padding-bottom: 1.2em; }}
331
+ #anim-window-prev *, #anim-window-after * {{ overflow: visible !important; }}
332
+ #anim-settings-accordion .block {{ border: none; }}
333
+ #anim-settings-accordion .color-picker {{ border: none !important; }}
334
+ #anim-settings-accordion .color-picker .block {{ border: none !important; }}
335
+
336
+ /* Merge style/granularity/color into one unified row */
337
+ #anim-style-row {{
338
+ gap: 0 !important;
339
+ border: 1px solid var(--border-color-primary, #ddd);
340
+ border-radius: var(--radius-lg, 8px);
341
+ overflow: visible;
342
+ }}
343
+ #anim-style-row > div {{
344
+ border: none !important;
345
+ box-shadow: none !important;
346
+ background: transparent !important;
347
+ }}
348
+ /* Side-by-side label + controls for animation settings */
349
+ #anim-style-row fieldset,
350
+ #anim-style-row > div:has(> .dialog-button) {{
351
+ display: flex !important;
352
+ flex-direction: row !important;
353
+ align-items: center !important;
354
+ gap: 8px;
355
+ }}
356
+ #anim-style-row .block-title,
357
+ #anim-style-row fieldset > span:first-child {{
358
+ white-space: nowrap;
359
+ min-width: fit-content;
360
+ margin: 0 !important;
361
+ font-size: 0.9em;
362
+ }}
363
+
364
+ .segment-text {{
365
+ font-family: 'DigitalKhatt', 'Traditional Arabic', sans-serif;
366
+ font-size: {QURAN_TEXT_SIZE_PX}px;
367
+ direction: rtl;
368
+ text-align: right;
369
+ line-height: 1.8;
370
+ word-spacing: {ARABIC_WORD_SPACING};
371
+ padding: 8px;
372
+ border-radius: 4px;
373
+ background: var(--block-background-fill);
374
+ }}
375
+ .segment-error {{
376
+ font-size: 12px;
377
+ margin-top: 4px;
378
+ color: var(--error-text-color, #dc3545);
379
+ }}
380
+ .no-match {{
381
+ opacity: 0.5;
382
+ }}
383
+ .no-segments {{
384
+ text-align: center;
385
+ opacity: 0.6;
386
+ padding: 40px;
387
+ }}
388
+ .segments-header {{
389
+ font-weight: bold;
390
+ margin-bottom: 16px;
391
+ }}
392
+
393
+
394
+ /* Confidence colors - light mode */
395
+ .segment-high {{ background: #d4edda; border-color: #28a745; }}
396
+ .segment-med {{ background: #fff3cd; border-color: #ffc107; }}
397
+ .segment-low {{ background: #f8d7da; border-color: #dc3545; }}
398
+ .segment-high-badge {{ background: #28a745; }}
399
+ .segment-med-badge {{ background: #ffc107; color: #333 !important; }}
400
+ .segment-low-badge {{ background: #dc3545; }}
401
+ .segment-underseg {{ background: #ffe5cc; border-color: #ff8c00; }}
402
+ .segment-underseg-badge {{ background: #ff8c00; }}
403
+
404
+ /* Review summary text colors */
405
+ .segments-review-summary {{ margin-bottom: 8px; font-size: 14px; }}
406
+ .segment-med-text {{ color: #856404; }}
407
+ .segment-low-text {{ color: #721c24; }}
408
+ .segment-underseg-text {{ color: #b35900; }}
409
+ @media (prefers-color-scheme: dark) {{
410
+ .segment-med-text {{ color: #ffc107; }}
411
+ .segment-low-text {{ color: #f8d7da; }}
412
+ .segment-underseg-text {{ color: #ff8c00; }}
413
+ }}
414
+ .dark .segment-med-text {{ color: #ffc107; }}
415
+ .dark .segment-low-text {{ color: #f8d7da; }}
416
+ .dark .segment-underseg-text {{ color: #ff8c00; }}
417
+
418
+ /* Confidence colors - dark mode */
419
+ @media (prefers-color-scheme: dark) {{
420
+ .segment-high {{ background: rgba(40, 167, 69, 0.2); border-color: #28a745; }}
421
+ .segment-med {{ background: rgba(255, 193, 7, 0.2); border-color: #ffc107; }}
422
+ .segment-low {{ background: rgba(220, 53, 69, 0.2); border-color: #dc3545; }}
423
+ .segment-underseg {{ background: rgba(255, 140, 0, 0.2); border-color: #ff8c00; }}
424
+ }}
425
+ /* Also support Gradio's dark class */
426
+ .dark .segment-high {{ background: rgba(40, 167, 69, 0.2); border-color: #28a745; }}
427
+ .dark .segment-med {{ background: rgba(255, 193, 7, 0.2); border-color: #ffc107; }}
428
+ .dark .segment-low {{ background: rgba(220, 53, 69, 0.2); border-color: #dc3545; }}
429
+ .dark .segment-underseg {{ background: rgba(255, 140, 0, 0.2); border-color: #ff8c00; }}
430
+
431
+ """