doctra 0.1.1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doctra/__init__.py +21 -18
- doctra/cli/main.py +5 -2
- doctra/cli/utils.py +12 -3
- doctra/engines/layout/paddle_layout.py +13 -78
- doctra/engines/vlm/provider.py +86 -58
- doctra/engines/vlm/service.py +10 -14
- doctra/exporters/html_writer.py +1235 -0
- doctra/parsers/structured_pdf_parser.py +35 -15
- doctra/parsers/table_chart_extractor.py +66 -28
- doctra/ui/__init__.py +5 -0
- doctra/ui/app.py +1012 -0
- doctra/utils/progress.py +428 -0
- doctra/utils/structured_utils.py +49 -49
- doctra/version.py +1 -1
- {doctra-0.1.1.dist-info → doctra-0.3.0.dist-info}/METADATA +45 -6
- {doctra-0.1.1.dist-info → doctra-0.3.0.dist-info}/RECORD +19 -15
- {doctra-0.1.1.dist-info → doctra-0.3.0.dist-info}/WHEEL +0 -0
- {doctra-0.1.1.dist-info → doctra-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {doctra-0.1.1.dist-info → doctra-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1235 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import os
|
3
|
+
import re
|
4
|
+
import base64
|
5
|
+
from typing import List, Dict, Any
|
6
|
+
from markdown_it import MarkdownIt
|
7
|
+
|
8
|
+
|
9
|
+
def _process_image_paths(md_content: str, out_dir: str) -> str:
|
10
|
+
"""
|
11
|
+
Process image paths in markdown content to ensure they work in HTML.
|
12
|
+
Converts relative paths to base64 embedded images for better portability.
|
13
|
+
|
14
|
+
:param md_content: The markdown content with image references
|
15
|
+
:param out_dir: The output directory where images are stored
|
16
|
+
:return: Processed markdown content with embedded images
|
17
|
+
"""
|
18
|
+
def replace_image(match):
|
19
|
+
caption = match.group(1)
|
20
|
+
img_path = match.group(2)
|
21
|
+
|
22
|
+
# Convert relative path to absolute path
|
23
|
+
if not os.path.isabs(img_path):
|
24
|
+
abs_img_path = os.path.join(out_dir, img_path)
|
25
|
+
else:
|
26
|
+
abs_img_path = img_path
|
27
|
+
|
28
|
+
# Check if image exists
|
29
|
+
if os.path.exists(abs_img_path):
|
30
|
+
try:
|
31
|
+
# Read image and convert to base64
|
32
|
+
with open(abs_img_path, 'rb') as f:
|
33
|
+
img_data = f.read()
|
34
|
+
img_ext = os.path.splitext(abs_img_path)[1].lower()
|
35
|
+
|
36
|
+
# Determine MIME type
|
37
|
+
if img_ext in ['.jpg', '.jpeg']:
|
38
|
+
mime_type = 'image/jpeg'
|
39
|
+
elif img_ext == '.png':
|
40
|
+
mime_type = 'image/png'
|
41
|
+
elif img_ext == '.gif':
|
42
|
+
mime_type = 'image/gif'
|
43
|
+
elif img_ext == '.webp':
|
44
|
+
mime_type = 'image/webp'
|
45
|
+
else:
|
46
|
+
mime_type = 'image/jpeg' # fallback
|
47
|
+
|
48
|
+
# Encode to base64
|
49
|
+
b64_data = base64.b64encode(img_data).decode('ascii')
|
50
|
+
|
51
|
+
# Return HTML img tag with base64 data
|
52
|
+
return f'<img src="data:{mime_type};base64,{b64_data}" alt="{caption}" />'
|
53
|
+
except Exception as e:
|
54
|
+
print(f"Warning: Could not process image {abs_img_path}: {e}")
|
55
|
+
return f'<div class="image-error">Image not found: {caption}</div>'
|
56
|
+
else:
|
57
|
+
print(f"Warning: Image file not found: {abs_img_path}")
|
58
|
+
return f'<div class="image-error">Image not found: {caption}</div>'
|
59
|
+
|
60
|
+
# Find all image references in markdown format 
|
61
|
+
pattern = r'!\[([^\]]*)\]\(([^)]+)\)'
|
62
|
+
processed_content = re.sub(pattern, replace_image, md_content)
|
63
|
+
|
64
|
+
return processed_content
|
65
|
+
|
66
|
+
|
67
|
+
def write_html(md_lines: List[str], out_dir: str, filename: str = "result.html") -> str:
|
68
|
+
"""
|
69
|
+
Convert collected Markdown lines into a single HTML file and save it.
|
70
|
+
|
71
|
+
Converts Markdown content to HTML with proper styling, table support,
|
72
|
+
and code highlighting. Includes a modern, responsive design.
|
73
|
+
|
74
|
+
:param md_lines: List of markdown strings to join into a single file
|
75
|
+
:param out_dir: Directory where the HTML file will be saved
|
76
|
+
:param filename: Name of the HTML file (default: "result.html")
|
77
|
+
:return: The absolute path of the written HTML file
|
78
|
+
"""
|
79
|
+
os.makedirs(out_dir, exist_ok=True)
|
80
|
+
|
81
|
+
# Join markdown lines and clean up excessive blank lines
|
82
|
+
md_content = "\n".join(md_lines).strip() + "\n"
|
83
|
+
md_content = re.sub(r"\n{3,}", "\n\n", md_content)
|
84
|
+
|
85
|
+
# Process image paths to convert relative paths to absolute paths or base64
|
86
|
+
md_content = _process_image_paths(md_content, out_dir)
|
87
|
+
|
88
|
+
# Convert markdown to HTML with markdown-it-py
|
89
|
+
md = MarkdownIt("commonmark", {"breaks": True, "html": True})
|
90
|
+
|
91
|
+
# Render markdown to HTML
|
92
|
+
html_body = md.render(md_content)
|
93
|
+
|
94
|
+
# Always apply table styling to ensure all tables are properly formatted
|
95
|
+
html_body = _add_table_styling(html_body)
|
96
|
+
|
97
|
+
# Create complete HTML document with modern styling
|
98
|
+
html_content = f"""<!DOCTYPE html>
|
99
|
+
<html lang="en">
|
100
|
+
<head>
|
101
|
+
<meta charset="UTF-8">
|
102
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
103
|
+
<title>Document Analysis Results</title>
|
104
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
105
|
+
<style>
|
106
|
+
{_get_css_styles()}
|
107
|
+
</style>
|
108
|
+
</head>
|
109
|
+
<body>
|
110
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle dark mode"></button>
|
111
|
+
<div class="container">
|
112
|
+
<header class="header">
|
113
|
+
<div class="header-content">
|
114
|
+
<div class="header-text">
|
115
|
+
<h1>Document Analysis Results</h1>
|
116
|
+
<p class="subtitle">Intelligent Document Processing & Analysis</p>
|
117
|
+
</div>
|
118
|
+
<div class="header-badge">
|
119
|
+
Generated by Doctra
|
120
|
+
</div>
|
121
|
+
</div>
|
122
|
+
</header>
|
123
|
+
<main class="content">
|
124
|
+
{html_body}
|
125
|
+
</main>
|
126
|
+
<footer class="footer">
|
127
|
+
<div class="footer-content">
|
128
|
+
<div class="footer-brand">Doctra</div>
|
129
|
+
<div class="footer-info">
|
130
|
+
<span>Intelligent Document Processing</span>
|
131
|
+
<a href="https://github.com/AdemBoukhris457/Doctra" target="_blank">GitHub</a>
|
132
|
+
</div>
|
133
|
+
</div>
|
134
|
+
</footer>
|
135
|
+
</div>
|
136
|
+
<script>
|
137
|
+
// Theme toggle functionality
|
138
|
+
function toggleTheme() {{
|
139
|
+
const body = document.body;
|
140
|
+
const currentTheme = body.getAttribute('data-theme');
|
141
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
142
|
+
|
143
|
+
body.setAttribute('data-theme', newTheme);
|
144
|
+
localStorage.setItem('doctra-theme', newTheme);
|
145
|
+
|
146
|
+
// Add smooth transition
|
147
|
+
body.style.transition = 'all 0.3s ease';
|
148
|
+
setTimeout(() => {{
|
149
|
+
body.style.transition = '';
|
150
|
+
}}, 300);
|
151
|
+
}}
|
152
|
+
|
153
|
+
// Load saved theme on page load
|
154
|
+
document.addEventListener('DOMContentLoaded', function() {{
|
155
|
+
const savedTheme = localStorage.getItem('doctra-theme') || 'light';
|
156
|
+
document.body.setAttribute('data-theme', savedTheme);
|
157
|
+
}});
|
158
|
+
|
159
|
+
// Add smooth scroll behavior
|
160
|
+
document.documentElement.style.scrollBehavior = 'smooth';
|
161
|
+
|
162
|
+
// Add loading animation
|
163
|
+
window.addEventListener('load', function() {{
|
164
|
+
document.body.style.opacity = '0';
|
165
|
+
document.body.style.transition = 'opacity 0.5s ease';
|
166
|
+
setTimeout(() => {{
|
167
|
+
document.body.style.opacity = '1';
|
168
|
+
}}, 100);
|
169
|
+
}});
|
170
|
+
</script>
|
171
|
+
</body>
|
172
|
+
</html>"""
|
173
|
+
|
174
|
+
html_path = os.path.join(out_dir, filename)
|
175
|
+
with open(html_path, "w", encoding="utf-8") as f:
|
176
|
+
f.write(html_content)
|
177
|
+
|
178
|
+
return os.path.abspath(html_path)
|
179
|
+
|
180
|
+
|
181
|
+
def write_structured_html(html_path: str, items: List[Dict[str, Any]]) -> str | None:
|
182
|
+
"""
|
183
|
+
Write a list of structured data items into an HTML file with tables.
|
184
|
+
|
185
|
+
Each item becomes a separate section with styled tables. The function
|
186
|
+
handles data normalization and creates a responsive HTML layout.
|
187
|
+
|
188
|
+
:param html_path: Path where the HTML file will be saved
|
189
|
+
:param items: List of dictionaries, each containing:
|
190
|
+
- 'title': Section title (optional)
|
191
|
+
- 'headers': List of column headers (optional)
|
192
|
+
- 'rows': List of data rows (optional)
|
193
|
+
:return: Path to the written HTML file if successful, None if no items provided
|
194
|
+
"""
|
195
|
+
if not items:
|
196
|
+
return None
|
197
|
+
|
198
|
+
# Filter out items that have no meaningful data
|
199
|
+
valid_items = []
|
200
|
+
for item in items:
|
201
|
+
headers = item.get("headers") or []
|
202
|
+
rows = item.get("rows") or []
|
203
|
+
# Keep items that have either headers or rows with data
|
204
|
+
if headers or (rows and any(
|
205
|
+
row for row in rows if any(cell for cell in row if cell is not None and str(cell).strip()))):
|
206
|
+
valid_items.append(item)
|
207
|
+
|
208
|
+
if not valid_items:
|
209
|
+
print("Warning: No valid items to write to HTML")
|
210
|
+
return None
|
211
|
+
|
212
|
+
os.makedirs(os.path.dirname(html_path) or ".", exist_ok=True)
|
213
|
+
|
214
|
+
# Generate HTML content
|
215
|
+
html_sections = []
|
216
|
+
for item in valid_items:
|
217
|
+
try:
|
218
|
+
title = item.get("title") or "Untitled"
|
219
|
+
headers = item.get("headers") or []
|
220
|
+
rows = item.get("rows") or []
|
221
|
+
|
222
|
+
# Normalize data to handle mismatched dimensions
|
223
|
+
normalized_headers, normalized_rows = _normalize_data(headers, rows)
|
224
|
+
|
225
|
+
if not normalized_rows and not normalized_headers:
|
226
|
+
print(f"Skipping empty item: {title}")
|
227
|
+
continue
|
228
|
+
|
229
|
+
# Create HTML table
|
230
|
+
table_html = _create_html_table(normalized_headers, normalized_rows)
|
231
|
+
section_html = f"""
|
232
|
+
<section class="data-section">
|
233
|
+
<h2 class="section-title">{_escape_html(title)}</h2>
|
234
|
+
{table_html}
|
235
|
+
</section>
|
236
|
+
"""
|
237
|
+
html_sections.append(section_html)
|
238
|
+
|
239
|
+
except Exception as e:
|
240
|
+
print(f"Error processing item '{item.get('title', 'Unknown')}': {e}")
|
241
|
+
continue
|
242
|
+
|
243
|
+
if not html_sections:
|
244
|
+
print("Warning: No valid sections to write to HTML")
|
245
|
+
return None
|
246
|
+
|
247
|
+
# Create complete HTML document
|
248
|
+
html_content = f"""<!DOCTYPE html>
|
249
|
+
<html lang="en">
|
250
|
+
<head>
|
251
|
+
<meta charset="UTF-8">
|
252
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
253
|
+
<title>Structured Data Export</title>
|
254
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
255
|
+
<style>
|
256
|
+
{_get_css_styles()}
|
257
|
+
{_get_table_css_styles()}
|
258
|
+
</style>
|
259
|
+
</head>
|
260
|
+
<body>
|
261
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle dark mode"></button>
|
262
|
+
<div class="container">
|
263
|
+
<header class="header">
|
264
|
+
<div class="header-content">
|
265
|
+
<div class="header-text">
|
266
|
+
<h1>Structured Data Export</h1>
|
267
|
+
<p class="subtitle">Intelligent Document Processing & Analysis</p>
|
268
|
+
</div>
|
269
|
+
<div class="header-badge">
|
270
|
+
Generated by Doctra
|
271
|
+
</div>
|
272
|
+
</div>
|
273
|
+
</header>
|
274
|
+
<main class="content">
|
275
|
+
{''.join(html_sections)}
|
276
|
+
</main>
|
277
|
+
<footer class="footer">
|
278
|
+
<div class="footer-content">
|
279
|
+
<div class="footer-brand">Doctra</div>
|
280
|
+
<div class="footer-info">
|
281
|
+
<span>Intelligent Document Processing</span>
|
282
|
+
<a href="https://github.com/AdemBoukhris457/Doctra" target="_blank">GitHub</a>
|
283
|
+
</div>
|
284
|
+
</div>
|
285
|
+
</footer>
|
286
|
+
</div>
|
287
|
+
<script>
|
288
|
+
// Theme toggle functionality
|
289
|
+
function toggleTheme() {{
|
290
|
+
const body = document.body;
|
291
|
+
const currentTheme = body.getAttribute('data-theme');
|
292
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
293
|
+
|
294
|
+
body.setAttribute('data-theme', newTheme);
|
295
|
+
localStorage.setItem('doctra-theme', newTheme);
|
296
|
+
|
297
|
+
// Add smooth transition
|
298
|
+
body.style.transition = 'all 0.3s ease';
|
299
|
+
setTimeout(() => {{
|
300
|
+
body.style.transition = '';
|
301
|
+
}}, 300);
|
302
|
+
}}
|
303
|
+
|
304
|
+
// Load saved theme on page load
|
305
|
+
document.addEventListener('DOMContentLoaded', function() {{
|
306
|
+
const savedTheme = localStorage.getItem('doctra-theme') || 'light';
|
307
|
+
document.body.setAttribute('data-theme', savedTheme);
|
308
|
+
}});
|
309
|
+
|
310
|
+
// Add smooth scroll behavior
|
311
|
+
document.documentElement.style.scrollBehavior = 'smooth';
|
312
|
+
|
313
|
+
// Add loading animation
|
314
|
+
window.addEventListener('load', function() {{
|
315
|
+
document.body.style.opacity = '0';
|
316
|
+
document.body.style.transition = 'opacity 0.5s ease';
|
317
|
+
setTimeout(() => {{
|
318
|
+
document.body.style.opacity = '1';
|
319
|
+
}}, 100);
|
320
|
+
}});
|
321
|
+
|
322
|
+
// Add table interaction enhancements
|
323
|
+
document.addEventListener('DOMContentLoaded', function() {{
|
324
|
+
const tables = document.querySelectorAll('.data-table');
|
325
|
+
tables.forEach(table => {{
|
326
|
+
table.addEventListener('mouseenter', function() {{
|
327
|
+
this.style.transform = 'scale(1.01)';
|
328
|
+
}});
|
329
|
+
table.addEventListener('mouseleave', function() {{
|
330
|
+
this.style.transform = 'scale(1)';
|
331
|
+
}});
|
332
|
+
}});
|
333
|
+
}});
|
334
|
+
</script>
|
335
|
+
</body>
|
336
|
+
</html>"""
|
337
|
+
|
338
|
+
with open(html_path, "w", encoding="utf-8") as f:
|
339
|
+
f.write(html_content)
|
340
|
+
|
341
|
+
return html_path
|
342
|
+
|
343
|
+
|
344
|
+
def _normalize_data(headers: List[str], rows: List[List]) -> tuple[List[str], List[List]]:
|
345
|
+
"""
|
346
|
+
Normalize headers and rows to ensure consistent dimensions.
|
347
|
+
|
348
|
+
:param headers: List of column headers
|
349
|
+
:param rows: List of data rows
|
350
|
+
:return: Tuple of (normalized_headers, normalized_rows)
|
351
|
+
"""
|
352
|
+
if not rows:
|
353
|
+
return headers, []
|
354
|
+
|
355
|
+
# Find the maximum number of columns across all rows
|
356
|
+
max_cols = max(len(row) for row in rows) if rows else 0
|
357
|
+
|
358
|
+
# If we have headers, use them as the basis, otherwise use max columns
|
359
|
+
if headers:
|
360
|
+
target_cols = max(len(headers), max_cols)
|
361
|
+
else:
|
362
|
+
target_cols = max_cols
|
363
|
+
headers = [f"Column_{i + 1}" for i in range(target_cols)]
|
364
|
+
|
365
|
+
# Normalize headers: pad with generic names if too short, truncate if too long
|
366
|
+
normalized_headers = list(headers)
|
367
|
+
while len(normalized_headers) < target_cols:
|
368
|
+
normalized_headers.append(f"Column_{len(normalized_headers) + 1}")
|
369
|
+
normalized_headers = normalized_headers[:target_cols]
|
370
|
+
|
371
|
+
# Normalize rows: pad with None if too short, truncate if too long
|
372
|
+
normalized_rows = []
|
373
|
+
for row in rows:
|
374
|
+
normalized_row = list(row)
|
375
|
+
while len(normalized_row) < target_cols:
|
376
|
+
normalized_row.append(None)
|
377
|
+
normalized_rows.append(normalized_row[:target_cols])
|
378
|
+
|
379
|
+
return normalized_headers, normalized_rows
|
380
|
+
|
381
|
+
|
382
|
+
def _create_html_table(headers: List[str], rows: List[List]) -> str:
|
383
|
+
"""
|
384
|
+
Create an HTML table from headers and rows.
|
385
|
+
|
386
|
+
:param headers: List of column headers
|
387
|
+
:param rows: List of data rows
|
388
|
+
:return: HTML table string
|
389
|
+
"""
|
390
|
+
if not headers and not rows:
|
391
|
+
return "<p class='no-data'>No data available</p>"
|
392
|
+
|
393
|
+
# Create table header
|
394
|
+
header_html = ""
|
395
|
+
if headers:
|
396
|
+
header_cells = "".join(f"<th>{_escape_html(str(header))}</th>" for header in headers)
|
397
|
+
header_html = f"<thead><tr>{header_cells}</tr></thead>"
|
398
|
+
|
399
|
+
# Create table body
|
400
|
+
body_rows = []
|
401
|
+
for row in rows:
|
402
|
+
cells = "".join(f"<td>{_escape_html(str(cell) if cell is not None else '')}</td>" for cell in row)
|
403
|
+
body_rows.append(f"<tr>{cells}</tr>")
|
404
|
+
|
405
|
+
body_html = f"<tbody>{''.join(body_rows)}</tbody>" if body_rows else ""
|
406
|
+
|
407
|
+
return f"""
|
408
|
+
<div class="table-container">
|
409
|
+
<table class="data-table">
|
410
|
+
{header_html}
|
411
|
+
{body_html}
|
412
|
+
</table>
|
413
|
+
</div>
|
414
|
+
"""
|
415
|
+
|
416
|
+
|
417
|
+
def _add_table_styling(html_content: str) -> str:
|
418
|
+
"""
|
419
|
+
Add table styling wrapper to HTML content.
|
420
|
+
|
421
|
+
:param html_content: HTML content that may contain tables
|
422
|
+
:return: HTML content with table styling
|
423
|
+
"""
|
424
|
+
# Wrap tables in a styled container - handle both <table> and <table class="...">
|
425
|
+
html_content = re.sub(
|
426
|
+
r'<table(?:\s+[^>]*)?>',
|
427
|
+
'<div class="table-container"><table class="markdown-table">',
|
428
|
+
html_content
|
429
|
+
)
|
430
|
+
html_content = re.sub(
|
431
|
+
r'</table>',
|
432
|
+
'</table></div>',
|
433
|
+
html_content
|
434
|
+
)
|
435
|
+
return html_content
|
436
|
+
|
437
|
+
|
438
|
+
def _escape_html(text: str) -> str:
|
439
|
+
"""
|
440
|
+
Escape HTML special characters.
|
441
|
+
|
442
|
+
:param text: Text to escape
|
443
|
+
:return: Escaped text
|
444
|
+
"""
|
445
|
+
if not text:
|
446
|
+
return ""
|
447
|
+
|
448
|
+
text = str(text)
|
449
|
+
text = text.replace("&", "&")
|
450
|
+
text = text.replace("<", "<")
|
451
|
+
text = text.replace(">", ">")
|
452
|
+
text = text.replace('"', """)
|
453
|
+
text = text.replace("'", "'")
|
454
|
+
return text
|
455
|
+
|
456
|
+
|
457
|
+
def _get_css_styles() -> str:
|
458
|
+
"""Get CSS styles for the HTML document."""
|
459
|
+
return """
|
460
|
+
:root {
|
461
|
+
--primary-color: #1e40af;
|
462
|
+
--secondary-color: #1e3a8a;
|
463
|
+
--accent-color: #3b82f6;
|
464
|
+
--success-color: #059669;
|
465
|
+
--warning-color: #d97706;
|
466
|
+
--error-color: #dc2626;
|
467
|
+
--text-color: #111827;
|
468
|
+
--text-light: #6b7280;
|
469
|
+
--text-muted: #9ca3af;
|
470
|
+
--bg-color: #f9fafb;
|
471
|
+
--card-bg: #ffffff;
|
472
|
+
--border-color: #e5e7eb;
|
473
|
+
--border-light: #f3f4f6;
|
474
|
+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
475
|
+
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06);
|
476
|
+
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
|
477
|
+
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);
|
478
|
+
--border-radius: 8px;
|
479
|
+
--border-radius-lg: 12px;
|
480
|
+
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
481
|
+
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
482
|
+
--font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
|
483
|
+
}
|
484
|
+
|
485
|
+
* {
|
486
|
+
margin: 0;
|
487
|
+
padding: 0;
|
488
|
+
box-sizing: border-box;
|
489
|
+
}
|
490
|
+
|
491
|
+
html {
|
492
|
+
scroll-behavior: smooth;
|
493
|
+
}
|
494
|
+
|
495
|
+
body {
|
496
|
+
font-family: var(--font-family);
|
497
|
+
line-height: 1.6;
|
498
|
+
color: var(--text-color);
|
499
|
+
background: var(--bg-color);
|
500
|
+
min-height: 100vh;
|
501
|
+
font-size: 16px;
|
502
|
+
-webkit-font-smoothing: antialiased;
|
503
|
+
-moz-osx-font-smoothing: grayscale;
|
504
|
+
}
|
505
|
+
|
506
|
+
.container {
|
507
|
+
max-width: 1200px;
|
508
|
+
margin: 0 auto;
|
509
|
+
padding: 0;
|
510
|
+
background: var(--card-bg);
|
511
|
+
min-height: 100vh;
|
512
|
+
box-shadow: var(--shadow-xl);
|
513
|
+
}
|
514
|
+
|
515
|
+
.header {
|
516
|
+
background: var(--primary-color);
|
517
|
+
color: white;
|
518
|
+
padding: 2rem 3rem;
|
519
|
+
border-bottom: 4px solid var(--accent-color);
|
520
|
+
position: relative;
|
521
|
+
}
|
522
|
+
|
523
|
+
.header::before {
|
524
|
+
content: '';
|
525
|
+
position: absolute;
|
526
|
+
top: 0;
|
527
|
+
left: 0;
|
528
|
+
right: 0;
|
529
|
+
bottom: 0;
|
530
|
+
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
|
531
|
+
pointer-events: none;
|
532
|
+
}
|
533
|
+
|
534
|
+
.header-content {
|
535
|
+
position: relative;
|
536
|
+
z-index: 1;
|
537
|
+
display: flex;
|
538
|
+
align-items: center;
|
539
|
+
justify-content: space-between;
|
540
|
+
}
|
541
|
+
|
542
|
+
.header-text {
|
543
|
+
flex: 1;
|
544
|
+
}
|
545
|
+
|
546
|
+
.header h1 {
|
547
|
+
font-size: 2.5rem;
|
548
|
+
margin-bottom: 0.5rem;
|
549
|
+
font-weight: 700;
|
550
|
+
letter-spacing: -0.025em;
|
551
|
+
}
|
552
|
+
|
553
|
+
.subtitle {
|
554
|
+
font-size: 1.1rem;
|
555
|
+
opacity: 0.9;
|
556
|
+
font-weight: 400;
|
557
|
+
color: rgba(255, 255, 255, 0.8);
|
558
|
+
}
|
559
|
+
|
560
|
+
.header-badge {
|
561
|
+
background: rgba(255, 255, 255, 0.2);
|
562
|
+
padding: 0.5rem 1rem;
|
563
|
+
border-radius: var(--border-radius);
|
564
|
+
font-size: 0.9rem;
|
565
|
+
font-weight: 500;
|
566
|
+
backdrop-filter: blur(10px);
|
567
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
568
|
+
}
|
569
|
+
|
570
|
+
.content {
|
571
|
+
padding: 3rem;
|
572
|
+
background: var(--card-bg);
|
573
|
+
}
|
574
|
+
|
575
|
+
.content h1, .content h2, .content h3, .content h4, .content h5, .content h6 {
|
576
|
+
color: var(--text-color);
|
577
|
+
margin-top: 2rem;
|
578
|
+
margin-bottom: 1rem;
|
579
|
+
font-weight: 600;
|
580
|
+
line-height: 1.3;
|
581
|
+
}
|
582
|
+
|
583
|
+
.content h1 {
|
584
|
+
font-size: 2.25rem;
|
585
|
+
font-weight: 700;
|
586
|
+
color: var(--primary-color);
|
587
|
+
border-bottom: 3px solid var(--accent-color);
|
588
|
+
padding-bottom: 0.5rem;
|
589
|
+
margin-bottom: 1.5rem;
|
590
|
+
}
|
591
|
+
.content h2 {
|
592
|
+
font-size: 1.875rem;
|
593
|
+
font-weight: 600;
|
594
|
+
color: var(--text-color);
|
595
|
+
margin-top: 2.5rem;
|
596
|
+
margin-bottom: 1rem;
|
597
|
+
position: relative;
|
598
|
+
}
|
599
|
+
.content h2::before {
|
600
|
+
content: '';
|
601
|
+
position: absolute;
|
602
|
+
left: -1rem;
|
603
|
+
top: 50%;
|
604
|
+
transform: translateY(-50%);
|
605
|
+
width: 4px;
|
606
|
+
height: 1.5rem;
|
607
|
+
background: var(--accent-color);
|
608
|
+
border-radius: 2px;
|
609
|
+
}
|
610
|
+
.content h3 {
|
611
|
+
font-size: 1.5rem;
|
612
|
+
font-weight: 600;
|
613
|
+
color: var(--text-color);
|
614
|
+
margin-top: 2rem;
|
615
|
+
}
|
616
|
+
.content h4 {
|
617
|
+
font-size: 1.25rem;
|
618
|
+
font-weight: 600;
|
619
|
+
color: var(--text-light);
|
620
|
+
margin-top: 1.5rem;
|
621
|
+
}
|
622
|
+
|
623
|
+
.content p {
|
624
|
+
margin-bottom: 1.25rem;
|
625
|
+
font-size: 1rem;
|
626
|
+
line-height: 1.7;
|
627
|
+
color: var(--text-color);
|
628
|
+
}
|
629
|
+
|
630
|
+
.content img {
|
631
|
+
max-width: 100%;
|
632
|
+
height: auto;
|
633
|
+
border-radius: var(--border-radius);
|
634
|
+
box-shadow: var(--shadow-md);
|
635
|
+
margin: 1.5rem 0;
|
636
|
+
transition: var(--transition);
|
637
|
+
border: 1px solid var(--border-color);
|
638
|
+
}
|
639
|
+
|
640
|
+
.content img:hover {
|
641
|
+
transform: translateY(-2px);
|
642
|
+
box-shadow: var(--shadow-lg);
|
643
|
+
}
|
644
|
+
|
645
|
+
.image-error {
|
646
|
+
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
647
|
+
border: 2px dashed #f44336;
|
648
|
+
border-radius: var(--border-radius);
|
649
|
+
padding: 1rem;
|
650
|
+
margin: 1rem 0;
|
651
|
+
text-align: center;
|
652
|
+
color: #d32f2f;
|
653
|
+
font-style: italic;
|
654
|
+
font-weight: 500;
|
655
|
+
}
|
656
|
+
|
657
|
+
.content pre {
|
658
|
+
background: #f8fafc;
|
659
|
+
border: 1px solid var(--border-color);
|
660
|
+
border-radius: var(--border-radius);
|
661
|
+
padding: 1.5rem;
|
662
|
+
overflow-x: auto;
|
663
|
+
margin: 1.5rem 0;
|
664
|
+
box-shadow: var(--shadow);
|
665
|
+
position: relative;
|
666
|
+
font-family: var(--font-mono);
|
667
|
+
}
|
668
|
+
|
669
|
+
.content pre::before {
|
670
|
+
content: '';
|
671
|
+
position: absolute;
|
672
|
+
top: 0;
|
673
|
+
left: 0;
|
674
|
+
right: 0;
|
675
|
+
height: 3px;
|
676
|
+
background: var(--accent-color);
|
677
|
+
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
678
|
+
}
|
679
|
+
|
680
|
+
.content code {
|
681
|
+
background: #f1f5f9;
|
682
|
+
padding: 0.25rem 0.5rem;
|
683
|
+
border-radius: 4px;
|
684
|
+
font-family: var(--font-mono);
|
685
|
+
font-size: 0.875em;
|
686
|
+
font-weight: 500;
|
687
|
+
color: var(--primary-color);
|
688
|
+
border: 1px solid var(--border-light);
|
689
|
+
}
|
690
|
+
|
691
|
+
.content pre code {
|
692
|
+
background: none;
|
693
|
+
padding: 0;
|
694
|
+
border: none;
|
695
|
+
color: var(--text-color);
|
696
|
+
}
|
697
|
+
|
698
|
+
.content blockquote {
|
699
|
+
border-left: 4px solid var(--accent-color);
|
700
|
+
margin: 1.5rem 0;
|
701
|
+
padding: 1rem 1.5rem;
|
702
|
+
color: var(--text-light);
|
703
|
+
font-style: italic;
|
704
|
+
background: #f8fafc;
|
705
|
+
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
706
|
+
position: relative;
|
707
|
+
}
|
708
|
+
|
709
|
+
.content blockquote::before {
|
710
|
+
content: '"';
|
711
|
+
font-size: 2rem;
|
712
|
+
color: var(--accent-color);
|
713
|
+
position: absolute;
|
714
|
+
top: -0.25rem;
|
715
|
+
left: 0.75rem;
|
716
|
+
opacity: 0.5;
|
717
|
+
font-family: serif;
|
718
|
+
}
|
719
|
+
|
720
|
+
.content ul, .content ol {
|
721
|
+
margin: 1rem 0;
|
722
|
+
padding-left: 1.5rem;
|
723
|
+
}
|
724
|
+
|
725
|
+
.content li {
|
726
|
+
margin-bottom: 0.5rem;
|
727
|
+
font-size: 1rem;
|
728
|
+
line-height: 1.6;
|
729
|
+
}
|
730
|
+
|
731
|
+
.content ul li {
|
732
|
+
position: relative;
|
733
|
+
list-style: none;
|
734
|
+
}
|
735
|
+
|
736
|
+
.content ul li::before {
|
737
|
+
content: '•';
|
738
|
+
color: var(--accent-color);
|
739
|
+
font-weight: bold;
|
740
|
+
position: absolute;
|
741
|
+
left: -1rem;
|
742
|
+
}
|
743
|
+
|
744
|
+
.footer {
|
745
|
+
background: #f8fafc;
|
746
|
+
border-top: 1px solid var(--border-color);
|
747
|
+
padding: 2rem 3rem;
|
748
|
+
color: var(--text-light);
|
749
|
+
font-size: 0.9rem;
|
750
|
+
}
|
751
|
+
|
752
|
+
.footer-content {
|
753
|
+
display: flex;
|
754
|
+
justify-content: space-between;
|
755
|
+
align-items: center;
|
756
|
+
max-width: 1200px;
|
757
|
+
margin: 0 auto;
|
758
|
+
}
|
759
|
+
|
760
|
+
.footer a {
|
761
|
+
color: var(--primary-color);
|
762
|
+
text-decoration: none;
|
763
|
+
font-weight: 500;
|
764
|
+
transition: var(--transition);
|
765
|
+
}
|
766
|
+
|
767
|
+
.footer a:hover {
|
768
|
+
color: var(--accent-color);
|
769
|
+
text-decoration: underline;
|
770
|
+
}
|
771
|
+
|
772
|
+
.footer-info {
|
773
|
+
display: flex;
|
774
|
+
gap: 2rem;
|
775
|
+
align-items: center;
|
776
|
+
}
|
777
|
+
|
778
|
+
.footer-brand {
|
779
|
+
font-weight: 600;
|
780
|
+
color: var(--text-color);
|
781
|
+
}
|
782
|
+
|
783
|
+
/* Professional table styling */
|
784
|
+
.markdown-table, table {
|
785
|
+
width: 100%;
|
786
|
+
border-collapse: collapse;
|
787
|
+
margin: 1.5rem 0;
|
788
|
+
background: var(--card-bg);
|
789
|
+
border-radius: var(--border-radius);
|
790
|
+
overflow: hidden;
|
791
|
+
box-shadow: var(--shadow-md);
|
792
|
+
border: 1px solid var(--border-color);
|
793
|
+
}
|
794
|
+
|
795
|
+
.markdown-table th, .markdown-table td,
|
796
|
+
table th, table td {
|
797
|
+
padding: 0.75rem 1rem;
|
798
|
+
text-align: left;
|
799
|
+
border-bottom: 1px solid var(--border-light);
|
800
|
+
}
|
801
|
+
|
802
|
+
.markdown-table th, table th {
|
803
|
+
background: #f8fafc;
|
804
|
+
color: var(--text-color);
|
805
|
+
font-weight: 600;
|
806
|
+
font-size: 0.875rem;
|
807
|
+
text-transform: uppercase;
|
808
|
+
letter-spacing: 0.05em;
|
809
|
+
border-bottom: 2px solid var(--accent-color);
|
810
|
+
}
|
811
|
+
|
812
|
+
.markdown-table tr:nth-child(even), table tr:nth-child(even) {
|
813
|
+
background: #f8fafc;
|
814
|
+
}
|
815
|
+
|
816
|
+
.markdown-table tr:hover, table tr:hover {
|
817
|
+
background: #f1f5f9;
|
818
|
+
transition: var(--transition);
|
819
|
+
}
|
820
|
+
|
821
|
+
.markdown-table tr:last-child td, table tr:last-child td {
|
822
|
+
border-bottom: none;
|
823
|
+
}
|
824
|
+
|
825
|
+
/* Ensure all tables are wrapped in containers */
|
826
|
+
.table-container {
|
827
|
+
margin: 1.5rem 0;
|
828
|
+
overflow-x: auto;
|
829
|
+
border-radius: var(--border-radius);
|
830
|
+
box-shadow: var(--shadow-md);
|
831
|
+
}
|
832
|
+
|
833
|
+
.table-container table {
|
834
|
+
margin: 0;
|
835
|
+
box-shadow: none;
|
836
|
+
}
|
837
|
+
|
838
|
+
/* Professional dark mode toggle */
|
839
|
+
.theme-toggle {
|
840
|
+
position: fixed;
|
841
|
+
top: 1.5rem;
|
842
|
+
right: 1.5rem;
|
843
|
+
background: var(--card-bg);
|
844
|
+
border: 1px solid var(--border-color);
|
845
|
+
border-radius: var(--border-radius);
|
846
|
+
width: 44px;
|
847
|
+
height: 44px;
|
848
|
+
display: flex;
|
849
|
+
align-items: center;
|
850
|
+
justify-content: center;
|
851
|
+
cursor: pointer;
|
852
|
+
box-shadow: var(--shadow-md);
|
853
|
+
transition: var(--transition);
|
854
|
+
z-index: 1000;
|
855
|
+
}
|
856
|
+
|
857
|
+
.theme-toggle:hover {
|
858
|
+
background: var(--accent-color);
|
859
|
+
color: white;
|
860
|
+
box-shadow: var(--shadow-lg);
|
861
|
+
}
|
862
|
+
|
863
|
+
.theme-toggle::before {
|
864
|
+
content: '🌙';
|
865
|
+
font-size: 1rem;
|
866
|
+
}
|
867
|
+
|
868
|
+
/* Dark mode styles */
|
869
|
+
[data-theme="dark"] {
|
870
|
+
--text-color: #f9fafb;
|
871
|
+
--text-light: #d1d5db;
|
872
|
+
--text-muted: #9ca3af;
|
873
|
+
--bg-color: #111827;
|
874
|
+
--card-bg: #1f2937;
|
875
|
+
--border-color: #374151;
|
876
|
+
--border-light: #4b5563;
|
877
|
+
}
|
878
|
+
|
879
|
+
[data-theme="dark"] body {
|
880
|
+
background: var(--bg-color);
|
881
|
+
}
|
882
|
+
|
883
|
+
[data-theme="dark"] .theme-toggle::before {
|
884
|
+
content: '☀️';
|
885
|
+
}
|
886
|
+
|
887
|
+
/* Professional scrollbar */
|
888
|
+
::-webkit-scrollbar {
|
889
|
+
width: 8px;
|
890
|
+
}
|
891
|
+
|
892
|
+
::-webkit-scrollbar-track {
|
893
|
+
background: var(--border-light);
|
894
|
+
}
|
895
|
+
|
896
|
+
::-webkit-scrollbar-thumb {
|
897
|
+
background: var(--accent-color);
|
898
|
+
border-radius: 4px;
|
899
|
+
}
|
900
|
+
|
901
|
+
::-webkit-scrollbar-thumb:hover {
|
902
|
+
background: var(--primary-color);
|
903
|
+
}
|
904
|
+
|
905
|
+
/* Professional selection styling */
|
906
|
+
::selection {
|
907
|
+
background: var(--accent-color);
|
908
|
+
color: white;
|
909
|
+
}
|
910
|
+
|
911
|
+
::-moz-selection {
|
912
|
+
background: var(--accent-color);
|
913
|
+
color: white;
|
914
|
+
}
|
915
|
+
|
916
|
+
/* Enhanced focus states for accessibility */
|
917
|
+
.theme-toggle:focus {
|
918
|
+
outline: 2px solid var(--accent-color);
|
919
|
+
outline-offset: 2px;
|
920
|
+
}
|
921
|
+
|
922
|
+
.footer a:focus {
|
923
|
+
outline: 2px solid var(--accent-color);
|
924
|
+
outline-offset: 2px;
|
925
|
+
border-radius: 4px;
|
926
|
+
}
|
927
|
+
|
928
|
+
/* Print styles */
|
929
|
+
@media print {
|
930
|
+
body {
|
931
|
+
background: white !important;
|
932
|
+
color: black !important;
|
933
|
+
}
|
934
|
+
|
935
|
+
.header {
|
936
|
+
background: #f8f9fa !important;
|
937
|
+
color: black !important;
|
938
|
+
box-shadow: none !important;
|
939
|
+
}
|
940
|
+
|
941
|
+
.content {
|
942
|
+
box-shadow: none !important;
|
943
|
+
}
|
944
|
+
|
945
|
+
.theme-toggle {
|
946
|
+
display: none !important;
|
947
|
+
}
|
948
|
+
|
949
|
+
.footer {
|
950
|
+
background: #f8f9fa !important;
|
951
|
+
border-top: 1px solid #ddd !important;
|
952
|
+
}
|
953
|
+
}
|
954
|
+
|
955
|
+
@media (max-width: 768px) {
|
956
|
+
.header {
|
957
|
+
padding: 1.5rem 2rem;
|
958
|
+
}
|
959
|
+
|
960
|
+
.header-content {
|
961
|
+
flex-direction: column;
|
962
|
+
align-items: flex-start;
|
963
|
+
gap: 1rem;
|
964
|
+
}
|
965
|
+
|
966
|
+
.header h1 {
|
967
|
+
font-size: 2rem;
|
968
|
+
}
|
969
|
+
|
970
|
+
.content {
|
971
|
+
padding: 2rem;
|
972
|
+
}
|
973
|
+
|
974
|
+
.markdown-table {
|
975
|
+
font-size: 0.875rem;
|
976
|
+
}
|
977
|
+
|
978
|
+
.markdown-table th,
|
979
|
+
.markdown-table td {
|
980
|
+
padding: 0.5rem;
|
981
|
+
}
|
982
|
+
|
983
|
+
.theme-toggle {
|
984
|
+
top: 1rem;
|
985
|
+
right: 1rem;
|
986
|
+
width: 40px;
|
987
|
+
height: 40px;
|
988
|
+
}
|
989
|
+
|
990
|
+
.footer {
|
991
|
+
padding: 1.5rem 2rem;
|
992
|
+
}
|
993
|
+
|
994
|
+
.footer-content {
|
995
|
+
flex-direction: column;
|
996
|
+
gap: 1rem;
|
997
|
+
text-align: center;
|
998
|
+
}
|
999
|
+
}
|
1000
|
+
|
1001
|
+
@media (max-width: 480px) {
|
1002
|
+
.header {
|
1003
|
+
padding: 1rem;
|
1004
|
+
}
|
1005
|
+
|
1006
|
+
.header h1 {
|
1007
|
+
font-size: 1.75rem;
|
1008
|
+
}
|
1009
|
+
|
1010
|
+
.content {
|
1011
|
+
padding: 1.5rem;
|
1012
|
+
}
|
1013
|
+
|
1014
|
+
.markdown-table {
|
1015
|
+
font-size: 0.8rem;
|
1016
|
+
}
|
1017
|
+
|
1018
|
+
.markdown-table th,
|
1019
|
+
.markdown-table td {
|
1020
|
+
padding: 0.375rem;
|
1021
|
+
}
|
1022
|
+
}
|
1023
|
+
"""
|
1024
|
+
|
1025
|
+
|
1026
|
+
def _get_table_css_styles() -> str:
|
1027
|
+
"""Get additional CSS styles for tables."""
|
1028
|
+
return """
|
1029
|
+
.data-section {
|
1030
|
+
margin-bottom: 4rem;
|
1031
|
+
position: relative;
|
1032
|
+
}
|
1033
|
+
|
1034
|
+
.data-section::before {
|
1035
|
+
content: '';
|
1036
|
+
position: absolute;
|
1037
|
+
top: -1rem;
|
1038
|
+
left: 0;
|
1039
|
+
right: 0;
|
1040
|
+
height: 2px;
|
1041
|
+
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color));
|
1042
|
+
border-radius: 1px;
|
1043
|
+
}
|
1044
|
+
|
1045
|
+
.section-title {
|
1046
|
+
color: var(--text-color);
|
1047
|
+
font-size: 1.8rem;
|
1048
|
+
margin-bottom: 2rem;
|
1049
|
+
padding-bottom: 1rem;
|
1050
|
+
border-bottom: 3px solid var(--primary-color);
|
1051
|
+
position: relative;
|
1052
|
+
font-weight: 700;
|
1053
|
+
}
|
1054
|
+
|
1055
|
+
.section-title::after {
|
1056
|
+
content: '';
|
1057
|
+
position: absolute;
|
1058
|
+
bottom: -3px;
|
1059
|
+
left: 0;
|
1060
|
+
width: 50px;
|
1061
|
+
height: 3px;
|
1062
|
+
background: var(--accent-color);
|
1063
|
+
}
|
1064
|
+
|
1065
|
+
.table-container {
|
1066
|
+
overflow-x: auto;
|
1067
|
+
margin: 2rem 0;
|
1068
|
+
border-radius: var(--border-radius);
|
1069
|
+
box-shadow: var(--shadow-lg);
|
1070
|
+
position: relative;
|
1071
|
+
background: var(--card-bg);
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
.table-container::before {
|
1075
|
+
content: '';
|
1076
|
+
position: absolute;
|
1077
|
+
top: 0;
|
1078
|
+
left: 0;
|
1079
|
+
right: 0;
|
1080
|
+
height: 4px;
|
1081
|
+
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color));
|
1082
|
+
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
1083
|
+
}
|
1084
|
+
|
1085
|
+
.data-table {
|
1086
|
+
width: 100%;
|
1087
|
+
border-collapse: collapse;
|
1088
|
+
background: var(--card-bg);
|
1089
|
+
font-size: 1rem;
|
1090
|
+
position: relative;
|
1091
|
+
}
|
1092
|
+
|
1093
|
+
.data-table th {
|
1094
|
+
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
1095
|
+
color: white;
|
1096
|
+
padding: 1.5rem 1.5rem;
|
1097
|
+
text-align: left;
|
1098
|
+
font-weight: 700;
|
1099
|
+
font-size: 0.95rem;
|
1100
|
+
text-transform: uppercase;
|
1101
|
+
letter-spacing: 0.8px;
|
1102
|
+
position: relative;
|
1103
|
+
}
|
1104
|
+
|
1105
|
+
.data-table th::after {
|
1106
|
+
content: '';
|
1107
|
+
position: absolute;
|
1108
|
+
bottom: 0;
|
1109
|
+
left: 0;
|
1110
|
+
right: 0;
|
1111
|
+
height: 2px;
|
1112
|
+
background: linear-gradient(90deg, transparent, white, transparent);
|
1113
|
+
}
|
1114
|
+
|
1115
|
+
.data-table th:first-child {
|
1116
|
+
border-top-left-radius: var(--border-radius);
|
1117
|
+
}
|
1118
|
+
|
1119
|
+
.data-table th:last-child {
|
1120
|
+
border-top-right-radius: var(--border-radius);
|
1121
|
+
}
|
1122
|
+
|
1123
|
+
.data-table td {
|
1124
|
+
padding: 1.25rem 1.5rem;
|
1125
|
+
border-bottom: 1px solid var(--border-color);
|
1126
|
+
vertical-align: top;
|
1127
|
+
font-size: 1rem;
|
1128
|
+
line-height: 1.6;
|
1129
|
+
}
|
1130
|
+
|
1131
|
+
.data-table tr:nth-child(even) {
|
1132
|
+
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
1133
|
+
}
|
1134
|
+
|
1135
|
+
.data-table tr:hover {
|
1136
|
+
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
|
1137
|
+
transform: scale(1.005);
|
1138
|
+
transition: var(--transition);
|
1139
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
1140
|
+
}
|
1141
|
+
|
1142
|
+
.data-table tr:last-child td:first-child {
|
1143
|
+
border-bottom-left-radius: var(--border-radius);
|
1144
|
+
}
|
1145
|
+
|
1146
|
+
.data-table tr:last-child td:last-child {
|
1147
|
+
border-bottom-right-radius: var(--border-radius);
|
1148
|
+
}
|
1149
|
+
|
1150
|
+
.data-table tr:last-child td {
|
1151
|
+
border-bottom: none;
|
1152
|
+
}
|
1153
|
+
|
1154
|
+
.no-data {
|
1155
|
+
text-align: center;
|
1156
|
+
color: var(--text-light);
|
1157
|
+
font-style: italic;
|
1158
|
+
padding: 3rem;
|
1159
|
+
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
1160
|
+
border-radius: var(--border-radius);
|
1161
|
+
font-size: 1.1rem;
|
1162
|
+
position: relative;
|
1163
|
+
}
|
1164
|
+
|
1165
|
+
.no-data::before {
|
1166
|
+
content: '📊';
|
1167
|
+
font-size: 2rem;
|
1168
|
+
display: block;
|
1169
|
+
margin-bottom: 1rem;
|
1170
|
+
opacity: 0.5;
|
1171
|
+
}
|
1172
|
+
|
1173
|
+
/* Enhanced table animations */
|
1174
|
+
.data-table tr {
|
1175
|
+
transition: var(--transition);
|
1176
|
+
}
|
1177
|
+
|
1178
|
+
.data-table td {
|
1179
|
+
transition: var(--transition);
|
1180
|
+
}
|
1181
|
+
|
1182
|
+
.data-table tr:hover td {
|
1183
|
+
color: var(--text-color);
|
1184
|
+
font-weight: 500;
|
1185
|
+
}
|
1186
|
+
|
1187
|
+
/* Table scrollbar styling */
|
1188
|
+
.table-container::-webkit-scrollbar {
|
1189
|
+
height: 8px;
|
1190
|
+
}
|
1191
|
+
|
1192
|
+
.table-container::-webkit-scrollbar-track {
|
1193
|
+
background: var(--border-color);
|
1194
|
+
border-radius: 4px;
|
1195
|
+
}
|
1196
|
+
|
1197
|
+
.table-container::-webkit-scrollbar-thumb {
|
1198
|
+
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
1199
|
+
border-radius: 4px;
|
1200
|
+
}
|
1201
|
+
|
1202
|
+
.table-container::-webkit-scrollbar-thumb:hover {
|
1203
|
+
background: linear-gradient(90deg, var(--secondary-color), var(--accent-color));
|
1204
|
+
}
|
1205
|
+
|
1206
|
+
@media (max-width: 768px) {
|
1207
|
+
.data-table {
|
1208
|
+
font-size: 0.9rem;
|
1209
|
+
}
|
1210
|
+
|
1211
|
+
.data-table th,
|
1212
|
+
.data-table td {
|
1213
|
+
padding: 1rem;
|
1214
|
+
}
|
1215
|
+
|
1216
|
+
.section-title {
|
1217
|
+
font-size: 1.5rem;
|
1218
|
+
}
|
1219
|
+
}
|
1220
|
+
|
1221
|
+
@media (max-width: 480px) {
|
1222
|
+
.data-table {
|
1223
|
+
font-size: 0.8rem;
|
1224
|
+
}
|
1225
|
+
|
1226
|
+
.data-table th,
|
1227
|
+
.data-table td {
|
1228
|
+
padding: 0.75rem;
|
1229
|
+
}
|
1230
|
+
|
1231
|
+
.section-title {
|
1232
|
+
font-size: 1.3rem;
|
1233
|
+
}
|
1234
|
+
}
|
1235
|
+
"""
|