Microservice to bring 2FA to self hosted PDSes
1:root,
2:root.light-mode {
3 --brand-color: rgb(16, 131, 254);
4 --primary-color: rgb(7, 10, 13);
5 --secondary-color: rgb(66, 86, 108);
6 --bg-primary-color: rgb(255, 255, 255);
7 --bg-secondary-color: rgb(240, 242, 245);
8 --border-color: rgb(220, 225, 230);
9 --danger-color: rgb(220, 38, 38);
10 --success-color: rgb(22, 163, 74);
11 --warning-color: rgb(234, 179, 8);
12 --table-stripe: rgba(0, 0, 0, 0.02);
13}
14
15@media (prefers-color-scheme: dark) {
16 :root {
17 --brand-color: rgb(16, 131, 254);
18 --primary-color: rgb(255, 255, 255);
19 --secondary-color: rgb(133, 152, 173);
20 --bg-primary-color: rgb(7, 10, 13);
21 --bg-secondary-color: rgb(13, 18, 23);
22 --border-color: rgb(40, 45, 55);
23 --table-stripe: rgba(255, 255, 255, 0.02);
24 }
25}
26
27:root.dark-mode {
28 --brand-color: rgb(16, 131, 254);
29 --primary-color: rgb(255, 255, 255);
30 --secondary-color: rgb(133, 152, 173);
31 --bg-primary-color: rgb(7, 10, 13);
32 --bg-secondary-color: rgb(13, 18, 23);
33 --border-color: rgb(40, 45, 55);
34 --table-stripe: rgba(255, 255, 255, 0.02);
35}
36
37/* --- Reset & Base ------------------------------------------- */
38
39* {
40 margin: 0;
41 padding: 0;
42 box-sizing: border-box;
43}
44
45body {
46 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
47 background: var(--bg-secondary-color);
48 color: var(--primary-color);
49 text-rendering: optimizeLegibility;
50 -webkit-font-smoothing: antialiased;
51}
52
53/* --- Layout: Sidebar + Main --------------------------------- */
54
55.layout {
56 display: flex;
57 min-height: 100vh;
58}
59
60.sidebar {
61 width: 220px;
62 background: var(--bg-primary-color);
63 border-right: 1px solid var(--border-color);
64 padding: 20px 0;
65 position: fixed;
66 top: 0;
67 left: 0;
68 bottom: 0;
69 overflow-y: auto;
70 display: flex;
71 flex-direction: column;
72}
73
74.sidebar-title {
75 font-size: 0.8125rem;
76 font-weight: 700;
77 padding: 0 20px;
78 margin-bottom: 4px;
79 white-space: nowrap;
80 overflow: hidden;
81 text-overflow: ellipsis;
82}
83
84.sidebar-subtitle {
85 font-size: 0.6875rem;
86 color: var(--secondary-color);
87 padding: 0 20px;
88 margin-bottom: 20px;
89}
90
91.sidebar nav {
92 flex: 1;
93}
94
95.sidebar nav a {
96 display: block;
97 padding: 8px 20px;
98 font-size: 0.8125rem;
99 color: var(--secondary-color);
100 text-decoration: none;
101 transition: background 0.1s, color 0.1s;
102}
103
104.sidebar nav a:hover {
105 background: var(--bg-secondary-color);
106 color: var(--primary-color);
107}
108
109.sidebar nav a.active {
110 color: var(--brand-color);
111 font-weight: 500;
112}
113
114.sidebar-footer {
115 padding: 16px 20px 0;
116 border-top: 1px solid var(--border-color);
117 margin-top: 16px;
118}
119
120.sidebar-footer .session-info {
121 font-size: 0.75rem;
122 color: var(--secondary-color);
123 margin-bottom: 8px;
124}
125
126.sidebar-footer form {
127 display: inline;
128}
129
130.sidebar-footer button {
131 background: none;
132 border: none;
133 font-size: 0.75rem;
134 color: var(--secondary-color);
135 cursor: pointer;
136 padding: 0;
137 text-decoration: underline;
138}
139
140.sidebar-footer button:hover {
141 color: var(--primary-color);
142}
143
144.main {
145 margin-left: 220px;
146 flex: 1;
147 padding: 32px;
148 max-width: 960px;
149}
150
151/* --- Common Components -------------------------------------- */
152
153.page-title {
154 font-size: 1.5rem;
155 font-weight: 700;
156 margin-bottom: 24px;
157}
158
159.page-subtitle {
160 font-size: 0.8125rem;
161 color: var(--secondary-color);
162 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
163 margin-top: -20px;
164 margin-bottom: 24px;
165 word-break: break-all;
166}
167
168.page-description {
169 font-size: 0.875rem;
170 color: var(--secondary-color);
171 margin-top: -16px;
172 margin-bottom: 24px;
173}
174
175.flash-success {
176 background: rgba(22, 163, 74, 0.1);
177 color: var(--success-color);
178 border: 1px solid rgba(22, 163, 74, 0.2);
179 border-radius: 8px;
180 padding: 10px 14px;
181 font-size: 0.875rem;
182 margin-bottom: 20px;
183}
184
185.flash-error {
186 background: rgba(220, 38, 38, 0.1);
187 color: var(--danger-color);
188 border: 1px solid rgba(220, 38, 38, 0.2);
189 border-radius: 8px;
190 padding: 10px 14px;
191 font-size: 0.875rem;
192 margin-bottom: 20px;
193}
194
195/* --- Buttons ------------------------------------------------ */
196
197.btn {
198 display: inline-flex;
199 align-items: center;
200 justify-content: center;
201 padding: 10px 20px;
202 font-size: 0.875rem;
203 font-weight: 500;
204 border: none;
205 border-radius: 8px;
206 cursor: pointer;
207 transition: opacity 0.15s;
208 text-decoration: none;
209}
210
211.btn:hover {
212 opacity: 0.85;
213}
214
215.btn-primary {
216 background: var(--brand-color);
217 color: #fff;
218}
219
220.btn-danger {
221 background: var(--danger-color);
222 color: #fff;
223 border-color: var(--danger-color);
224}
225
226.btn-warning {
227 background: var(--warning-color);
228 color: #000;
229 border-color: var(--warning-color);
230}
231
232.btn-small {
233 padding: 6px 12px;
234 font-size: 0.75rem;
235}
236
237.btn-outline-danger {
238 background: transparent;
239 color: var(--danger-color);
240 border: 1px solid var(--danger-color);
241}
242
243/* --- Detail Sections ---------------------------------------- */
244
245.detail-section {
246 background: var(--bg-primary-color);
247 border: 1px solid var(--border-color);
248 border-radius: 10px;
249 padding: 20px;
250 margin-bottom: 16px;
251}
252
253.detail-section h3 {
254 font-size: 0.875rem;
255 font-weight: 600;
256 margin-bottom: 12px;
257}
258
259.detail-row {
260 display: flex;
261 justify-content: space-between;
262 align-items: center;
263 padding: 8px 0;
264 font-size: 0.8125rem;
265 border-bottom: 1px solid var(--border-color);
266}
267
268.detail-row:last-child {
269 border-bottom: none;
270}
271
272.detail-row .label {
273 color: var(--secondary-color);
274 flex-shrink: 0;
275}
276
277.detail-row .value {
278 font-weight: 500;
279 word-break: break-all;
280 text-align: right;
281 max-width: 65%;
282}
283
284.detail-row .value a {
285 color: var(--brand-color);
286 text-decoration: none;
287}
288
289.detail-row .value a:hover {
290 text-decoration: underline;
291}
292
293/* --- Tables ------------------------------------------------- */
294
295.table-container {
296 background: var(--bg-primary-color);
297 border: 1px solid var(--border-color);
298 border-radius: 10px;
299 overflow: hidden;
300}
301
302table {
303 width: 100%;
304 border-collapse: collapse;
305}
306
307thead th {
308 text-align: left;
309 padding: 12px 16px;
310 font-size: 0.75rem;
311 font-weight: 600;
312 color: var(--secondary-color);
313 text-transform: uppercase;
314 letter-spacing: 0.5px;
315 border-bottom: 1px solid var(--border-color);
316}
317
318tbody tr {
319 border-bottom: 1px solid var(--border-color);
320}
321
322tbody tr:last-child {
323 border-bottom: none;
324}
325
326tbody tr:nth-child(even) {
327 background: var(--table-stripe);
328}
329
330tbody td {
331 padding: 10px 16px;
332 font-size: 0.8125rem;
333}
334
335tbody td a {
336 color: var(--brand-color);
337 text-decoration: none;
338}
339
340tbody td a:hover {
341 text-decoration: underline;
342}
343
344/* --- Badges ------------------------------------------------- */
345
346.badge {
347 display: inline-block;
348 padding: 2px 8px;
349 border-radius: 4px;
350 font-size: 0.75rem;
351 font-weight: 500;
352}
353
354.badge-success {
355 background: rgba(22, 163, 74, 0.1);
356 color: var(--success-color);
357}
358
359.badge-danger {
360 background: rgba(220, 38, 38, 0.1);
361 color: var(--danger-color);
362}
363
364.badge-warning {
365 background: rgba(234, 179, 8, 0.1);
366 color: var(--warning-color);
367}
368
369/* --- Forms -------------------------------------------------- */
370
371.form-card {
372 background: var(--bg-primary-color);
373 border: 1px solid var(--border-color);
374 border-radius: 10px;
375 padding: 24px;
376 max-width: 480px;
377}
378
379.form-group {
380 margin-bottom: 16px;
381}
382
383.form-group label {
384 display: block;
385 font-size: 0.8125rem;
386 font-weight: 500;
387 margin-bottom: 6px;
388 color: var(--primary-color);
389}
390
391.form-group input {
392 width: 100%;
393 padding: 10px 12px;
394 font-size: 0.875rem;
395 border: 1px solid var(--border-color);
396 border-radius: 8px;
397 background: var(--bg-primary-color);
398 color: var(--primary-color);
399 outline: none;
400 transition: border-color 0.15s;
401}
402
403.form-group input:focus {
404 border-color: var(--brand-color);
405}
406
407.form-group .hint {
408 font-size: 0.75rem;
409 color: var(--secondary-color);
410 margin-top: 4px;
411}
412
413.search-form {
414 display: flex;
415 gap: 8px;
416 margin-bottom: 24px;
417}
418
419.search-form input {
420 flex: 1;
421 padding: 10px 12px;
422 font-size: 0.875rem;
423 border: 1px solid var(--border-color);
424 border-radius: 8px;
425 background: var(--bg-primary-color);
426 color: var(--primary-color);
427 outline: none;
428}
429
430.search-form input:focus {
431 border-color: var(--brand-color);
432}
433
434.create-form {
435 display: flex;
436 gap: 8px;
437 align-items: flex-end;
438 margin-bottom: 24px;
439}
440
441.create-form .form-group {
442 display: flex;
443 flex-direction: column;
444 gap: 4px;
445}
446
447.create-form label {
448 font-size: 0.75rem;
449 font-weight: 500;
450 color: var(--secondary-color);
451}
452
453.create-form input[type="number"] {
454 padding: 10px 12px;
455 font-size: 0.875rem;
456 border: 1px solid var(--border-color);
457 border-radius: 8px;
458 background: var(--bg-primary-color);
459 color: var(--primary-color);
460 outline: none;
461 width: 120px;
462}
463
464.create-form input[type="number"]:focus {
465 border-color: var(--brand-color);
466}
467
468/* --- Utility ------------------------------------------------ */
469
470.mono-text {
471 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
472 font-size: 0.75rem;
473}
474
475.inline-form {
476 display: inline;
477}
478
479.empty-state {
480 text-align: center;
481 padding: 40px 20px;
482 color: var(--secondary-color);
483 font-size: 0.875rem;
484}
485
486.load-more {
487 text-align: center;
488 padding: 16px;
489}
490
491.load-more a {
492 color: var(--brand-color);
493 text-decoration: none;
494 font-size: 0.875rem;
495 font-weight: 500;
496}
497
498.load-more a:hover {
499 text-decoration: underline;
500}
501
502.pagination {
503 display: flex;
504 align-items: center;
505 justify-content: center;
506 gap: 16px;
507 padding: 16px 0;
508}
509
510.pagination-info {
511 font-size: 0.8125rem;
512 color: var(--secondary-color);
513}
514
515/* --- Dashboard: Cards --------------------------------------- */
516
517.cards {
518 display: grid;
519 grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
520 gap: 16px;
521 margin-bottom: 32px;
522}
523
524.card {
525 background: var(--bg-primary-color);
526 border: 1px solid var(--border-color);
527 border-radius: 10px;
528 padding: 20px;
529}
530
531.card-label {
532 font-size: 0.75rem;
533 font-weight: 500;
534 color: var(--secondary-color);
535 text-transform: uppercase;
536 letter-spacing: 0.5px;
537 margin-bottom: 6px;
538}
539
540.card-value {
541 font-size: 1.25rem;
542 font-weight: 600;
543}
544
545.card-value.success {
546 color: var(--success-color);
547}
548
549.card-value.danger {
550 color: var(--danger-color);
551}
552
553/* --- Accounts: DID Cell ------------------------------------- */
554
555.did-cell {
556 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
557 font-size: 0.75rem;
558 color: var(--secondary-color);
559}
560
561/* --- Account Detail ----------------------------------------- */
562
563.actions {
564 display: flex;
565 flex-wrap: wrap;
566 gap: 8px;
567 margin-top: 8px;
568}
569
570.actions form {
571 display: inline;
572}
573
574.actions .btn {
575 padding: 8px 16px;
576 font-size: 0.8125rem;
577 border: 1px solid var(--border-color);
578 background: var(--bg-primary-color);
579 color: var(--primary-color);
580}
581
582.actions .btn-primary {
583 background: var(--brand-color);
584 color: #fff;
585 border-color: var(--brand-color);
586}
587
588.actions .btn-danger {
589 background: var(--danger-color);
590 color: #fff;
591 border-color: var(--danger-color);
592}
593
594.actions .btn-warning {
595 background: var(--warning-color);
596 color: #000;
597 border-color: var(--warning-color);
598}
599
600.password-box {
601 background: rgba(22, 163, 74, 0.08);
602 border: 1px solid rgba(22, 163, 74, 0.2);
603 border-radius: 10px;
604 padding: 16px 20px;
605 margin-bottom: 16px;
606}
607
608.password-box .pw-label {
609 font-size: 0.75rem;
610 font-weight: 600;
611 color: var(--success-color);
612 margin-bottom: 6px;
613}
614
615.password-box .pw-value {
616 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
617 font-size: 1rem;
618 font-weight: 600;
619 user-select: all;
620}
621
622.back-link {
623 display: inline-block;
624 color: var(--brand-color);
625 text-decoration: none;
626 font-size: 0.8125rem;
627 margin-bottom: 16px;
628}
629
630.back-link:hover {
631 text-decoration: underline;
632}
633
634.collection-list {
635 max-height: 200px;
636 overflow-y: auto;
637 padding: 8px 0;
638}
639
640.collection-item {
641 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
642 font-size: 0.75rem;
643 padding: 4px 0;
644 color: var(--secondary-color);
645 border-bottom: 1px solid var(--border-color);
646}
647
648.collection-item:last-child {
649 border-bottom: none;
650}
651
652.threat-sig {
653 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
654 font-size: 0.75rem;
655 padding: 4px 0;
656 color: var(--secondary-color);
657}
658
659/* --- Create Account ----------------------------------------- */
660
661.success-card {
662 background: var(--bg-primary-color);
663 border: 1px solid var(--border-color);
664 border-radius: 10px;
665 padding: 24px;
666 max-width: 480px;
667}
668
669.success-card h3 {
670 font-size: 1rem;
671 font-weight: 600;
672 color: var(--success-color);
673 margin-bottom: 16px;
674}
675
676.success-card .detail-row .value {
677 display: flex;
678 align-items: center;
679 gap: 6px;
680}
681
682.password-highlight {
683 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
684 background: rgba(22, 163, 74, 0.08);
685 padding: 2px 6px;
686 border-radius: 4px;
687 user-select: all;
688}
689
690.copy-btn {
691 background: none;
692 border: 1px solid var(--border-color);
693 border-radius: 4px;
694 padding: 2px 6px;
695 font-size: 0.6875rem;
696 cursor: pointer;
697 color: var(--secondary-color);
698 transition: color 0.15s, border-color 0.15s;
699 white-space: nowrap;
700}
701
702.copy-btn:hover {
703 color: var(--primary-color);
704 border-color: var(--primary-color);
705}
706
707/* --- Invite Codes ------------------------------------------- */
708
709.code-box {
710 background: rgba(22, 163, 74, 0.08);
711 border: 1px solid rgba(22, 163, 74, 0.2);
712 border-radius: 10px;
713 padding: 16px 20px;
714 margin-bottom: 24px;
715}
716
717.code-box .code-label {
718 font-size: 0.75rem;
719 font-weight: 600;
720 color: var(--success-color);
721 margin-bottom: 6px;
722}
723
724.code-box .code-value {
725 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
726 font-size: 1rem;
727 font-weight: 600;
728 user-select: all;
729}
730
731.code-cell {
732 font-family: 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
733 font-size: 0.75rem;
734}
735
736/* --- Login Page --------------------------------------------- */
737
738.login-card {
739 background: var(--bg-primary-color);
740 border: 1px solid var(--border-color);
741 border-radius: 12px;
742 padding: 40px;
743 width: 100%;
744 max-width: 400px;
745 margin: 20px;
746}
747
748.login-title {
749 font-size: 1.5rem;
750 font-weight: 700;
751 text-align: center;
752 margin-bottom: 4px;
753}
754
755.login-subtitle {
756 font-size: 0.875rem;
757 color: var(--secondary-color);
758 text-align: center;
759 margin-bottom: 32px;
760}
761
762.login-card .form-group {
763 margin-bottom: 20px;
764}
765
766.login-card .form-group label {
767 font-size: 0.875rem;
768}
769
770.login-card .btn-primary {
771 width: 100%;
772}
773
774.error-msg {
775 background: rgba(220, 38, 38, 0.1);
776 color: var(--danger-color);
777 border: 1px solid rgba(220, 38, 38, 0.2);
778 border-radius: 8px;
779 padding: 10px 14px;
780 font-size: 0.875rem;
781 margin-bottom: 20px;
782}
783
784/* --- Error Page ---------------------------------------------- */
785
786.centered {
787 display: flex;
788 align-items: center;
789 justify-content: center;
790 min-height: 100vh;
791}
792
793.error-card {
794 background: var(--bg-primary-color);
795 border: 1px solid var(--border-color);
796 border-radius: 12px;
797 padding: 40px;
798 text-align: center;
799 max-width: 480px;
800 width: 100%;
801 margin: 20px;
802}
803
804.error-card--inset {
805 margin: 60px auto;
806}
807
808.error-icon {
809 font-size: 2.5rem;
810 margin-bottom: 16px;
811 color: var(--danger-color);
812}
813
814.error-title {
815 font-size: 1.25rem;
816 font-weight: 700;
817 margin-bottom: 8px;
818}
819
820.error-message {
821 font-size: 0.875rem;
822 color: var(--secondary-color);
823 margin-bottom: 24px;
824 line-height: 1.5;
825}
826
827.error-link {
828 display: inline-flex;
829 align-items: center;
830 justify-content: center;
831 padding: 10px 20px;
832 font-size: 0.875rem;
833 font-weight: 500;
834 border: none;
835 border-radius: 8px;
836 cursor: pointer;
837 text-decoration: none;
838 background: var(--brand-color);
839 color: #fff;
840 transition: opacity 0.15s;
841}
842
843.error-link:hover {
844 opacity: 0.85;
845}
846
847/* --- Mobile: Topbar + Hamburger ----------------------------- */
848
849.topbar {
850 display: none;
851 position: fixed;
852 top: 0;
853 left: 0;
854 right: 0;
855 height: 52px;
856 background: var(--bg-primary-color);
857 border-bottom: 1px solid var(--border-color);
858 align-items: center;
859 padding: 0 16px;
860 gap: 12px;
861 z-index: 100;
862}
863
864.topbar-title {
865 font-size: 0.875rem;
866 font-weight: 600;
867 overflow: hidden;
868 text-overflow: ellipsis;
869 white-space: nowrap;
870 flex: 1;
871}
872
873.hamburger {
874 background: none;
875 border: none;
876 cursor: pointer;
877 padding: 4px;
878 display: flex;
879 flex-direction: column;
880 gap: 5px;
881 flex-shrink: 0;
882}
883
884.hamburger span {
885 display: block;
886 width: 22px;
887 height: 2px;
888 background: var(--primary-color);
889 border-radius: 2px;
890 transition: transform 0.25s ease, opacity 0.25s ease;
891}
892
893body.sidebar-open .hamburger span:nth-child(1) {
894 transform: translateY(7px) rotate(45deg);
895}
896
897body.sidebar-open .hamburger span:nth-child(2) {
898 opacity: 0;
899}
900
901body.sidebar-open .hamburger span:nth-child(3) {
902 transform: translateY(-7px) rotate(-45deg);
903}
904
905.sidebar-overlay {
906 display: none;
907 position: fixed;
908 inset: 0;
909 background: rgba(0, 0, 0, 0.4);
910 z-index: 149;
911}
912
913/* --- Responsive --------------------------------------------- */
914
915@media (max-width: 768px) {
916 .topbar {
917 display: flex;
918 }
919
920 .sidebar {
921 transform: translateX(-220px);
922 transition: transform 0.25s ease;
923 z-index: 150;
924 }
925
926 body.sidebar-open .sidebar {
927 transform: translateX(0);
928 }
929
930 body.sidebar-open .sidebar-overlay {
931 display: block;
932 }
933
934 .main {
935 margin-left: 0;
936 padding: 24px 16px;
937 padding-top: 76px; /* 52px topbar + 24px breathing room */
938 }
939
940 /* Tables scroll horizontally */
941 .table-container {
942 overflow-x: auto;
943 -webkit-overflow-scrolling: touch;
944 }
945
946 /* Cards: single column */
947 .cards {
948 grid-template-columns: 1fr;
949 }
950
951 /* Form cards: full width */
952 .form-card,
953 .success-card {
954 max-width: 100%;
955 }
956
957 /* Search / create forms: stack vertically */
958 .search-form,
959 .create-form {
960 flex-direction: column;
961 align-items: stretch;
962 }
963
964 .create-form input[type="number"] {
965 width: 100%;
966 }
967
968 /* Detail rows wrap on narrow screens */
969 .detail-row {
970 flex-wrap: wrap;
971 gap: 4px;
972 }
973
974 .detail-row .value {
975 max-width: 100%;
976 text-align: left;
977 }
978
979 /* Actions wrap tightly */
980 .actions {
981 flex-direction: column;
982 align-items: flex-start;
983 }
984
985 .actions form,
986 .actions .btn {
987 width: 100%;
988 display: block;
989 text-align: center;
990 }
991}