style.css
⎇
Raw
1/* Custom layout and components on top of Pico.css */
2
3/* Scale down Pico's default sizing */
4html {
5 font-size: 16px;
6}
7
8/* Remove Pico's default bottom margin on button types */
9[type="button"],
10[type="reset"],
11[type="submit"] {
12 margin-bottom: 0;
13}
14
15/* Layout */
16body {
17 min-height: 100vh;
18 display: flex;
19 flex-direction: column;
20}
21
22header {
23 padding: 1rem 2rem;
24 position: sticky;
25 top: 0;
26 z-index: 100;
27 background-color: var(--pico-card-background-color);
28 border-bottom: 1px solid var(--pico-muted-border-color);
29 display: flex;
30 align-items: center;
31 gap: 2rem;
32}
33
34header h1 {
35 margin: 0;
36 flex-shrink: 0;
37 font-size: 1.5rem;
38}
39
40/* Brand: logo + wordmark, no longer a link */
41.brand {
42 display: flex;
43 align-items: center;
44 gap: 0.55rem;
45}
46
47.brand-logo {
48 display: block;
49 flex-shrink: 0;
50}
51
52nav {
53 display: flex;
54 gap: 1rem;
55}
56
57nav a {
58 color: var(--pico-muted-color);
59 text-decoration: none;
60 padding: 0.5rem 1rem;
61 border-radius: var(--pico-border-radius);
62 transition: all 0.2s;
63}
64
65nav a:hover {
66 color: var(--pico-primary);
67 background-color: var(--pico-card-sectioning-background-color);
68}
69
70nav a.active {
71 background-color: var(--pico-primary);
72 color: var(--pico-primary-inverse);
73 font-weight: 600;
74}
75
76main {
77 flex: 1;
78 padding: 2rem 1rem;
79}
80
81main > * {
82 max-width: 1400px;
83 margin-left: auto;
84 margin-right: auto;
85}
86
87footer {
88 padding: 1rem 2rem;
89 text-align: center;
90 color: var(--pico-muted-color);
91 font-size: 0.9rem;
92 background-color: var(--pico-card-background-color);
93 border-top: 1px solid var(--pico-muted-border-color);
94}
95
96/* Page headers */
97.page-header {
98 display: flex;
99 justify-content: space-between;
100 align-items: center;
101 margin-bottom: 1.5rem;
102 flex-wrap: wrap;
103 gap: 1rem;
104}
105
106.page-header h2 {
107 font-size: 1.5rem;
108 margin: 0;
109}
110
111.actions {
112 display: flex;
113 gap: 1rem;
114 align-items: center;
115 flex-wrap: wrap;
116}
117
118/* Forms */
119.form-group {
120 margin-bottom: 1rem;
121}
122
123.form-row {
124 display: grid;
125 grid-template-columns: 1fr 1fr;
126 gap: 1rem;
127}
128
129.radio-group {
130 display: flex;
131 flex-wrap: wrap;
132 gap: 1rem;
133}
134
135.radio-group label {
136 display: flex;
137 align-items: center;
138 gap: 0.3rem;
139 cursor: pointer;
140 font-weight: normal;
141 margin-bottom: 0;
142}
143
144.form-actions {
145 display: flex;
146 gap: 1rem;
147 margin-top: 1.5rem;
148}
149
150.file-selector {
151 display: flex;
152 align-items: center;
153 gap: 0.75rem;
154 flex-wrap: wrap;
155 margin-bottom: 1rem;
156}
157
158.file-selector label {
159 margin: 0;
160}
161
162.file-selector select {
163 flex: 1;
164 min-width: 0;
165 margin: 0;
166}
167
168.file-selector button {
169 margin: 0;
170 width: auto;
171 flex: none;
172}
173
174/* Buttons */
175button.small,
176[role="button"].small {
177 padding: 0.3rem 0.6rem;
178 font-size: 0.85rem;
179}
180
181button.danger,
182button.danger:hover,
183button.danger:focus,
184[role="button"].danger,
185[role="button"].danger:hover,
186[role="button"].danger:focus {
187 --pico-background-color: #c62828;
188 --pico-border-color: #c62828;
189 --pico-color: white;
190}
191
192button.danger:hover,
193button.danger:focus,
194[role="button"].danger:hover,
195[role="button"].danger:focus {
196 --pico-background-color: #b71c1c;
197 --pico-border-color: #b71c1c;
198}
199
200button:disabled {
201 opacity: 0.5;
202 cursor: not-allowed;
203}
204
205/* Library */
206.breadcrumbs {
207 margin-bottom: 1rem;
208 display: flex;
209 align-items: center;
210 justify-content: flex-start;
211 gap: 0.35rem;
212 flex-wrap: wrap;
213 text-align: left;
214 width: 100%;
215}
216
217.breadcrumbs a,
218.breadcrumbs span {
219 flex-shrink: 0;
220}
221
222.breadcrumbs .current {
223 cursor: default;
224 pointer-events: none;
225}
226
227.library-grid {
228 display: grid;
229 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
230 gap: 1rem;
231}
232
233.folder-item,
234.media-item {
235 background: var(--pico-card-background-color);
236 border: 1px solid var(--pico-card-border-color);
237 border-radius: var(--pico-border-radius);
238 overflow: hidden;
239 text-decoration: none;
240 color: var(--pico-color);
241 box-shadow: var(--pico-card-box-shadow);
242}
243
244.folder-item:hover,
245.media-item:hover {
246 background-color: var(--pico-primary-background);
247 border-color: var(--pico-primary-border);
248 box-shadow: var(--pico-card-box-shadow), 0 4px 12px rgba(0, 0, 0, 0.15);
249}
250
251.folder-item:hover .folder-name,
252.media-item:hover .media-info,
253.media-item:hover .media-title,
254.media-item:hover .media-path {
255 color: var(--pico-primary-inverse);
256}
257
258.media-info {
259 transition: color 0.2s;
260}
261
262.media-thumbnail {
263 aspect-ratio: 16/9;
264 background: var(--pico-card-sectioning-background-color);
265 display: flex;
266 align-items: center;
267 justify-content: center;
268 font-size: 3rem;
269 position: relative;
270}
271
272/* Each thumbnail sits in a tile so it can carry its own per-file duration
273 badge; the tile fills the container (single file) or a grid cell (multi). */
274.thumb-tile {
275 position: relative;
276 display: flex;
277 align-items: center;
278 justify-content: center;
279 overflow: hidden;
280 width: 100%;
281 height: 100%;
282 min-width: 0;
283 min-height: 0;
284}
285
286.thumb-tile .media-duration {
287 position: absolute;
288 bottom: 0.4rem;
289 right: 0.4rem;
290 background: rgba(0, 0, 0, 0.75);
291 color: #fff;
292 padding: 0.15rem 0.4rem;
293 border-radius: var(--pico-border-radius);
294 font-size: 0.7rem;
295 font-weight: 500;
296 line-height: 1;
297}
298
299.folder-icon {
300 aspect-ratio: 16/9;
301 background: var(--pico-card-sectioning-background-color);
302 display: flex;
303 align-items: center;
304 justify-content: center;
305 font-size: 3rem;
306}
307
308.thumb-tile img {
309 width: 100%;
310 height: 100%;
311 /* contain: show the whole thumbnail (letterboxed) and never overflow the
312 16/9 box, including portrait/vertical sources. */
313 object-fit: contain;
314}
315
316.thumb-tile img[src$=".svg"] {
317 width: 48px;
318 height: 48px;
319 opacity: 0.5;
320 object-fit: contain;
321}
322
323/* Multi-file items: tile each media file's thumbnail in a grid. */
324.media-thumbnail.count-2,
325.media-thumbnail.count-3,
326.media-thumbnail.count-many {
327 display: grid;
328 gap: 2px;
329}
330
331.media-thumbnail.count-2 {
332 grid-template-columns: 1fr 1fr;
333}
334
335.media-thumbnail.count-3,
336.media-thumbnail.count-many {
337 grid-template-columns: 1fr 1fr;
338 grid-template-rows: 1fr 1fr;
339}
340
341/* For 3 files, let the first thumbnail span the full top row. */
342.media-thumbnail.count-3 .thumb-tile:first-of-type {
343 grid-column: 1 / -1;
344}
345
346/* "+N" overlay on the last tile when there are more than 4 files. */
347.media-thumbnail .thumb-more {
348 position: absolute;
349 right: 0;
350 bottom: 0;
351 width: 50%;
352 height: 50%;
353 display: flex;
354 align-items: center;
355 justify-content: center;
356 background: rgba(0, 0, 0, 0.6);
357 color: #fff;
358 font-size: 1.1rem;
359 font-weight: 600;
360 pointer-events: none;
361}
362
363/* File-count badge in the top-left for multi-file items. */
364.media-thumbnail .thumb-count {
365 position: absolute;
366 top: 0.4rem;
367 left: 0.4rem;
368 background: rgba(0, 0, 0, 0.75);
369 color: #fff;
370 padding: 0.15rem 0.4rem;
371 border-radius: var(--pico-border-radius);
372 font-size: 0.7rem;
373 font-weight: 500;
374 line-height: 1;
375}
376
377.folder-name,
378.media-info {
379 padding: 0.75rem;
380}
381
382.media-title {
383 font-weight: 500;
384 margin-bottom: 0.3rem;
385 overflow: hidden;
386 text-overflow: ellipsis;
387 white-space: nowrap;
388}
389
390.media-path {
391 font-size: 0.85rem;
392}
393
394.media-info .media-duration {
395 display: none;
396}
397
398/* Detail view */
399.detail-view {
400 max-width: 900px;
401}
402
403.detail-header {
404 display: flex;
405 justify-content: space-between;
406 align-items: center;
407 margin-bottom: 1.5rem;
408 flex-wrap: wrap;
409 gap: 1rem;
410}
411
412.player-container {
413 background: #000;
414 border-radius: var(--pico-border-radius);
415 overflow: hidden;
416 margin-bottom: 1.5rem;
417 max-height: 80vh;
418 display: flex;
419 align-items: center;
420 justify-content: center;
421}
422
423.player-container video,
424.player-container audio {
425 max-width: 100%;
426 max-height: 80vh;
427 width: auto;
428 height: auto;
429 display: block;
430}
431
432.detail-meta dl {
433 display: grid;
434 grid-template-columns: 150px 1fr;
435 gap: 0.5rem 1rem;
436 align-items: baseline;
437}
438
439/* Render keys as understated labels so they read as keys — not just another
440 line of value text — in both the two-column and the stacked (narrow) layout. */
441.detail-meta dt {
442 font-size: 0.75rem;
443 font-weight: 600;
444 text-transform: uppercase;
445 letter-spacing: 0.04em;
446 color: var(--pico-muted-color);
447}
448
449.detail-meta dd {
450 word-break: break-all;
451 margin: 0;
452}
453
454/* Description: preserve the source line breaks; collapse long ones to ~2 lines
455 with a pure-CSS (no-JS, CSP-safe) Show more/less toggle. */
456.detail-meta dd.description {
457 margin: 0;
458}
459
460.description .desc-text {
461 white-space: pre-wrap;
462 word-break: normal;
463 overflow-wrap: anywhere;
464 line-height: 1.5;
465}
466
467/* The checkbox is the toggle state; it's driven by the label, never shown. */
468.description.collapsible .desc-toggle {
469 display: none;
470}
471
472/* Collapsed: clamp to ~2 lines (max-height works with pre-wrap; line-clamp
473 does not), with a fade so the cut reads as intentional. */
474.description.collapsible .desc-text {
475 position: relative;
476 max-height: 3em;
477 overflow: hidden;
478}
479
480.description.collapsible .desc-text::after {
481 content: "";
482 position: absolute;
483 left: 0;
484 right: 0;
485 bottom: 0;
486 height: 1.5em;
487 background: linear-gradient(to bottom, transparent, var(--pico-background-color));
488 pointer-events: none;
489}
490
491.description.collapsible .desc-toggle:checked ~ .desc-text {
492 max-height: none;
493}
494
495.description.collapsible .desc-toggle:checked ~ .desc-text::after {
496 display: none;
497}
498
499.description.collapsible .desc-toggle-label {
500 display: inline-block;
501 margin-top: 0.4rem;
502 color: var(--pico-primary);
503 font-size: 0.85rem;
504 font-weight: 600;
505 cursor: pointer;
506 user-select: none;
507}
508
509.description.collapsible .desc-toggle-label::after {
510 content: "Show more \25be";
511}
512
513.description.collapsible .desc-toggle:checked ~ .desc-toggle-label::after {
514 content: "Show less \25b4";
515}
516
517/* Queue */
518.queue-list {
519 display: flex;
520 flex-direction: column;
521 gap: 1rem;
522}
523
524.queue-item {
525 padding: 1rem;
526 margin-bottom: 0;
527 position: relative;
528 transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
529}
530
531/* Stretched link: the whole row navigates to the detail page, matching the
532 library card hover. Interactive children (Remove) sit above it via z-index. */
533.queue-link {
534 position: absolute;
535 inset: 0;
536 z-index: 1;
537}
538
539.queue-item:hover {
540 background-color: var(--pico-primary-background);
541 border-color: var(--pico-primary-border);
542 box-shadow: var(--pico-card-box-shadow), 0 4px 12px rgba(0, 0, 0, 0.15);
543 cursor: pointer;
544}
545
546/* Keep text legible on the solid primary hover background (status badges and
547 the error message keep their own colors). */
548.queue-item:hover .queue-url,
549.queue-item:hover .queue-meta {
550 color: var(--pico-primary-inverse);
551}
552
553.queue-header {
554 display: flex;
555 justify-content: space-between;
556 align-items: flex-start;
557 gap: 1rem;
558 flex-wrap: wrap;
559 margin-bottom: 0.5rem;
560}
561
562.queue-url {
563 flex: 1;
564 min-width: 0;
565 word-break: break-all;
566}
567
568.queue-status {
569 display: flex;
570 gap: 0.5rem;
571 align-items: center;
572 flex-wrap: wrap;
573}
574
575/* Keep the Remove action clickable above the stretched link overlay. */
576.queue-action {
577 position: relative;
578 z-index: 2;
579}
580
581.status-badge {
582 padding: 0.2rem 0.6rem;
583 border-radius: 12px;
584 font-size: 0.85rem;
585 font-weight: 500;
586}
587
588.status-badge.queued {
589 background: var(--pico-card-sectioning-background-color);
590 color: var(--pico-muted-color);
591}
592
593.status-badge.downloading {
594 background: var(--pico-primary);
595 color: var(--pico-primary-inverse);
596}
597
598.status-badge.completed {
599 background: var(--pico-ins-color);
600 color: white;
601}
602
603.status-badge.error {
604 background: var(--pico-del-color);
605 color: white;
606}
607
608.error-message {
609 color: var(--pico-del-color);
610 font-size: 0.9rem;
611 margin-top: 0.5rem;
612}
613
614.error-text {
615 color: var(--pico-del-color);
616}
617
618.queue-meta {
619 margin-top: 0.5rem;
620 font-size: 0.85rem;
621 display: flex;
622 gap: 1rem;
623}
624
625/* Logs */
626.logs {
627 max-height: 500px;
628 overflow-y: auto;
629 white-space: pre-wrap;
630 word-break: break-all;
631}
632
633pre {
634 padding: 1.5rem;
635}
636
637/* Settings */
638.settings-layout {
639 display: flex;
640 flex-direction: column;
641 gap: 2rem;
642}
643
644.presets-list {
645 display: flex;
646 flex-direction: column;
647 gap: 1rem;
648 margin-bottom: 1.5rem;
649}
650
651.preset-header {
652 display: flex;
653 justify-content: space-between;
654 align-items: center;
655 margin-bottom: 0.5rem;
656}
657
658.preset-header h4 {
659 display: flex;
660 align-items: center;
661 gap: 0.5rem;
662 margin: 0;
663}
664
665.badge {
666 background: var(--pico-primary);
667 color: var(--pico-primary-inverse);
668 padding: 0.1rem 0.4rem;
669 border-radius: 10px;
670 font-size: 0.75rem;
671}
672
673.preset-actions {
674 display: flex;
675 gap: 0.5rem;
676 flex-wrap: wrap;
677 margin-top: 1rem;
678}
679
680.preset-description {
681 margin-bottom: 0.5rem;
682}
683
684.preset-flags {
685 margin-top: 0.5rem;
686}
687
688.preset-flags pre {
689 overflow-x: auto;
690 margin-top: 0.3rem;
691 margin-bottom: 0;
692}
693
694/* Nested articles need visible borders */
695.presets-list article {
696 border: 1px solid var(--pico-muted-border-color);
697}
698
699/* Suppress Pico details marker for button-like summaries */
700details:has(summary[role="button"]) summary::after {
701 display: none !important;
702}
703
704details summary[role="button"] {
705 display: inline-flex;
706 width: auto;
707}
708
709/* Toggle text: Edit vs Cancel */
710details .cancel-text {
711 display: none;
712}
713
714details[open] .edit-text {
715 display: none;
716}
717
718details[open] .cancel-text {
719 display: inline;
720}
721
722/* Delete overlay popover */
723.delete-overlay {
724 position: relative;
725}
726
727.delete-overlay summary {
728 list-style: none;
729 cursor: pointer;
730}
731
732.delete-overlay summary::-webkit-details-marker {
733 display: none;
734}
735
736.delete-overlay summary::after {
737 display: none !important;
738}
739
740.delete-overlay .overlay-panel {
741 position: absolute;
742 top: calc(100% + 0.5rem);
743 right: 0;
744 z-index: 1000;
745 width: 260px;
746 background: var(--pico-background-color);
747 border: 1px solid var(--pico-muted-border-color);
748 border-radius: var(--pico-border-radius);
749 padding: 1rem;
750 box-shadow: var(--pico-dropdown-box-shadow);
751}
752
753/* Popover arrow */
754.delete-overlay .overlay-panel::before {
755 content: '';
756 position: absolute;
757 top: -6px;
758 right: 1rem;
759 width: 10px;
760 height: 10px;
761 background: var(--pico-background-color);
762 border-left: 1px solid var(--pico-muted-border-color);
763 border-top: 1px solid var(--pico-muted-border-color);
764 transform: rotate(45deg);
765}
766
767.delete-overlay .overlay-panel p {
768 margin-bottom: 1rem;
769 font-size: 0.9rem;
770}
771
772.delete-overlay .overlay-panel .form-actions {
773 margin-top: 0;
774 justify-content: flex-end;
775}
776
777/* Inline edit form */
778.edit-form {
779 padding: 1rem;
780}
781
782/* Conditional sections (no-JS show/hide) */
783.conditional-section {
784 margin-bottom: 1rem;
785}
786
787.conditional-section .conditional-content {
788 display: none;
789 margin-top: 1rem;
790}
791
792/* Checkbox triggers */
793.conditional-section:has(input[type="checkbox"].conditional-trigger:checked) .conditional-content {
794 display: block;
795}
796
797/* Radio triggers for format mode */
798.conditional-section:has(.radio-group input[value="preset"]:checked) .conditional-content.format-preset,
799.conditional-section:has(.radio-group input[value="custom"]:checked) .conditional-content.format-custom {
800 display: block;
801}
802
803/* Subscriptions: lock the cron field unless the schedule is "Custom (cron)".
804 Pure CSS — the :has() reacts live as the <select> changes, no JS needed. */
805input[name="cron_expr"] {
806 opacity: 0.5;
807 pointer-events: none;
808}
809.form-row:has(select[name="schedule_kind"] option[value="cron"]:checked) input[name="cron_expr"] {
810 opacity: 1;
811 pointer-events: auto;
812}
813
814/* Format mode selector - hidden radio inputs with button-like labels */
815.radio-group.format-mode {
816 display: flex;
817 gap: 0.5rem;
818 margin-top: 0.5rem;
819 flex-wrap: wrap;
820}
821
822.radio-group.format-mode input[type="radio"] {
823 position: absolute;
824 opacity: 0;
825 width: 0;
826 height: 0;
827}
828
829.radio-group.format-mode label {
830 padding: 0.4rem 0.8rem;
831 border: 1px solid var(--pico-muted-border-color);
832 border-radius: var(--pico-border-radius);
833 cursor: pointer;
834 background: var(--pico-background-color);
835 transition: all 0.2s;
836 font-weight: normal;
837}
838
839.radio-group.format-mode label:hover {
840 background: var(--pico-card-sectioning-background-color);
841}
842
843.radio-group.format-mode label:has(input:checked) {
844 background: var(--pico-primary);
845 color: var(--pico-primary-inverse);
846 border-color: var(--pico-primary);
847}
848
849/* Inline informational note paragraph */
850.note {
851 font-size: 0.85rem;
852 color: var(--pico-muted-color);
853}
854
855/* Collapsible "managed yt-dlp options" notes component */
856.ytdlp-notes {
857 margin: 0 0 1rem;
858 border: 1px solid var(--pico-muted-border-color);
859 border-radius: var(--pico-border-radius);
860 background: var(--pico-card-sectioning-background-color);
861 overflow: hidden;
862}
863
864.ytdlp-notes > summary {
865 cursor: pointer;
866 padding: 0.6rem 0.9rem;
867 font-size: 0.9rem;
868 font-weight: 600;
869 color: var(--pico-color);
870 user-select: none;
871 transition: background 0.15s ease-in-out;
872}
873
874.ytdlp-notes > summary:hover {
875 background: var(--pico-card-background-color);
876}
877
878.ytdlp-notes-icon {
879 color: var(--pico-primary);
880 font-style: normal;
881 font-size: 1rem;
882 margin-right: 0.4rem;
883}
884
885.ytdlp-notes-body {
886 padding: 0 0.9rem 0.6rem;
887 border-top: 1px solid var(--pico-muted-border-color);
888}
889
890.ytdlp-notes-body > p {
891 font-size: 0.85rem;
892 color: var(--pico-muted-color);
893 margin: 0.7rem 0;
894}
895
896.ytdlp-notes-table-wrap {
897 overflow-x: auto;
898}
899
900.ytdlp-notes-body table {
901 margin: 0;
902 font-size: 0.85rem;
903}
904
905.ytdlp-notes-body th,
906.ytdlp-notes-body td {
907 padding: 0.4rem 0.6rem;
908 vertical-align: top;
909}
910
911.ytdlp-notes-body td:first-child {
912 white-space: nowrap;
913}
914
915.ytdlp-notes-body code {
916 font-size: 0.8rem;
917}
918
919/* Theme selector */
920.theme-selector {
921 display: flex;
922 gap: 0.5rem;
923}
924
925.theme-selector button {
926 min-width: 80px;
927}
928
929.theme-selector button.active {
930 background: var(--pico-primary);
931 color: var(--pico-primary-inverse);
932 border-color: var(--pico-primary);
933}
934
935/* Download form */
936.download-form {
937 max-width: 900px;
938}
939
940.download-form form {
941 max-width: 800px;
942}
943
944/* Filter form inline */
945.filter-form {
946 display: flex;
947 gap: 0.5rem;
948 align-items: center;
949}
950
951.filter-form input,
952.filter-form select,
953.filter-form button {
954 margin-bottom: 0;
955 width: auto;
956 display: inline-block;
957 flex-shrink: 0;
958}
959
960.filter-form input[type="text"] {
961 min-width: 200px;
962}
963
964/* Empty state */
965.empty-state {
966 text-align: center;
967 padding: 3rem;
968}
969
970/* Utility */
971.inline {
972 display: inline;
973}
974
975/* Responsive */
976@media (max-width: 768px) {
977 main {
978 padding: 1rem;
979 }
980
981 .page-header {
982 flex-direction: column;
983 align-items: flex-start;
984 }
985
986 .library-grid {
987 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
988 }
989
990 .form-row {
991 grid-template-columns: 1fr;
992 }
993
994 .detail-meta dl {
995 grid-template-columns: 1fr;
996 gap: 0;
997 }
998
999 /* Stacked layout: tighten key→value and space out each pair so the grouping
1000 is obvious without the column alignment to lean on. */
1001 .detail-meta dt {
1002 margin-top: 0.85rem;
1003 }
1004
1005 .detail-meta dt:first-child {
1006 margin-top: 0;
1007 }
1008
1009 .detail-meta dd {
1010 margin-top: 0.15rem;
1011 }
1012
1013 .queue-header {
1014 flex-direction: column;
1015 }
1016
1017 header {
1018 flex-direction: column;
1019 gap: 1rem;
1020 padding: 1rem;
1021 }
1022
1023 nav {
1024 width: 100%;
1025 justify-content: center;
1026 }
1027}
1028