pg_reports 0.4.0 → 0.5.1

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +130 -0
  3. data/README.md +170 -4
  4. data/app/controllers/pg_reports/dashboard_controller.rb +348 -47
  5. data/app/views/layouts/pg_reports/application.html.erb +370 -79
  6. data/app/views/pg_reports/dashboard/_show_modals.html.erb +1 -1
  7. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +254 -29
  8. data/app/views/pg_reports/dashboard/_show_styles.html.erb +373 -0
  9. data/app/views/pg_reports/dashboard/index.html.erb +485 -5
  10. data/config/locales/en.yml +45 -0
  11. data/config/locales/ru.yml +45 -0
  12. data/config/routes.rb +8 -0
  13. data/lib/pg_reports/configuration.rb +21 -0
  14. data/lib/pg_reports/dashboard/reports_registry.rb +24 -1
  15. data/lib/pg_reports/definitions/connections/connection_churn.yml +49 -0
  16. data/lib/pg_reports/definitions/connections/pool_saturation.yml +42 -0
  17. data/lib/pg_reports/definitions/connections/pool_usage.yml +43 -0
  18. data/lib/pg_reports/definitions/connections/pool_wait_times.yml +44 -0
  19. data/lib/pg_reports/definitions/queries/missing_index_queries.yml +3 -3
  20. data/lib/pg_reports/explain_analyzer.rb +338 -0
  21. data/lib/pg_reports/modules/schema_analysis.rb +4 -6
  22. data/lib/pg_reports/modules/system.rb +26 -3
  23. data/lib/pg_reports/query_monitor.rb +293 -0
  24. data/lib/pg_reports/sql/connections/connection_churn.sql +37 -0
  25. data/lib/pg_reports/sql/connections/pool_saturation.sql +90 -0
  26. data/lib/pg_reports/sql/connections/pool_usage.sql +31 -0
  27. data/lib/pg_reports/sql/connections/pool_wait_times.sql +19 -0
  28. data/lib/pg_reports/sql/queries/all_queries.sql +17 -15
  29. data/lib/pg_reports/sql/queries/expensive_queries.sql +9 -4
  30. data/lib/pg_reports/sql/queries/heavy_queries.sql +14 -12
  31. data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +16 -14
  32. data/lib/pg_reports/sql/queries/missing_index_queries.sql +18 -16
  33. data/lib/pg_reports/sql/queries/slow_queries.sql +14 -12
  34. data/lib/pg_reports/sql/system/databases_list.sql +8 -0
  35. data/lib/pg_reports/version.rb +1 -1
  36. data/lib/pg_reports.rb +2 -0
  37. metadata +55 -2
@@ -48,7 +48,7 @@
48
48
  .container {
49
49
  max-width: 1400px;
50
50
  margin: 0 auto;
51
- padding: 2rem;
51
+ padding: 1.5rem;
52
52
  }
53
53
 
54
54
  /* Header */
@@ -56,30 +56,30 @@
56
56
  display: flex;
57
57
  align-items: center;
58
58
  justify-content: space-between;
59
- margin-bottom: 2.5rem;
60
- padding-bottom: 1.5rem;
59
+ margin-bottom: 1.5rem;
60
+ padding-bottom: 1rem;
61
61
  border-bottom: 1px solid var(--border-color);
62
62
  }
63
63
 
64
64
  .logo {
65
65
  display: flex;
66
66
  align-items: center;
67
- gap: 1rem;
67
+ gap: 0.75rem;
68
68
  }
69
69
 
70
70
  .logo-icon {
71
- width: 48px;
72
- height: 48px;
71
+ width: 44px;
72
+ height: 44px;
73
73
  background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
74
- border-radius: 12px;
74
+ border-radius: 6px;
75
75
  display: flex;
76
76
  align-items: center;
77
77
  justify-content: center;
78
- font-size: 1.5rem;
78
+ font-size: 1.4rem;
79
79
  }
80
80
 
81
81
  .logo-text h1 {
82
- font-size: 1.5rem;
82
+ font-size: 1.25rem;
83
83
  font-weight: 700;
84
84
  background: linear-gradient(135deg, var(--text-primary), var(--text-secondary));
85
85
  -webkit-background-clip: text;
@@ -87,19 +87,19 @@
87
87
  }
88
88
 
89
89
  .logo-text span {
90
- font-size: 0.875rem;
90
+ font-size: 0.75rem;
91
91
  color: var(--text-muted);
92
92
  }
93
93
 
94
94
  .header-badge {
95
95
  display: flex;
96
96
  align-items: center;
97
- gap: 0.5rem;
98
- padding: 0.5rem 1rem;
97
+ gap: 0.4rem;
98
+ padding: 0.4rem 0.75rem;
99
99
  background: var(--bg-tertiary);
100
100
  border: 1px solid var(--border-color);
101
- border-radius: 9999px;
102
- font-size: 0.875rem;
101
+ border-radius: 4px;
102
+ font-size: 0.75rem;
103
103
  }
104
104
 
105
105
  .badge-dot {
@@ -122,72 +122,72 @@
122
122
  /* Categories Grid */
123
123
  .categories-grid {
124
124
  display: grid;
125
- grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
126
- gap: 1.5rem;
127
- margin-bottom: 2rem;
125
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
126
+ gap: 1rem;
127
+ margin-bottom: 1.5rem;
128
128
  }
129
129
 
130
130
  .category-card {
131
131
  background: var(--bg-card);
132
132
  border: 1px solid var(--border-color);
133
- border-radius: 16px;
134
- padding: 1.5rem;
133
+ border-radius: 8px;
134
+ padding: 1rem;
135
135
  transition: all 0.2s ease;
136
136
  }
137
137
 
138
138
  .category-card:hover {
139
139
  border-color: var(--accent-purple);
140
- transform: translateY(-2px);
140
+ transform: translateY(-1px);
141
141
  }
142
142
 
143
143
  .category-header {
144
144
  display: flex;
145
145
  align-items: center;
146
- gap: 1rem;
147
- margin-bottom: 1.25rem;
146
+ gap: 0.75rem;
147
+ margin-bottom: 0.875rem;
148
148
  }
149
149
 
150
150
  .category-icon {
151
- width: 44px;
152
- height: 44px;
153
- border-radius: 12px;
151
+ width: 36px;
152
+ height: 36px;
153
+ border-radius: 6px;
154
154
  display: flex;
155
155
  align-items: center;
156
156
  justify-content: center;
157
- font-size: 1.25rem;
157
+ font-size: 1.125rem;
158
158
  }
159
159
 
160
160
  .category-title {
161
- font-size: 1.125rem;
161
+ font-size: 1rem;
162
162
  font-weight: 600;
163
163
  }
164
164
 
165
165
  .category-count {
166
166
  margin-left: auto;
167
- padding: 0.25rem 0.75rem;
167
+ padding: 0.2rem 0.6rem;
168
168
  background: var(--bg-tertiary);
169
- border-radius: 9999px;
170
- font-size: 0.75rem;
169
+ border-radius: 4px;
170
+ font-size: 0.7rem;
171
171
  color: var(--text-muted);
172
172
  }
173
173
 
174
174
  .reports-list {
175
175
  display: flex;
176
176
  flex-direction: column;
177
- gap: 0.5rem;
177
+ gap: 0.4rem;
178
178
  }
179
179
 
180
180
  .report-link {
181
181
  display: flex;
182
182
  align-items: center;
183
183
  justify-content: space-between;
184
- padding: 0.75rem 1rem;
184
+ padding: 0.6rem 0.875rem;
185
185
  background: var(--bg-tertiary);
186
186
  border: 1px solid transparent;
187
- border-radius: 10px;
187
+ border-radius: 6px;
188
188
  color: var(--text-secondary);
189
189
  text-decoration: none;
190
- font-size: 0.9rem;
190
+ font-size: 0.85rem;
191
191
  transition: all 0.15s ease;
192
192
  }
193
193
 
@@ -212,14 +212,14 @@
212
212
  .report-page {
213
213
  display: flex;
214
214
  flex-direction: column;
215
- gap: 1.5rem;
215
+ gap: 1rem;
216
216
  }
217
217
 
218
218
  .breadcrumb {
219
219
  display: flex;
220
220
  align-items: center;
221
- gap: 0.5rem;
222
- font-size: 0.875rem;
221
+ gap: 0.4rem;
222
+ font-size: 0.75rem;
223
223
  color: var(--text-muted);
224
224
  }
225
225
 
@@ -237,35 +237,36 @@
237
237
  display: flex;
238
238
  align-items: flex-start;
239
239
  justify-content: space-between;
240
- gap: 2rem;
240
+ gap: 1.5rem;
241
241
  }
242
242
 
243
243
  .report-info h2 {
244
- font-size: 1.75rem;
244
+ font-size: 1.4rem;
245
245
  font-weight: 700;
246
- margin-bottom: 0.5rem;
246
+ margin-bottom: 0.4rem;
247
247
  }
248
248
 
249
249
  .report-info p {
250
250
  color: var(--text-muted);
251
+ font-size: 0.875rem;
251
252
  }
252
253
 
253
254
  .report-actions {
254
255
  display: flex;
255
- gap: 0.75rem;
256
+ gap: 0.5rem;
256
257
  }
257
258
 
258
259
  .btn {
259
260
  display: inline-flex;
260
261
  align-items: center;
261
262
  justify-content: center;
262
- gap: 0.5rem;
263
- height: 40px;
264
- padding: 0 1.25rem;
263
+ gap: 0.4rem;
264
+ height: 36px;
265
+ padding: 0 1rem;
265
266
  border: 1px solid transparent;
266
- border-radius: 10px;
267
+ border-radius: 6px;
267
268
  font-family: inherit;
268
- font-size: 0.9rem;
269
+ font-size: 0.8rem;
269
270
  font-weight: 500;
270
271
  cursor: pointer;
271
272
  transition: all 0.15s ease;
@@ -315,7 +316,7 @@
315
316
  .results-container {
316
317
  background: var(--bg-card);
317
318
  border: 1px solid var(--border-color);
318
- border-radius: 16px;
319
+ border-radius: 8px;
319
320
  overflow: hidden;
320
321
  }
321
322
 
@@ -323,20 +324,21 @@
323
324
  display: flex;
324
325
  align-items: center;
325
326
  justify-content: space-between;
326
- padding: 1.25rem 1.5rem;
327
+ padding: 0.875rem 1rem;
327
328
  border-bottom: 1px solid var(--border-color);
328
329
  }
329
330
 
330
331
  .results-title {
331
332
  font-weight: 600;
333
+ font-size: 0.9rem;
332
334
  }
333
335
 
334
336
  .results-meta {
335
337
  display: flex;
336
338
  align-items: center;
337
- gap: 1.5rem;
339
+ gap: 1rem;
338
340
  color: var(--text-muted);
339
- font-size: 0.875rem;
341
+ font-size: 0.75rem;
340
342
  }
341
343
 
342
344
  .results-table-wrapper {
@@ -347,24 +349,24 @@
347
349
  width: 100%;
348
350
  border-collapse: collapse;
349
351
  font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
350
- font-size: 0.8rem;
352
+ font-size: 0.75rem;
351
353
  }
352
354
 
353
355
  .results-table th {
354
- padding: 1rem 1.25rem;
356
+ padding: 0.7rem 0.875rem;
355
357
  text-align: left;
356
358
  background: var(--bg-tertiary);
357
359
  color: var(--text-muted);
358
360
  font-weight: 500;
359
361
  text-transform: uppercase;
360
- font-size: 0.7rem;
362
+ font-size: 0.65rem;
361
363
  letter-spacing: 0.05em;
362
364
  white-space: nowrap;
363
365
  border-bottom: 1px solid var(--border-color);
364
366
  }
365
367
 
366
368
  .results-table td {
367
- padding: 0.875rem 1.25rem;
369
+ padding: 0.65rem 0.875rem;
368
370
  border-bottom: 1px solid var(--border-color);
369
371
  color: var(--text-secondary);
370
372
  max-width: 400px;
@@ -384,26 +386,27 @@
384
386
  }
385
387
 
386
388
  .empty-state {
387
- padding: 4rem 2rem;
389
+ padding: 3rem 1.5rem;
388
390
  text-align: center;
389
391
  color: var(--text-muted);
392
+ font-size: 0.85rem;
390
393
  }
391
394
 
392
395
  .empty-state-icon {
393
- font-size: 3rem;
394
- margin-bottom: 1rem;
396
+ font-size: 2.5rem;
397
+ margin-bottom: 0.75rem;
395
398
  }
396
399
 
397
400
  .loading {
398
401
  display: flex;
399
402
  align-items: center;
400
403
  justify-content: center;
401
- padding: 4rem 2rem;
404
+ padding: 3rem 1.5rem;
402
405
  }
403
406
 
404
407
  .spinner {
405
- width: 40px;
406
- height: 40px;
408
+ width: 36px;
409
+ height: 36px;
407
410
  border: 3px solid var(--border-color);
408
411
  border-top-color: var(--accent-purple);
409
412
  border-radius: 50%;
@@ -416,24 +419,26 @@
416
419
 
417
420
  /* Error state */
418
421
  .error-message {
419
- padding: 1.5rem;
422
+ padding: 0.875rem;
420
423
  background: rgba(244, 63, 94, 0.1);
421
424
  border: 1px solid rgba(244, 63, 94, 0.3);
422
- border-radius: 12px;
425
+ border-radius: 6px;
423
426
  color: var(--accent-rose);
427
+ font-size: 0.85rem;
424
428
  }
425
429
 
426
430
  /* Toast notification */
427
431
  .toast {
428
432
  position: fixed;
429
- bottom: 2rem;
430
- right: 2rem;
431
- padding: 1rem 1.5rem;
433
+ bottom: 1.5rem;
434
+ right: 1.5rem;
435
+ padding: 0.75rem 1rem;
432
436
  background: var(--bg-card);
433
437
  border: 1px solid var(--border-color);
434
- border-radius: 12px;
438
+ border-radius: 6px;
435
439
  color: var(--text-primary);
436
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
440
+ font-size: 0.85rem;
441
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
437
442
  transform: translateY(100px);
438
443
  opacity: 0;
439
444
  transition: all 0.3s ease;
@@ -468,10 +473,10 @@
468
473
  .modal-content {
469
474
  background: var(--bg-card);
470
475
  border: 1px solid var(--border-color);
471
- border-radius: 16px;
472
- max-width: 600px;
476
+ border-radius: 8px;
477
+ max-width: 900px;
473
478
  width: 90%;
474
- max-height: 80vh;
479
+ max-height: 90vh;
475
480
  overflow: auto;
476
481
  }
477
482
 
@@ -484,23 +489,23 @@
484
489
  display: flex;
485
490
  align-items: center;
486
491
  justify-content: space-between;
487
- padding: 1.25rem 1.5rem;
492
+ padding: 0.875rem 1rem;
488
493
  border-bottom: 1px solid var(--border-color);
489
494
  }
490
495
 
491
496
  .modal-header h3 {
492
- font-size: 1.125rem;
497
+ font-size: 1rem;
493
498
  font-weight: 600;
494
499
  }
495
500
 
496
501
  .modal-close {
497
- width: 32px;
498
- height: 32px;
502
+ width: 28px;
503
+ height: 28px;
499
504
  background: var(--bg-tertiary);
500
505
  border: 1px solid var(--border-color);
501
- border-radius: 8px;
506
+ border-radius: 4px;
502
507
  color: var(--text-muted);
503
- font-size: 1.25rem;
508
+ font-size: 1.125rem;
504
509
  cursor: pointer;
505
510
  transition: all 0.15s;
506
511
  }
@@ -512,18 +517,18 @@
512
517
  }
513
518
 
514
519
  .modal-body {
515
- padding: 1.5rem;
520
+ padding: 1rem;
516
521
  }
517
522
 
518
523
  /* Responsive */
519
524
  @media (max-width: 768px) {
520
525
  .container {
521
- padding: 1rem;
526
+ padding: 0.875rem;
522
527
  }
523
528
 
524
529
  .header {
525
530
  flex-direction: column;
526
- gap: 1rem;
531
+ gap: 0.75rem;
527
532
  align-items: flex-start;
528
533
  }
529
534
 
@@ -540,6 +545,292 @@
540
545
  justify-content: center;
541
546
  }
542
547
  }
548
+
549
+ /* ============================================
550
+ Query Monitoring Panel
551
+ ============================================ */
552
+ .query-monitoring-panel {
553
+ margin-bottom: 1.5rem;
554
+ background: var(--bg-card);
555
+ border: 1px solid var(--border-color);
556
+ border-radius: 8px;
557
+ padding: 0.875rem 1rem;
558
+ }
559
+
560
+ .query-monitoring-header {
561
+ display: flex;
562
+ align-items: center;
563
+ justify-content: space-between;
564
+ margin-bottom: 0.875rem;
565
+ padding-bottom: 0.75rem;
566
+ border-bottom: 1px solid var(--border-color);
567
+ }
568
+
569
+ .query-monitoring-title {
570
+ display: flex;
571
+ align-items: center;
572
+ gap: 0.6rem;
573
+ font-weight: 600;
574
+ font-size: 0.9rem;
575
+ }
576
+
577
+ .monitoring-indicator {
578
+ width: 7px;
579
+ height: 7px;
580
+ border-radius: 50%;
581
+ background: var(--text-muted);
582
+ }
583
+
584
+ .monitoring-indicator.active {
585
+ background: var(--accent-rose);
586
+ animation: pulse 2s infinite;
587
+ }
588
+
589
+ @keyframes pulse {
590
+ 0%, 100% {
591
+ opacity: 1;
592
+ }
593
+ 50% {
594
+ opacity: 0.5;
595
+ }
596
+ }
597
+
598
+ .session-badge {
599
+ margin-left: 0.4rem;
600
+ padding: 0.3rem 0.6rem;
601
+ background: var(--bg-tertiary);
602
+ border: 1px solid var(--border-color);
603
+ border-radius: 4px;
604
+ font-size: 0.65rem;
605
+ font-weight: 500;
606
+ color: var(--text-secondary);
607
+ }
608
+
609
+ .session-badge strong {
610
+ color: var(--accent-purple);
611
+ font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
612
+ }
613
+
614
+ .query-monitoring-controls {
615
+ display: flex;
616
+ align-items: center;
617
+ gap: 0.75rem;
618
+ }
619
+
620
+ .query-monitoring-count {
621
+ font-size: 0.75rem;
622
+ color: var(--text-secondary);
623
+ }
624
+
625
+ .query-monitoring-count strong {
626
+ color: var(--accent-blue);
627
+ font-weight: 600;
628
+ }
629
+
630
+ /* Query Feed */
631
+ .query-feed {
632
+ max-height: 500px;
633
+ overflow-y: auto;
634
+ background: var(--bg-tertiary);
635
+ border: 1px solid var(--border-color);
636
+ border-radius: 6px;
637
+ padding: 0.75rem;
638
+ }
639
+
640
+ .query-feed::-webkit-scrollbar {
641
+ width: 6px;
642
+ }
643
+
644
+ .query-feed::-webkit-scrollbar-track {
645
+ background: var(--bg-secondary);
646
+ border-radius: 3px;
647
+ }
648
+
649
+ .query-feed::-webkit-scrollbar-thumb {
650
+ background: var(--border-color);
651
+ border-radius: 3px;
652
+ }
653
+
654
+ .query-feed::-webkit-scrollbar-thumb:hover {
655
+ background: var(--text-muted);
656
+ }
657
+
658
+ .query-feed-empty {
659
+ text-align: center;
660
+ padding: 2rem;
661
+ color: var(--text-muted);
662
+ font-size: 0.8rem;
663
+ }
664
+
665
+ .query-item {
666
+ background: var(--bg-card);
667
+ border: 1px solid var(--border-color);
668
+ border-radius: 6px;
669
+ padding: 0.5rem;
670
+ margin-bottom: 0.5rem;
671
+ font-size: 0.8rem;
672
+ transition: all 0.15s;
673
+ }
674
+
675
+ .query-item:hover {
676
+ border-color: var(--accent-purple);
677
+ }
678
+
679
+ .query-item:last-child {
680
+ margin-bottom: 0;
681
+ }
682
+
683
+ .query-header {
684
+ display: flex;
685
+ justify-content: space-between;
686
+ align-items: center;
687
+ margin-bottom: 0.3rem;
688
+ }
689
+
690
+ .query-meta {
691
+ display: flex;
692
+ gap: 0.6rem;
693
+ font-size: 0.7rem;
694
+ color: var(--text-muted);
695
+ }
696
+
697
+ .query-timestamp {
698
+ color: var(--text-secondary);
699
+ font-size: 0.7rem;
700
+ }
701
+
702
+ .query-duration {
703
+ color: var(--accent-amber);
704
+ font-weight: 600;
705
+ font-size: 0.7rem;
706
+ }
707
+
708
+ .query-duration.fast {
709
+ color: var(--accent-green);
710
+ }
711
+
712
+ .query-duration.slow {
713
+ color: var(--accent-rose);
714
+ }
715
+
716
+ .query-name {
717
+ color: var(--text-secondary);
718
+ font-style: italic;
719
+ font-size: 0.7rem;
720
+ }
721
+
722
+ .query-expand-btn {
723
+ background: transparent;
724
+ border: none;
725
+ color: var(--text-muted);
726
+ cursor: pointer;
727
+ padding: 0.15rem 0.35rem;
728
+ font-size: 0.7rem;
729
+ transition: all 0.15s;
730
+ border-radius: 3px;
731
+ }
732
+
733
+ .query-expand-btn:hover {
734
+ background: var(--bg-tertiary);
735
+ color: var(--accent-purple);
736
+ }
737
+
738
+ .query-sql-wrapper {
739
+ margin-bottom: 0.3rem;
740
+ }
741
+
742
+ .query-sql {
743
+ padding: 0.5rem;
744
+ background: var(--bg-primary);
745
+ border: 1px solid var(--border-color);
746
+ border-radius: 4px;
747
+ font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
748
+ font-size: 0.7rem;
749
+ color: var(--accent-blue);
750
+ word-wrap: break-word;
751
+ overflow-x: auto;
752
+ }
753
+
754
+ .query-sql-collapsed {
755
+ white-space: nowrap;
756
+ overflow: hidden;
757
+ text-overflow: ellipsis;
758
+ }
759
+
760
+ .query-sql-expanded {
761
+ white-space: pre-wrap;
762
+ }
763
+
764
+ .query-source {
765
+ font-size: 0.7rem;
766
+ color: var(--text-muted);
767
+ font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>monospace;
768
+ }
769
+
770
+ .query-source a {
771
+ color: var(--accent-purple);
772
+ text-decoration: none;
773
+ }
774
+
775
+ .query-source a:hover {
776
+ text-decoration: underline;
777
+ }
778
+
779
+ /* Download Dropdown */
780
+ .download-dropdown {
781
+ position: relative;
782
+ display: inline-block;
783
+ }
784
+
785
+ .download-menu {
786
+ position: absolute;
787
+ top: calc(100% + 0.4rem);
788
+ right: 0;
789
+ min-width: 160px;
790
+ background: var(--bg-card);
791
+ border: 1px solid var(--border-color);
792
+ border-radius: 6px;
793
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
794
+ z-index: 100;
795
+ overflow: hidden;
796
+ }
797
+
798
+ .download-menu a {
799
+ display: block;
800
+ padding: 0.6rem 0.875rem;
801
+ color: var(--text-secondary);
802
+ text-decoration: none;
803
+ font-size: 0.75rem;
804
+ transition: all 0.15s ease;
805
+ border-bottom: 1px solid var(--border-color);
806
+ }
807
+
808
+ .download-menu a:last-child {
809
+ border-bottom: none;
810
+ }
811
+
812
+ .download-menu a:hover {
813
+ background: var(--bg-tertiary);
814
+ color: var(--text-primary);
815
+ }
816
+
817
+ .btn-small {
818
+ height: 30px;
819
+ padding: 0 0.875rem;
820
+ font-size: 0.75rem;
821
+ line-height: 1;
822
+ }
823
+
824
+ .btn-danger {
825
+ background: var(--accent-rose);
826
+ color: white;
827
+ border-color: var(--accent-rose);
828
+ }
829
+
830
+ .btn-danger:hover {
831
+ opacity: 0.9;
832
+ transform: translateY(-1px);
833
+ }
543
834
  </style>
544
835
  </head>
545
836
  <body>