Skip to main content

max / makenotwork

17.9 KB · 848 lines History Blame Raw
1 /* ===========================================
2 HTMX LOADING INDICATORS
3 =========================================== */
4
5 .htmx-indicator {
6 display: none;
7 animation: htmx-spin 1s ease-in-out infinite;
8 }
9
10 .htmx-request .htmx-indicator {
11 display: inline;
12 }
13
14 .htmx-request.htmx-indicator {
15 display: inline;
16 }
17
18 /* Loading spinner animation. Applied on .htmx-indicator above. */
19 @keyframes htmx-spin {
20 0% { opacity: 0.3; }
21 50% { opacity: 1; }
22 100% { opacity: 0.3; }
23 }
24
25 /* Disable buttons during request */
26 .htmx-request button[type="submit"] {
27 opacity: 0.6;
28 cursor: wait;
29 }
30
31 /* ===========================================
32 TOAST NOTIFICATIONS
33 =========================================== */
34
35 .toast-container {
36 position: fixed;
37 bottom: 1.5rem;
38 right: 1.5rem;
39 z-index: var(--z-toast);
40 display: flex;
41 flex-direction: column;
42 gap: var(--gap-peer);
43 }
44
45 .toast {
46 background: var(--primary-dark);
47 color: var(--primary-light);
48 padding: var(--gap-section);
49 font-family: var(--font-mono);
50 font-size: var(--text-note);
51 box-shadow: var(--elevation-overlay);
52 animation: toast-in var(--motion-fade) ease-out;
53 max-width: 300px;
54 display: flex;
55 align-items: center;
56 gap: var(--gap-section);
57 }
58
59 .toast-dismiss {
60 background: none;
61 border: none;
62 color: inherit;
63 font-size: var(--text-lead);
64 cursor: pointer;
65 opacity: 0.7;
66 padding: 0;
67 line-height: 1;
68 flex-shrink: 0;
69 }
70
71 .toast-dismiss:hover {
72 opacity: 1;
73 }
74
75 .toast-success {
76 background: var(--success);
77 }
78
79 .toast-error {
80 background: var(--danger);
81 }
82
83 .toast-warning {
84 background: var(--warning);
85 }
86
87 .toast-info {
88 background: var(--primary-dark);
89 }
90
91 .toast-retry-btn {
92 background: none;
93 border: 1px solid currentColor;
94 color: inherit;
95 font-family: var(--font-mono);
96 font-size: var(--text-fine);
97 font-weight: 600;
98 padding: var(--gap-bound) var(--gap-peer);
99 margin-left: var(--gap-section);
100 cursor: pointer;
101 }
102
103 .toast-retry-btn:hover {
104 background: color-mix(in oklch, var(--primary-light) 15%, transparent);
105 }
106
107 .toast.fade-out {
108 /* `Motion::Fade`, the same number the script waits before removing the node.
109 A literal here and a token there is the two parting silently, and reduced
110 motion zeroes the token. */
111 animation: toast-out var(--motion-fade) ease-in forwards;
112 }
113
114 @keyframes toast-in {
115 from {
116 transform: translateX(100%);
117 opacity: 0;
118 }
119 to {
120 transform: translateX(0);
121 opacity: 1;
122 }
123 }
124
125 @keyframes toast-out {
126 from {
127 transform: translateX(0);
128 opacity: 1;
129 }
130 to {
131 transform: translateX(100%);
132 opacity: 0;
133 }
134 }
135
136 /* ===========================================
137 RESTART WARNING BANNER
138 =========================================== */
139
140 /* Page-top behavior extensions on top of .banner.banner--warning.
141 Color / typography come from the .banner primitive. */
142 #restart-banner {
143 position: fixed;
144 top: 0;
145 left: 0;
146 right: 0;
147 z-index: var(--z-banner);
148 padding: var(--gap-group) var(--gap-section);
149 animation: banner-slide-down 0.3s ease-out;
150 }
151
152 @keyframes banner-slide-down {
153 from {
154 transform: translateY(-100%);
155 }
156 to {
157 transform: translateY(0);
158 }
159 }
160
161 /* ===========================================
162 SAVE STATUS INDICATORS
163 =========================================== */
164
165 /* Username availability status */
166 .username-status {
167 display: block;
168 margin-top: var(--gap-bound);
169 font-size: var(--text-note);
170 }
171
172 .username-status.available {
173 color: var(--success);
174 }
175
176 .username-status.taken {
177 color: var(--danger);
178 }
179
180 /* ===========================================
181 HTMX TRANSITIONS
182 =========================================== */
183
184 /* Fade in for swapped content */
185 .htmx-added {
186 animation: htmx-fade-in 0.2s ease-out;
187 }
188
189 @keyframes htmx-fade-in {
190 from {
191 opacity: 0;
192 }
193 to {
194 opacity: 1;
195 }
196 }
197
198 /* Settling animation for swapped content */
199 .htmx-settling {
200 opacity: 0.8;
201 }
202
203 /* Swap animation */
204 .htmx-swapping {
205 opacity: 0.5;
206 transition: opacity 0.2s ease-out;
207 }
208
209 /* ===========================================
210 ACCESSIBILITY - FOCUS STYLES
211 =========================================== */
212
213 /* Focus-visible for keyboard navigation */
214 *:focus-visible {
215 outline: 2px solid var(--focus-ring);
216 outline-offset: 2px;
217 }
218
219 /* Remove default focus outline since we use focus-visible */
220 *:focus:not(:focus-visible) {
221 outline: none;
222 }
223
224 /* Ensure buttons and links are keyboard accessible */
225 button:focus-visible,
226 a:focus-visible,
227 input:focus-visible,
228 select:focus-visible,
229 textarea:focus-visible {
230 outline: 2px solid var(--focus-ring);
231 outline-offset: 2px;
232 }
233
234 /* Tab focus is layout.css's, same ring as everything else at outline-offset
235 2px. The local rule inset it to -2px, which is only worth doing where a
236 strip clips its overflow; `.tabs` is flex/nowrap with no overflow set, so
237 there was nothing to clip and the tabs read differently from every other
238 focusable thing on the page for no reason. */
239
240 /* Canonical "selected" recipe (charter: docs/design-system.md).
241 Apply `.is-selected` to any interactive container to mark it as the
242 currently-selected option. A badge never takes it: a badge reports a status
243 and nobody selects "Enabled", so badges take a `data-tone` instead.
244 Visibility toggles (`.tab-content.active`, `.section-panel.active`) also
245 keep `.active` since they're not selection state. */
246 .is-selected {
247 background: var(--highlight-faint);
248 border-color: var(--focus-ring);
249 }
250
251 /* Skip to main content link (for screen readers) */
252 .skip-to-main {
253 position: absolute;
254 top: -100px;
255 left: 0;
256 background: var(--primary-dark);
257 color: var(--primary-light);
258 padding: 8px;
259 z-index: var(--z-skip-link);
260 transition: top 0.2s ease;
261 }
262
263 .skip-to-main:focus {
264 top: 0;
265 }
266
267 /* Visually hidden but accessible to screen readers */
268 .sr-only {
269 position: absolute;
270 width: 1px;
271 height: 1px;
272 padding: 0;
273 margin: -1px;
274 overflow: hidden;
275 clip: rect(0, 0, 0, 0);
276 white-space: nowrap;
277 border: 0;
278 }
279
280 /* Status indicator with text for accessibility */
281 .status-indicator::after {
282 content: attr(data-status);
283 position: absolute;
284 width: 1px;
285 height: 1px;
286 padding: 0;
287 margin: -1px;
288 overflow: hidden;
289 clip: rect(0, 0, 0, 0);
290 white-space: nowrap;
291 border: 0;
292 }
293
294 /* ===========================================
295 PAYWALL
296 =========================================== */
297
298 .paywall-section {
299 margin: var(--gap-page) 0;
300 padding: var(--gap-page);
301 background: var(--surface-sunken);
302 text-align: center;
303 }
304
305 .paywall-box h2 {
306 font-family: var(--font-display);
307 font-weight: normal;
308 margin-bottom: var(--gap-peer);
309 }
310
311 .paywall-price {
312 font-family: var(--font-mono);
313 font-size: var(--text-head);
314 margin-bottom: var(--gap-peer);
315 }
316
317 .paywall-desc {
318 font-family: var(--font-sans);
319 color: var(--content-muted);
320 margin-bottom: var(--gap-pane);
321 }
322
323 .paywall-note {
324 font-family: var(--font-mono);
325 font-size: var(--text-note);
326 color: var(--content-muted);
327 margin-top: var(--gap-section);
328 }
329
330 .paywall-note a {
331 color: var(--action);
332 }
333
334 /* ===========================================
335 MODALS
336 =========================================== */
337
338 .modal-overlay {
339 position: fixed;
340 inset: 0;
341 background: var(--overlay);
342 display: flex;
343 align-items: center;
344 justify-content: center;
345 z-index: var(--z-modal);
346 }
347
348 .modal {
349 background: var(--surface-page);
350 padding: var(--gap-page);
351 max-width: 500px;
352 width: 90%;
353 max-height: 90vh;
354 overflow-y: auto;
355 }
356
357 .modal h2 {
358 margin-bottom: var(--gap-pane);
359 }
360
361 .modal .form-actions {
362 display: flex;
363 gap: var(--gap-section);
364 justify-content: flex-end;
365 margin-top: var(--gap-pane);
366 }
367
368 /* ===========================================
369 LINK ROWS (Dashboard Links Section)
370 =========================================== */
371
372 .link-row {
373 display: flex;
374 gap: var(--gap-section);
375 align-items: center;
376 margin-bottom: var(--gap-section);
377 padding: var(--gap-section);
378 background: var(--surface-sunken);
379 transition: opacity 0.2s ease, background 0.2s ease;
380 }
381
382 .link-row input[type="text"],
383 .link-row input[type="url"] {
384 flex: 1;
385 min-width: 0;
386 }
387
388 .link-row .btn-danger {
389 padding: var(--gap-peer) var(--gap-section);
390 font-size: var(--text-note);
391 }
392
393 /* Link order buttons */
394 .link-order-buttons {
395 display: flex;
396 flex-direction: column;
397 gap: 2px;
398 }
399
400 .order-btn {
401 background: var(--surface-sunken);
402 border: 1px solid var(--border);
403 padding: var(--gap-bound) var(--gap-peer);
404 font-size: var(--text-fine);
405 line-height: 1;
406 cursor: pointer;
407 opacity: 0.6;
408 transition: opacity 0.15s ease, background 0.15s ease;
409 }
410
411 .order-btn:hover {
412 opacity: 1;
413 background: var(--border);
414 }
415
416 .order-btn:active {
417 box-shadow: var(--shadow-inset);
418 }
419
420 /* ===========================================
421 TEXT EDITOR
422 =========================================== */
423
424 .text-editor {
425 width: 100%;
426 }
427
428 .editor-footer {
429 display: flex;
430 justify-content: space-between;
431 padding: var(--gap-peer) var(--gap-section);
432 background: var(--surface-sunken);
433 font-size: var(--text-note);
434 opacity: 0.7;
435 }
436
437 /* ===========================================
438 RICH FIELD (crate::quasi::rich_field)
439 =========================================== */
440
441 /* The wrapper is this app's; the group inside it is the renderer's, and
442 `static/layout.css` carries every rule that decides what is showing. What is
443 left for this file is the two things a description does not say: how tall the
444 editor stands, and how the rendered preview reads.
445
446 Selected through the `[data-format="markdown"]` mark rather than through
447 makeover's class names, so nothing here overlaps the generated sheet. */
448
449 .rich-field [data-format="markdown"] > textarea {
450 padding: var(--gap-section);
451 font-family: var(--font-mono);
452 font-size: var(--text-note);
453 line-height: 1.6;
454 resize: vertical;
455 border: none;
456 background: var(--surface-sunken);
457 }
458
459 /* Three sizes for four surfaces: the item editor and the blog editor were
460 already the same surface written two ways (400px of CSS, and rows="20"). */
461 .rich-field--compact [data-format="markdown"] > textarea,
462 .rich-field--compact [data-editor-preview] {
463 min-height: 9rem;
464 }
465
466 .rich-field--standard [data-format="markdown"] > textarea,
467 .rich-field--standard [data-editor-preview] {
468 min-height: 15rem;
469 }
470
471 .rich-field--tall [data-format="markdown"] > textarea,
472 .rich-field--tall [data-editor-preview] {
473 min-height: 400px;
474 }
475
476 /* The pane stands where the control stood, so it reads as prose rather than as
477 source. The rules below are the retired `.markdown-preview` set, unchanged
478 except for what selects them. */
479 .rich-field [data-editor-preview] {
480 padding: var(--gap-pane);
481 font-family: var(--font-sans);
482 line-height: 1.8;
483 }
484
485 .rich-field [data-editor-preview] h1,
486 .rich-field [data-editor-preview] h2,
487 .rich-field [data-editor-preview] h3 {
488 margin-top: var(--gap-pane);
489 margin-bottom: var(--gap-peer);
490 }
491
492 .rich-field [data-editor-preview] h1 {
493 font-size: var(--text-title);
494 }
495
496 .rich-field [data-editor-preview] h2 {
497 font-size: var(--text-head);
498 }
499
500 .rich-field [data-editor-preview] h3 {
501 font-size: var(--text-lead);
502 }
503
504 .rich-field [data-editor-preview] p {
505 margin-bottom: var(--gap-section);
506 }
507
508 .rich-field [data-editor-preview] code {
509 background: var(--surface-sunken);
510 padding: var(--gap-bound);
511 font-family: var(--font-mono);
512 font-size: var(--text-note);
513 }
514
515 /* ===========================================
516 AUDIO UPLOAD
517 =========================================== */
518
519 .audio-upload {
520 width: 100%;
521 }
522
523 .current-audio {
524 display: flex;
525 justify-content: space-between;
526 align-items: center;
527 padding: var(--gap-section);
528 background: var(--surface-sunken);
529 margin-bottom: var(--gap-section);
530 }
531
532 .audio-info {
533 display: flex;
534 align-items: center;
535 gap: var(--gap-section);
536 }
537
538 .audio-icon {
539 font-size: var(--text-head);
540 opacity: 0.6;
541 }
542
543 .audio-details {
544 display: flex;
545 flex-direction: column;
546 }
547
548 .audio-filename {
549 font-family: var(--font-mono);
550 font-size: var(--text-note);
551 }
552
553 .audio-duration {
554 font-size: var(--text-note);
555 opacity: 0.6;
556 }
557
558 .upload-area .file-upload-area {
559 padding: var(--gap-page);
560 }
561
562 .upload-icon {
563 font-size: var(--text-title);
564 opacity: 0.4;
565 margin-bottom: var(--gap-peer);
566 }
567
568 .upload-text {
569 font-size: var(--text-lead);
570 margin-bottom: var(--gap-peer);
571 }
572
573 .upload-hint {
574 font-size: var(--text-note);
575 opacity: 0.6;
576 }
577
578 .upload-progress {
579 padding: var(--gap-pane);
580 background: var(--surface-sunken);
581 }
582
583 .progress-info {
584 display: flex;
585 justify-content: space-between;
586 margin-bottom: var(--gap-section);
587 font-size: var(--text-note);
588 }
589
590 /* Canonical progress bar primitive. One of these in the codebase.
591 - .progress-bar-container is the track (8px default; --slim = 6px)
592 - .progress-bar is the fill (success-green default; --highlight = purple)
593 - Add .progress-bar-container--rounded for the 3px-radius pill variant
594 Spacing (mb-4 / mt-2 etc.) is added by the caller, not baked in. */
595 .progress-bar-container {
596 height: 8px;
597 background: var(--border);
598 overflow: hidden;
599 }
600 .progress-bar-container--slim { height: 6px; }
601 .progress-bar-container--rounded { border-radius: var(--radius-control); }
602
603 /* Fill proportion comes from the template as --fill, because it is
604 per-render data. The property that reads it lives here. */
605 .progress-bar {
606 width: var(--fill, 0%);
607 height: 100%;
608 background: var(--success);
609 transition: width 0.2s ease;
610 }
611 .progress-bar--highlight { background: var(--action); }
612 .progress-bar--warn { background: var(--warning); }
613 .progress-bar--danger { background: var(--danger); }
614
615 /* Upload status block (filename + percent label + progress bar). One of these
616 in the codebase. Used by wizard upload flows and standalone audio/video upload partials. */
617 .upload-status { margin-top: var(--gap-peer); }
618 .upload-status-row {
619 display: flex;
620 align-items: center;
621 gap: var(--gap-peer);
622 }
623 .upload-status-row span { font-size: var(--text-note); }
624 .upload-status-msg { margin-top: var(--gap-peer); }
625 .upload-status-msg.is-success { color: var(--success); }
626 .upload-status-msg.is-error { color: var(--danger); }
627
628 /* Inline progress cell rendered inside a table-row status cell (batch uploads). */
629 .upload-progress-row {
630 display: flex;
631 align-items: center;
632 gap: var(--gap-peer);
633 }
634 .upload-progress-row .progress-bar-container { flex: 1; min-width: 60px; }
635 .upload-progress-pct {
636 font-size: var(--text-fine);
637 min-width: 2.5em;
638 text-align: right;
639 }
640
641 .upload-success {
642 padding: var(--gap-pane);
643 background: var(--surface-sunken);
644 text-align: center;
645 }
646 .upload-error {
647 padding: var(--gap-pane);
648 background: var(--surface-sunken);
649 text-align: center;
650 }
651
652 .hidden {
653 display: none !important;
654 }
655
656 /* ===========================================
657 VIEW TOGGLE (Grid/List)
658 =========================================== */
659
660 .view-controls {
661 display: flex;
662 gap: var(--gap-bound);
663 margin-left: auto;
664 }
665
666 .view-btn {
667 background: var(--surface-sunken);
668 border: none;
669 padding: var(--gap-peer) var(--gap-group);
670 font-family: var(--font-mono);
671 font-size: var(--text-note);
672 cursor: pointer;
673 opacity: 0.5;
674 transition: opacity 0.2s ease;
675 }
676
677 .view-btn:hover {
678 opacity: 0.8;
679 }
680
681 .view-btn.is-selected {
682 opacity: 1;
683 background: var(--primary-dark);
684 color: var(--primary-light);
685 }
686 .results-container.results-list .results-table {
687 display: block;
688 }
689
690 .results-container.results-list .results-grid {
691 display: none;
692 }
693
694 .results-container.results-grid .results-table {
695 display: none;
696 }
697
698 .results-container.results-grid .results-grid {
699 display: grid;
700 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
701 gap: var(--gap-pane);
702 border: 1px solid var(--border);
703 border-top: none;
704 padding: var(--gap-pane);
705 background: var(--surface-overlay);
706 }
707
708 /* Grid cards */
709 .grid-card {
710 background: var(--surface-overlay);
711 text-decoration: none;
712 color: var(--content);
713 display: flex;
714 flex-direction: column;
715 transition: background 0.2s ease;
716 }
717
718 .grid-card:hover {
719 background: var(--surface-sunken);
720 text-decoration: none;
721 }
722
723 .grid-card-thumbnail {
724 height: 120px;
725 background: var(--border);
726 display: flex;
727 align-items: center;
728 justify-content: center;
729 font-size: var(--text-fine);
730 opacity: 0.5;
731 text-transform: uppercase;
732 }
733
734 .grid-card-content {
735 padding: var(--gap-section);
736 display: flex;
737 flex-direction: column;
738 gap: var(--gap-bound);
739 }
740
741 .grid-card-title {
742 font-family: var(--font-display);
743 font-size: var(--text-lead);
744 }
745
746 .grid-card-meta {
747 font-size: var(--text-note);
748 opacity: 0.6;
749 }
750
751 .grid-card-footer {
752 display: flex;
753 justify-content: space-between;
754 align-items: center;
755 margin-top: var(--gap-peer);
756 font-size: var(--text-note);
757 }
758
759 .grid-card-category {
760 opacity: 0.6;
761 }
762
763 .grid-card-price {
764 font-weight: bold;
765 }
766
767 .grid-card-stat {
768 opacity: 0.6;
769 }
770
771 /* ===========================================
772 PROJECT PAGE VIEW TOGGLE
773 =========================================== */
774
775 .items-header {
776 display: flex;
777 justify-content: space-between;
778 align-items: center;
779 margin-bottom: var(--gap-pane);
780 }
781
782 .items-header .section-header {
783 margin-bottom: 0;
784 border-bottom: none;
785 padding-bottom: 0;
786 }
787
788 /* Grid view is default on project page */
789 .items-container.items-grid-view .items-grid {
790 display: grid;
791 }
792
793 .items-container.items-grid-view .items-list {
794 display: none;
795 }
796
797 .items-container.items-list-view .items-grid {
798 display: none;
799 }
800
801 .items-container.items-list-view .items-list {
802 display: block;
803 }
804
805 /* List items styling */
806 .items-list {
807 display: none;
808 }
809
810 .list-item {
811 display: flex;
812 justify-content: space-between;
813 align-items: center;
814 padding: var(--gap-section);
815 background: var(--surface-overlay);
816 margin-bottom: var(--gap-peer);
817 text-decoration: none;
818 color: var(--content);
819 transition: background 0.2s ease;
820 }
821
822 .list-item:hover {
823 background: var(--surface-sunken);
824 text-decoration: none;
825 }
826
827 .list-item-info {
828 display: flex;
829 flex-direction: column;
830 gap: var(--gap-bound);
831 }
832
833 .list-item-title {
834 font-family: var(--font-display);
835 font-size: var(--text-lead);
836 }
837
838 .list-item-meta {
839 font-size: var(--text-note);
840 opacity: 0.6;
841 }
842
843 .list-item-price {
844 font-size: var(--text-lead);
845 font-weight: bold;
846 }
847
848