Skip to main content

max / makenotwork

48.0 KB · 1645 lines History Blame Raw
1 :root {
2 /* Typography: nothing. All three tokens come from typography.css, which
3 loads before this file.
4
5 `--font-mono` and `--font-sans` are the house slots and `--font-display`
6 is MNW's own, an override declared in build.rs. Do not redefine any of the
7 three here -- that is the divergence the unification deleted, and this
8 file is the one that would win. A product face belongs in the override,
9 where the whole tree can read it. */
10
11 /* --------------------------------------------------------------------
12 INTENT LAYER, the shared theme vocabulary (themeable)
13
14 These are the intent tokens emitted by theme_common::intent_css_vars():
15 colors declared by ROLE (surface/content/action/status/line/category),
16 plus the derived interactive states (hover/active/selection/row-stripe/
17 contrast) the crate computes once for every product. The values below
18 are the platform default (shared/themes/makenotwork.toml resolved) so
19 non-creator pages render without injection; creator profile/project/item
20 pages inject a <style> that supersedes this block with the chosen theme.
21 Generated. Keep in sync with `cargo run --example dump_intents makenotwork`.
22 -------------------------------------------------------------------- */
23 --surface-page: #ddd6c9;
24 --surface-raised: #efe9e0;
25 --surface-sunken: #cdc5b5;
26 --surface-overlay: #efe9e0;
27 /* The content surface cut into a raised one, so a list reads as content in a
28 container rather than as bands on a panel. Derived by makeover from
29 surface-raised, away from resolved content, so it goes lighter on a light
30 theme and darker on a dark one. */
31 --surface-well: #fffef5;
32 /* The two edges of a bevel. makeover derives both from the theme's raised
33 surface, so they arrive with the rest of the intent set through
34 intent_css_vars() and every creator theme gains them automatically. */
35 --bevel-light: #fffef5;
36 --bevel-dark: #b5afa6;
37 --content: #3d3530;
38 --content-secondary: #5c554f;
39 --content-muted: #8a8480;
40 --content-on-action: #ffffff;
41 --action: #6c5ce7;
42 --action-hover: #7a6cf8;
43 --danger: #c0392b;
44 --success: #27ae60;
45 --warning: #f59e0b;
46 --info: #17a2b8;
47 --border: #b3ab97;
48 --border-strong: #a39c88;
49 --focus-ring: #6c5ce7;
50 --hover-surface: #cdc5b5;
51 --category-one: #c0392b;
52 --category-two: #27ae60;
53 --category-three: #6c5ce7;
54 --category-four: #f59e0b;
55 --category-five: #a29bfe;
56 --category-six: #17a2b8;
57 --overlay: rgba(3, 1, 0, 0.5);
58 /* What a surface floating over the page casts onto it. Same tone as the
59 scrim above and pinned near-black on every theme, because a shadow is
60 absence of light rather than a tint. layout.css composes the geometry
61 into --elevation-overlay; a consumer that needs a different geometry
62 builds its own from this token rather than from a literal. */
63 --elevation: rgba(3, 1, 0, 0.18);
64
65 /* --------------------------------------------------------------------
66 APP-LOCAL CONSTANTS, not theme colors, not derivable from intents.
67
68 Component CSS references the intent tokens above directly (no brand
69 aliases). Status backgrounds derive from the intents inline via
70 color-mix(); these few stay fixed:
71 - pure black/white contrast anchors
72 - Stripe's brand color (an external constant)
73 - diff add/del, a hard readability convention that must not re-theme
74 -------------------------------------------------------------------- */
75 --primary-dark: #000000;
76 --primary-light: #ffffff;
77 --stripe: #635BFF;
78
79 /* The faint accent tint every selection and highlight recipe fills with.
80 Derived from --action, so it re-tints with the creator's theme. Named
81 because the charter names it and because a dozen rules read it. */
82 --highlight-faint: color-mix(in oklch, var(--action) 8%, transparent);
83
84 /* Diff / code review, fixed green-add / red-del convention */
85 --diff-add: #2d7d2d;
86 --diff-add-bg: rgba(45, 125, 45, 0.08);
87 --diff-del: #a83232;
88 --diff-del-bg: rgba(168, 50, 50, 0.08);
89
90 /* Radius lives in geometry.css now, generated from makeover-geometry:
91 --radius-square / -fine / -control / -panel / -round. Containers are
92 square and controls keep a corner, so the rounding survives only where a
93 thing is meant to be pressed. */
94
95 /* Syntax highlighting for the git source browser: base16 Tomorrow, used
96 whole rather than assembled colour by colour. It stays fixed across all
97 31 themes on purpose, the same argument as the diff add/del pair above:
98 a reader recognises a highlighting palette, and re-tinting it per theme
99 costs that recognition to gain nothing. Named so it is legible as one
100 palette instead of seven hex literals scattered through the git rules. */
101 --syntax-keyword: #8959a8;
102 --syntax-string: #718c00;
103 --syntax-comment: #8e908c;
104 --syntax-constant: #f5871f;
105 --syntax-entity: #4271ae;
106 --syntax-variable: #c82829;
107 --syntax-support: #3e999f;
108
109 /* Stacking order, named for what floats over what. Local rather than
110 generated: this is a claim about MNW's own furniture, not an invariant
111 like spacing. The numbers are spaced so a value can be slotted between
112 two of them without renumbering, and nothing outside this block should
113 write a z-index literal. Anything not listed here does not float. */
114 --z-raised: 1; /* lifted within the flow: a saved badge on a card */
115 --z-nav: 10; /* the nav panel that drops out of the mobile header */
116 --z-header: 11; /* the header's own toggle and search panel, both of
117 which have to stay above the panel they open */
118 --z-dropdown: 20; /* menus and suggestion lists opened from a control */
119 --z-picker: 30; /* pickers that open over a dropdown */
120 --z-context: 40; /* right-click / overflow context menus */
121 --z-overlay: 50; /* full-page scrims */
122 --z-modal: 60; /* dialogs, above their scrim */
123 --z-toast: 70; /* transient messages, above a dialog */
124 --z-banner: 80; /* page-level interruptions, above everything */
125 --z-skip-link: 90; /* the keyboard escape hatch outranks all of it */
126
127 /* Depth, and there are only two kinds of it.
128
129 A surface IN the page takes a bevel: an edge, not a distance. That is
130 --shadow-raised/-card/-inset, aliasing the pair layout.css derives from
131 each theme's own raised surface. --shadow-raised and --shadow-card
132 collapse onto one value because an edge has no heavier and lighter
133 version to pick between.
134
135 A surface floating OVER the page takes --elevation-overlay, also from
136 layout.css, derived from the theme's page colour. Menus, toasts,
137 popovers, dropdowns and pickers float. A card, a plate and a framed
138 image do not; they are .raised.
139
140 There used to be a third kind, --shadow-1/-2/-3, a blurred scale of
141 rgba-black literals picked against parchment and then applied to all 31
142 themes a creator can select. It was the same defect the bevel trio was
143 rewritten to undo, one scale later, and the elevation intent is what
144 replaced it. No literal survives here now, so every consumer re-themes
145 without being opened. Geometry stays consumer-side: a site that needs a
146 different blur composes it from var(--elevation), never from a literal. */
147 --shadow-raised: var(--bevel-raised);
148 --shadow-card: var(--bevel-raised);
149 --shadow-inset: var(--bevel-inset);
150 }
151
152 /* Reset */
153 * {
154 margin: 0;
155 padding: 0;
156 box-sizing: border-box;
157 }
158
159 /* Base styles */
160 body {
161 margin: 0;
162 background-color: var(--surface-page);
163 color: var(--content);
164 font-family: var(--font-sans);
165 line-height: 1.6;
166 }
167
168 /* Padded page layout for most content pages */
169 .padded-page {
170 padding: var(--gap-pane);
171 }
172
173 /* Centered page layout for landing, login, signup */
174 .centered-page {
175 display: flex;
176 flex-direction: column;
177 justify-content: center;
178 align-items: center;
179 min-height: 100vh;
180 gap: var(--gap-page);
181 }
182
183 .centered-page h1 {
184 font-size: var(--text-hero);
185 }
186
187 .message-container {
188 max-width: 500px;
189 text-align: center;
190 }
191
192 .centered-wrapper {
193 justify-content: center;
194 text-align: center;
195 }
196
197 h1 {
198 font-family: var(--font-display);
199 font-weight: normal;
200 color: var(--content);
201 text-align: center;
202 font-size: var(--text-display);
203 }
204
205 h2,
206 h3 {
207 font-family: var(--font-mono);
208 font-weight: normal;
209 color: var(--content);
210 }
211
212 /* Heading classes (charter: docs/design-system.md, every h1/h2 in a
213 template must carry one of these so the role is explicit).
214
215 .brand-h1: the "Makenot.work" wordmark on auth/wizard pages.
216 .page-title: page-level h1 (Young Serif, centered).
217 .subtitle-h2: page subtitle h2 used under .brand-h1 in auth/wizards.
218 .subsection-title: h2 inside dashboards, tabs, and prose partials
219 (mono, no border, the default-h2 role made explicit).
220 .section-header: h2 prose sub-section heading with bottom border
221 (existing rule, see SECTIONS block). */
222 .brand-h1,
223 .page-title {
224 font-family: var(--font-display);
225 font-weight: normal;
226 color: var(--content);
227 text-align: center;
228 font-size: var(--text-display);
229 }
230
231 .subtitle-h2 {
232 font-family: var(--font-mono);
233 font-weight: normal;
234 color: var(--content);
235 text-align: center;
236 font-size: var(--text-head);
237 }
238
239 .subsection-title {
240 font-family: var(--font-mono);
241 font-weight: normal;
242 color: var(--content);
243 }
244
245 p {
246 font-family: var(--font-sans);
247 color: var(--content);
248 }
249
250 a {
251 color: var(--content);
252 text-decoration: none;
253 }
254
255 a:hover {
256 text-decoration: underline;
257 }
258
259 /* The signature dot */
260 .dot {
261 color: var(--action);
262 }
263
264 /* Container, default 1200px max-width. Modifier classes for narrower pages,
265 or comma-grouped page-scoped overrides below for body-class-based control. */
266 .container {
267 max-width: 1200px;
268 margin: 0 auto;
269 padding: var(--gap-page);
270 }
271
272 /* Per-page container width overrides (selected via body class). One rule
273 per width tier; new pages should add their body class here rather than
274 defining a new .foo-page .container rule. */
275 .delete-account-page .container,
276 .receipt-page .container { max-width: 600px; margin: 0 auto; }
277
278 /* The profile is described, so it has no container: the measure lands on the
279 element the renderer draws the page into. The width is this page's own and
280 the rest of the block was the design system's. */
281 .user-page main { max-width: 600px; }
282
283 .import-page .container,
284 .export-page .container { max-width: 800px; margin: 0 auto; }
285
286 .fan-plus-page main,
287 .creators-page main { max-width: 900px; margin: 0 auto; }
288
289 /* READING COLUMN, the 680px measure every long-form surface uses. Narrower
290 than .container and with tighter horizontal padding, so it is its own
291 primitive rather than a .container width tier.
292
293 These four wrappers were four identical rules until 2026-08-16. They are
294 grouped rather than replaced by one class because the charter treats a new
295 class as a smell, and because grouping keeps each selector's own
296 specificity intact. A fifth long-form surface joins this list; it does not
297 copy the recipe.
298
299 .media-container's rule used to live in media-player.css. It is here now so
300 the measure has one definition; that sheet keeps its descendant rules. */
301 .article-page .article-container,
302 .doc-container,
303 .docs-index,
304 .media-container {
305 max-width: 680px;
306 margin: 0 auto;
307 padding: var(--gap-page) var(--gap-pane) var(--gap-page);
308 }
309
310 /* ===========================================
311 BUTTONS
312 =========================================== */
313
314 button {
315 color: var(--content);
316 background: var(--surface-overlay);
317 padding: 6px 12px;
318 border: 1px solid var(--content);
319 font-family: inherit;
320 cursor: pointer;
321 box-shadow: var(--shadow-raised);
322 transition: box-shadow 0.1s ease, background 0.2s ease;
323 }
324
325 button:hover {
326 background: var(--hover-surface);
327 }
328
329 button:active {
330 box-shadow: var(--shadow-inset);
331 }
332
333 /* A described act carries the generated `.button` class, and the element rule
334 above sets the same surface, bevel and tone the generated class does. This
335 file is in @layer components, which the order statement puts after
336 `makeover`, so the element rule wins whatever the specificity. Without the
337 handoff below a described act renders in --surface-overlay with
338 --shadow-raised and, worse, in --content rather than its tone: a
339 destructive act looks identical to an ordinary one. Same mechanism and same
340 remedy as `.tab.chosen` further down; read the comment there.
341
342 The split is deliberate. The app keeps padding, border and font from the
343 element rule, makeover keeps surface, bevel and tone. That is the
344 arrangement that already makes `.field` work, where the app's input rule
345 happens to set only geometry and typography.
346
347 Tone is handed back on `[data-tone]` rather than on `.button`, because an
348 untoned button has no colour in the layer to roll back to and would land on
349 the UA default instead of --content. */
350 .button,
351 .chip,
352 .segment,
353 .toggle {
354 /* respec-ok: a layer handoff, not a value. */
355 background: revert-layer;
356 box-shadow: revert-layer;
357 }
358
359 .button:hover,
360 .chip:hover,
361 .segment:hover,
362 .toggle:hover {
363 /* respec-ok: a layer handoff, not a value. */
364 background: revert-layer;
365 }
366
367 .button:active,
368 .chip:active,
369 .segment:active,
370 .toggle:active {
371 /* respec-ok: a layer handoff, not a value. */
372 background: revert-layer;
373 box-shadow: revert-layer;
374 }
375
376 .button[data-tone] {
377 /* respec-ok: a layer handoff, not a value. */
378 color: revert-layer;
379 }
380
381 /* A described link, same handoff on the `a` and `button` element rules.
382 `a { color: var(--content); text-decoration: none }` renders a described
383 link as body text -- the reader cannot tell it is a link at all, which is
384 the same defect as the lost tone above and worse for being silent.
385
386 `button.link` is the second arm: a link that posts is a button in the
387 markup, and makeover already flattens it (`background: none`, no border, no
388 padding, `font: inherit`). The element rule's chrome beat all four, so it
389 rendered as an ordinary raised button. Only the properties makeover sets on
390 this class are handed back; `font` is handed back on the button arm alone,
391 because an `a.link` has no lower-layer font to roll back to and would land
392 on the UA default face. */
393 .link {
394 /* respec-ok: a layer handoff, not a value. */
395 color: revert-layer;
396 text-decoration: revert-layer;
397 }
398
399 button.link {
400 /* respec-ok: a layer handoff, not a value. */
401 background: revert-layer;
402 border: revert-layer;
403 padding: revert-layer;
404 font: revert-layer;
405 box-shadow: revert-layer;
406 cursor: revert-layer;
407 }
408
409 /* A described facet value, the third arm of the same handoff. makeover 0.67.0
410 names the element-chrome reset, and a facet control withdraws the button's
411 fill, stroke and bevel with it: a value reads flat until it is hovered,
412 pressed or in force. The element rule above supplies all three, so without
413 this a described facet renders as an ordinary raised button and
414 `[aria-pressed="true"]` is its only correct state -- which is the whole of
415 what the panel had to say and the one thing it could not.
416
417 Wider than the `.button` group above by one property. That group keeps the
418 app's border deliberately, because a described act is still a button. Here
419 the flat rest look IS the withdrawal of the border, so it is handed back
420 with the other two. */
421 .facet-take,
422 .facet-prune {
423 /* respec-ok: a layer handoff, not a value. */
424 background: revert-layer;
425 border: revert-layer;
426 box-shadow: revert-layer;
427 }
428
429 .facet-take:hover,
430 .facet-prune:hover {
431 /* respec-ok: a layer handoff, not a value. */
432 background: revert-layer;
433 }
434
435 .facet-take:active,
436 .facet-prune:active,
437 .facet-take[aria-pressed="true"],
438 .facet-prune[aria-pressed="true"] {
439 /* respec-ok: a layer handoff, not a value. */
440 background: revert-layer;
441 box-shadow: revert-layer;
442 }
443
444 /* A described field on a `select` or a `textarea`. The input rule further down
445 sets padding and font only, which is why the shipped `.field` precedents
446 survived; these two also set `background` and `box-shadow`, so a described
447 field on either renders in --surface-sunken rather than the well. The focus
448 ring is handed back with them: makeover insets it (`calc(-1 * 2px)`) so it
449 does not collide with the bevel, and the app's outer 2px was winning. */
450 .field {
451 /* respec-ok: a layer handoff, not a value. */
452 background: revert-layer;
453 box-shadow: revert-layer;
454 }
455
456 .field:focus-visible {
457 /* respec-ok: a layer handoff, not a value. */
458 outline-offset: revert-layer;
459 }
460
461 .btn-primary {
462 background: var(--primary-dark);
463 color: var(--primary-light);
464 border: none;
465 padding: var(--gap-section) var(--gap-pane);
466 font-family: inherit;
467 font-size: var(--text-body);
468 cursor: pointer;
469 box-shadow: var(--shadow-raised);
470 transition: box-shadow 0.1s ease, background 0.2s ease;
471 display: inline-block;
472 text-decoration: none;
473 }
474
475 /* A solid-fill button shifts its own fill rather than taking --hover-surface.
476 The charter's hover rule is "one step lighter than the resting fill", and on
477 a neutral control that step is --hover-surface; on this one it is not, since
478 the label is --primary-light and a parchment fill under it is unreadable. */
479 .btn-primary:hover {
480 background: color-mix(in oklch, var(--primary-dark) 85%, var(--primary-light));
481 text-decoration: none;
482 }
483
484 .btn-primary:active {
485 box-shadow: var(--shadow-inset);
486 }
487
488 .btn-secondary {
489 background: var(--surface-sunken);
490 color: var(--content);
491 border: none;
492 padding: var(--gap-section) var(--gap-pane);
493 font-family: var(--font-mono);
494 font-size: var(--text-body);
495 cursor: pointer;
496 box-shadow: var(--shadow-raised);
497 transition: box-shadow 0.1s ease, background 0.2s ease;
498 display: inline-block;
499 text-decoration: none;
500 }
501
502 .btn-secondary:hover {
503 background: var(--hover-surface);
504 text-decoration: none;
505 }
506
507 .btn-secondary:active {
508 box-shadow: var(--shadow-inset);
509 }
510
511 .btn-danger {
512 background: var(--danger);
513 color: var(--primary-light);
514 border: none;
515 padding: var(--gap-section) var(--gap-pane);
516 font-family: var(--font-mono);
517 font-size: var(--text-body);
518 cursor: pointer;
519 box-shadow: var(--shadow-raised);
520 transition: box-shadow 0.1s ease, background 0.2s ease;
521 display: inline-block;
522 text-decoration: none;
523 }
524
525 /* Solid fill, so the same exception as .btn-primary applies. */
526 .btn-danger:hover {
527 background: color-mix(in oklch, var(--danger) 85%, var(--primary-light));
528 text-decoration: none;
529 }
530
531 .btn-danger:active {
532 box-shadow: var(--shadow-inset);
533 }
534
535 /* Disabled state for all button variants (charter rule).
536 Applies whether disabled via attribute or aria-disabled. */
537 button:disabled,
538 button[aria-disabled="true"],
539 .btn-primary:disabled,
540 .btn-secondary:disabled,
541 .btn-danger:disabled {
542 opacity: 0.5;
543 cursor: not-allowed;
544 box-shadow: none;
545 transform: none;
546 }
547
548 /* Button size + style modifiers (charter: docs/design-system.md).
549 Use these on top of .btn-primary / -secondary / -danger.
550
551 .btn--large: large CTA padding bump.
552 .btn--icon: square icon-only / micro button (small padding,
553 tight line-height).
554 .btn--link: visually a link, semantically a button (no bg/
555 border, opacity-fade hover, mono).
556
557 Some surfaces have tuned button variants that are NOT compositions
558 of these modifiers and keep their own classes:
559 .big-button: Young Serif 200×60 anchor on splash pages.
560 .order-btn: list reorder up/down (with active flash).
561 .play-button: circular media-player play control.
562 .speed-button: segment in media-player speed group.
563 .shortcuts-help-btn: 1.5rem square help glyph in toolbars.
564 .toast-retry-btn: inline bordered button inside a toast,
565 uses currentColor to inherit toast tone.
566 These are documented variants, not deprecation targets. */
567 .btn--large {
568 padding: var(--gap-section) var(--gap-page);
569 font-size: var(--text-lead);
570 }
571
572 .btn--icon {
573 padding: var(--gap-bound) var(--gap-peer);
574 font-size: var(--text-note);
575 line-height: 1;
576 min-width: 1.75rem;
577 }
578
579 .btn--link {
580 background: none;
581 color: var(--content);
582 border: none;
583 padding: 0;
584 font-family: var(--font-mono);
585 font-size: var(--text-body);
586 cursor: pointer;
587 /* Link semantics, flat, no depth. Overrides the shadow that would
588 otherwise cascade from `button` / `form button` when a .btn--link
589 happens to be a <button> inside a <form> (e.g. logout). */
590 box-shadow: none;
591 transition: opacity 0.2s ease;
592 }
593
594 .btn--link:hover {
595 opacity: 0.6;
596 box-shadow: none;
597 }
598
599 .btn--link:active {
600 box-shadow: var(--shadow-inset);
601 }
602
603 /* Menu / disclosure openers, buttons whose job is to reveal a menu,
604 filter panel, expander, or popover. They sit flat rather than raised.
605 Add new opener selectors here as they appear. */
606 .context-menu-btn,
607 .discover-filter-toggle,
608 .bundle-toggle {
609 box-shadow: none;
610 }
611
612 .context-menu-btn:hover,
613 .discover-filter-toggle:hover,
614 .bundle-toggle:hover {
615 box-shadow: none;
616 }
617
618 .context-menu-btn:active,
619 .discover-filter-toggle:active,
620 .bundle-toggle:active {
621 box-shadow: var(--shadow-inset);
622 }
623
624 /* ===========================================
625 FORMS
626 =========================================== */
627
628 form {
629 display: flex;
630 flex-direction: column;
631 gap: var(--gap-section);
632 width: 100%;
633 }
634
635 .form-container {
636 max-width: 400px;
637 background: var(--surface-overlay);
638 padding: var(--gap-page);
639 }
640
641 label {
642 font-family: var(--font-mono);
643 color: var(--content);
644 font-size: var(--text-note);
645 }
646
647 input[type="text"],
648 input[type="email"],
649 input[type="password"],
650 input[type="number"],
651 input[type="date"],
652 textarea,
653 select {
654 width: 100%;
655 background: var(--surface-sunken);
656 border: 1px solid var(--border);
657 padding: var(--gap-section);
658 font-family: var(--font-mono);
659 font-size: var(--text-note);
660 color: var(--content);
661 box-shadow: var(--shadow-inset);
662 }
663
664 input::placeholder,
665 textarea::placeholder {
666 opacity: 0.5;
667 }
668
669 input[type="text"]:focus,
670 input[type="email"]:focus,
671 input[type="password"]:focus,
672 input[type="number"]:focus,
673 textarea:focus,
674 select:focus {
675 outline: none;
676 border: 1px solid var(--action);
677 }
678
679 textarea {
680 min-height: 100px;
681 resize: vertical;
682 }
683
684 select {
685 cursor: pointer;
686 }
687
688 input[type="submit"],
689 form button {
690 font-family: var(--font-display);
691 font-size: var(--text-body);
692 color: var(--content);
693 background: var(--surface-overlay);
694 padding: 12px 24px;
695 border: 1px solid var(--content);
696 cursor: pointer;
697 box-shadow: var(--shadow-raised);
698 transition: box-shadow 0.1s ease, background 0.2s ease;
699 }
700
701 input[type="submit"]:hover,
702 form button:hover {
703 background: var(--hover-surface);
704 }
705
706 input[type="submit"]:active,
707 form button:active {
708 box-shadow: var(--shadow-inset);
709 }
710
711 .form-group {
712 margin-bottom: var(--gap-pane);
713 }
714
715 .form-group label {
716 display: block;
717 margin-bottom: var(--gap-peer);
718 font-size: var(--text-note);
719 }
720
721 .form-group input,
722 .form-group textarea,
723 .form-group select {
724 width: 100%;
725 }
726
727 .form-row {
728 display: grid;
729 grid-template-columns: 1fr 1fr;
730 gap: var(--gap-section);
731 }
732
733 .hint {
734 font-size: var(--text-note);
735 opacity: 0.6;
736 margin-top: var(--gap-peer);
737 }
738
739 /* Field-level validation error, paired with .form-group--error.
740 Canonical: `partials/_ui.html` ui::form_field macro. */
741 .form-group--error label {
742 color: var(--danger);
743 }
744
745 .form-group--error input,
746 .form-group--error textarea,
747 .form-group--error select {
748 border-color: var(--danger);
749 }
750
751 .field-error {
752 font-size: var(--text-note);
753 color: var(--danger);
754 margin-top: var(--gap-peer);
755 }
756
757 /* Checkbox group */
758 .checkbox-group {
759 display: flex;
760 align-items: start;
761 gap: var(--gap-section);
762 cursor: pointer;
763 transition: background 0.2s ease;
764 margin-bottom: var(--gap-peer);
765 }
766
767 .checkbox-group input[type="checkbox"] {
768 width: auto;
769 margin-top: var(--gap-bound);
770 margin-bottom: var(--gap-bound);
771 cursor: pointer;
772 }
773 /* Radio group */
774 .radio-group {
775 display: flex;
776 flex-direction: column;
777 gap: var(--gap-section);
778 margin-top: var(--gap-peer);
779 }
780 /* ===========================================
781 TABS
782 =========================================== */
783
784 .tabs {
785 display: flex;
786 flex-wrap: nowrap;
787 gap: 0;
788 margin-bottom: 0;
789 }
790
791 /* Geometry and type only. The surface is layout.css's: `.tab` is sunken,
792 `:hover` steps to --hover-surface on a fine pointer, `:active` insets. This
793 rule used to restate the sunken background verbatim and then carry a second,
794 redundant selection signal on top of it — opacity 0.6 unselected, 1 selected
795 — so a tab said "not chosen" twice and the generated surface step did
796 nothing. The opacity idiom is gone and the surface contrast is the signal. */
797 .tab {
798 color: var(--content);
799 border: none;
800 padding: var(--gap-section) var(--gap-page);
801 font-family: var(--font-mono);
802 font-size: var(--text-body);
803 cursor: pointer;
804 /* Tabs navigate, they don't commit, stay flat. Overrides the base
805 `button` shadow if a tab is a <button>. */
806 box-shadow: none;
807 transition: background 0.2s ease;
808 white-space: nowrap;
809 flex-shrink: 0;
810 }
811
812 /* A tab's selected state is `.tab.chosen` in layout.css and nowhere else now.
813 This file used to carry a `.tab.is-selected` rule setting the same two
814 declarations byte for byte, on the argument that `.is-selected` is the
815 site's own idiom across git nav links, editor tabs, section tabs, issue tabs
816 and filter buttons, so renaming the tabs alone would split it. That is true
817 and it is still the wrong trade: a second name for a state the design system
818 already names is invisible only while both say the same thing, and stops
819 being invisible the moment makeover changes what a chosen tab looks like.
820 The other components keep `.is-selected`, because makeover has no primitive
821 for a git nav link; the tab has one.
822
823 The chosen arm's box-shadow comes back through the layer rather than being
824 restated. `.tab` below zeroes the base `button` shadow, and this file is in
825 @layer components, which the order statement puts after `makeover`, so it
826 outranks it whatever the specificity: without the revert, the generated
827 bevel never applies and a chosen tab reads flat.
828 `revert-layer` hands the property back instead of naming a value, which is
829 the difference between deferring to the design system and copying it. */
830 .tab.chosen {
831 /* respec-ok: a layer handoff, not a value. See the comment above. */
832 box-shadow: revert-layer;
833 }
834
835 .tab-more-wrap {
836 position: relative;
837 flex-shrink: 0;
838 }
839
840 .tab-overflow-menu {
841 position: absolute;
842 top: 100%;
843 right: 0;
844 z-index: var(--z-dropdown);
845 background: var(--surface-page);
846 border: 1px solid var(--border);
847 min-width: 180px;
848 box-shadow: var(--elevation-overlay);
849 }
850
851 /* The overflow menu restates the tab as a menu row: block, full width, left
852 aligned, tighter. It carried the same opacity pair the strip did and it goes
853 for the same reason; hover and selection are the generated surface steps. */
854 .tab-overflow-menu .tab {
855 display: block;
856 width: 100%;
857 text-align: left;
858 padding: var(--gap-group) var(--gap-section);
859 }
860
861
862 .tab-content {
863 display: none;
864 background: var(--surface-overlay);
865 padding: var(--gap-page);
866 }
867
868 .tab-content.active {
869 display: block;
870 }
871
872 /* ===========================================
873 TABLES
874 =========================================== */
875
876 .data-table {
877 width: 100%;
878 border-collapse: collapse;
879 font-size: var(--text-note);
880 font-family: var(--font-sans);
881 margin-bottom: var(--gap-section);
882 }
883
884 .data-table thead {
885 background: var(--surface-sunken);
886 }
887
888 .data-table th {
889 text-align: left;
890 padding: var(--gap-section);
891 font-weight: normal;
892 }
893
894 /* `.data-table th.sortable` and its four rules were here and had no consumer:
895 nothing in templates/ ever put `sortable` on a `th`. The one sortable table
896 in the app is templates/partials/tabs/project_content.html, which used
897 `.sortable-th` further down this file. Both are gone; the affordance and the
898 caret are `.table-heading[data-sortable]` in the generated static/layout.css.
899 See the block that replaced `.sortable-th` for what stayed behind. */
900
901 .data-table tbody tr {
902 transition: background 0.1s ease;
903 }
904
905 /* No zebra. There were two rules here, :nth-child(odd) and (even), and both
906 set --surface-overlay — the same value, so the stripe never existed and the
907 effect was every row painted the panel's own colour. The table is a .well
908 now and the rows sit on it unpainted, which is what the banding was
909 reaching for. makeover derives no row-stripe intent, so if one is ever
910 wanted it is a makeover-side question, not a literal here. */
911
912 .data-table tbody tr:hover {
913 background: var(--surface-sunken);
914 }
915
916 .data-table td {
917 padding: var(--gap-section);
918 border-top: 1px solid var(--border);
919 }
920
921 /* Marks the row a table is about, used across the dashboard and library
922 tables. Lived in the pricing block until the fee calculator replaced it. */
923 .data-table .highlight {
924 background: var(--surface-overlay);
925 border-left: 3px solid var(--action);
926 }
927
928 /* ===========================================
929 COMPACT TABLES (dashboard/admin, lighter than .data-table)
930 =========================================== */
931
932 .compact-table {
933 width: 100%;
934 border-collapse: collapse;
935 }
936
937 .compact-table thead tr {
938 font-size: var(--text-note);
939 opacity: 0.7;
940 }
941
942 .compact-table th {
943 padding: var(--gap-peer) var(--gap-section);
944 text-align: left;
945 }
946
947 .compact-table td {
948 padding: var(--gap-section);
949 }
950
951 .compact-table tbody tr {
952 border-bottom: 1px solid var(--border);
953 }
954
955 /* ===========================================
956 UTILITIES
957 =========================================== */
958
959 .text-sm { font-size: var(--text-note); }
960 .text-xs { font-size: var(--text-fine); }
961 .muted { opacity: 0.7; }
962 .dimmed { opacity: 0.6; }
963 .dimmer { opacity: 0.5; }
964 .meta { font-size: var(--text-note); opacity: 0.7; }
965 .nowrap { white-space: nowrap; }
966 .scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }
967 .text-center { text-align: center; }
968 .text-right { text-align: right; }
969 .fw-bold { font-weight: bold; }
970 .unstyled-link { text-decoration: none; color: inherit; }
971 .square-cover { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; }
972
973 /* Spacing utilities.
974 mt = margin-top, mb = margin-bottom, my = vertical, py = vertical padding.
975 Named for the relationship, same vocabulary as the --gap-* tokens they
976 read. Use sparingly; prefer parent layout primitives. */
977 .m-0 { margin: 0; }
978 .mt-0 { margin-top: 0; }
979 .mb-0 { margin-bottom: 0; }
980 .mt-peer { margin-top: var(--gap-peer); }
981 .mt-section { margin-top: var(--gap-section); }
982 .mt-pane { margin-top: var(--gap-pane); }
983 .mt-page { margin-top: var(--gap-page); }
984 .mb-peer { margin-bottom: var(--gap-peer); }
985 .mb-section { margin-bottom: var(--gap-section); }
986 .mb-pane { margin-bottom: var(--gap-pane); }
987 .my-peer { margin-block: var(--gap-peer); }
988 .my-section { margin-block: var(--gap-section); }
989 .py-section { padding-block: var(--gap-section); }
990
991 /* Min-width utilities for tables inside .scroll-x wrappers. */
992 .minw-300 { min-width: 300px; }
993 .minw-400 { min-width: 400px; }
994 .minw-500 { min-width: 500px; }
995 .minw-600 { min-width: 600px; }
996 .minw-700 { min-width: 700px; }
997 .minw-800 { min-width: 800px; }
998
999 /* Compact button sizes. Canonical: .small (broad use, 0.25rem 0.6rem 0.8rem).
1000 The tiny / row-btn / cart-row-btn aliases are visually equivalent, within
1001 sub-pixel of each other in the original templates, and folded into .small. */
1002 .small { padding: var(--gap-bound) var(--gap-group); font-size: var(--text-fine); }
1003
1004 /* Canonical empty-state primitive. Use modifiers for tight/sized contexts.
1005 Markup: <div class="empty-state">…</div> (optionally with .empty-state--compact
1006 inside dropdowns or .empty-state--chart inside a fixed-height chart slot). */
1007 .empty-state { text-align: center; padding: var(--gap-page); opacity: 0.6; }
1008 .empty-state--compact { padding: var(--gap-section); font-size: var(--text-note); }
1009 .empty-state--chart { height: 200px; padding: 0; display: flex; align-items: center; justify-content: center; opacity: 0.5; }
1010 .empty-state-hint { font-family: var(--font-mono); font-size: var(--text-fine); margin-top: var(--gap-peer); opacity: 0.6; }
1011
1012 .tab-docs {
1013 display: flex;
1014 justify-content: flex-end;
1015 margin-bottom: var(--gap-section);
1016 }
1017
1018 .tab-docs a {
1019 font-family: var(--font-mono);
1020 font-size: var(--text-fine);
1021 opacity: 0.5;
1022 transition: opacity 0.2s ease;
1023 }
1024
1025 .tab-docs a:hover {
1026 opacity: 1;
1027 }
1028
1029 /* ===========================================
1030 BADGES
1031 =========================================== */
1032
1033 /* Geometry and type only. The colour is layout.css's: a badge is coloured
1034 text on the page, and its status colour comes from `data-tone` — see
1035 src/types/badge.rs, which maps every status enum onto a tone name.
1036
1037 This used to be a filled pill (near-black ground, --primary-light label)
1038 with a local `.badge--live/pending/failed` set painting the fill per
1039 status. All of it went in wave 2 tier B2: the fill was carrying exactly
1040 what the generated `color` carries, so it was the same thing said in a
1041 different channel, and keeping it meant the tone attribute could never do
1042 anything. */
1043 .badge {
1044 display: inline-block;
1045 padding: var(--gap-bound) var(--gap-peer);
1046 font-size: var(--text-fine);
1047 font-family: var(--font-mono);
1048 }
1049
1050 /* AI disclosure tier badges. See site-docs/public/about/generative-ai.md.
1051 Colours track the brand palette: violet for the cleanest case, warm-tan for
1052 disclosed AI use, charcoal for primarily generated.
1053
1054 respec-ok: this is a taxonomy makeover has no vocabulary for. Its four
1055 tones are info / success / warning / danger, which all read as alarm, and
1056 disclosure is not alarm — a handmade item is not a "success" and a
1057 generated one is not a "danger". So the three hues stay local. They set
1058 `color` rather than a background, because the badge form is the generated
1059 one now and only the hue is ours. */
1060 .badge.ai-tier {
1061 letter-spacing: 0.02em;
1062 }
1063 .badge.ai-tier-handmade {
1064 color: var(--action);
1065 }
1066 .badge.ai-tier-assisted {
1067 color: var(--warning);
1068 }
1069 .badge.ai-tier-generated {
1070 color: var(--content);
1071 }
1072
1073 .item-ai-tier {
1074 margin: var(--gap-bound) 0 var(--gap-section);
1075 }
1076
1077 .ai-disclosure {
1078 margin: var(--gap-section) 0;
1079 padding: var(--gap-section);
1080 background: var(--surface-sunken);
1081 border-left: 3px solid var(--warning);
1082 }
1083 .ai-disclosure-label {
1084 font-family: var(--font-mono);
1085 font-size: var(--text-fine);
1086 text-transform: uppercase;
1087 letter-spacing: 0.05em;
1088 color: var(--content-muted);
1089 margin-bottom: var(--gap-bound);
1090 }
1091 .ai-disclosure-text {
1092 font-size: var(--text-note);
1093 color: var(--content);
1094 }
1095
1096 /* Discover row: plain text mention per design decision 2026-06-03:
1097 no pill in the row, so fans skim the listing without disclosure noise
1098 and use the explicit filter when they care. */
1099 .discover-row-ai-tier {
1100 font-family: var(--font-mono);
1101 font-size: var(--text-fine);
1102 color: var(--content-muted);
1103 }
1104
1105 /* ===========================================
1106 TAGS
1107 =========================================== */
1108
1109 .tag {
1110 display: inline-block;
1111 background: var(--primary-dark);
1112 color: var(--primary-light);
1113 padding: var(--gap-bound) var(--gap-peer);
1114 font-size: var(--text-fine);
1115 margin-right: var(--gap-bound);
1116 }
1117
1118 .tag-input {
1119 background: var(--surface-sunken);
1120 padding: var(--gap-peer);
1121 margin-bottom: var(--gap-peer);
1122 display: flex;
1123 flex-wrap: wrap;
1124 gap: var(--gap-peer);
1125 min-height: 40px;
1126 }
1127
1128 .tag-input .tag {
1129 display: flex;
1130 align-items: center;
1131 gap: var(--gap-peer);
1132 padding: var(--gap-bound) var(--gap-peer);
1133 font-size: var(--text-note);
1134 }
1135
1136 .tag-input .tag button {
1137 background: none;
1138 border: none;
1139 color: var(--primary-light);
1140 cursor: pointer;
1141 padding: 0;
1142 font-size: var(--text-body);
1143 }
1144
1145 /* ===========================================
1146 CARDS
1147 =========================================== */
1148
1149 /* Canonical card primitive (charter: docs/design-system.md).
1150 Variants:
1151 .card: filled, hover-step (default content card).
1152 .card.card-muted: surface-muted fill, no hover (dashboard stat / analytics blocks).
1153 .card.card--bordered: bordered, padded, no fill (marketing cards: tier, fork).
1154 .card.card--selectable: radio-card pattern; pair with .is-selected.
1155 .card.card--grid: grid-cell card (discover grid).
1156 No aliases. A template names the modifier it wants and adds its own
1157 component class beside it; a recipe reached only through a per-page name is
1158 a recipe nobody can find from the charter. */
1159 /* Fill and depth come from layout.css, generated by makeover-webview: the card
1160 is `--surface-raised` plus `--bevel-raised`, and `:hover` goes to
1161 `--hover-surface` on a fine pointer only. Declaring either here would take
1162 them back, since style.css is in @layer components, which the order
1163 statement puts after `makeover`, whatever the specificity. The box-shadow this used to carry was
1164 `--shadow-card`, which already aliases `--bevel-raised`, so only the fill
1165 actually moved: `--surface-overlay` to `--surface-raised`. Those are equal on
1166 the platform default and a theme may set them apart. */
1167 .card {
1168 padding: var(--gap-pane);
1169 margin-bottom: var(--gap-section);
1170 text-decoration: none;
1171 color: var(--content);
1172 display: block;
1173 border: 1px solid var(--border);
1174 transition: background 0.2s ease;
1175 }
1176
1177 /* Muted card primitive. */
1178 .card-muted {
1179 background: var(--surface-sunken);
1180 padding: var(--gap-pane);
1181 }
1182 /* Bordered card. Templates put this modifier on the element alongside any
1183 component class of their own (.tier-card, .fork-card), so the name the
1184 charter teaches is the name in the markup. A described screen names none of
1185 them: a card there is a region, and the design system draws it. */
1186 .card--bordered {
1187 background: transparent;
1188 padding: var(--gap-section);
1189 border: 1px solid var(--border);
1190 margin-bottom: 0;
1191 }
1192 .fork-card { padding: var(--gap-pane); display: flex; flex-direction: column; }
1193
1194 .card-title {
1195 font-size: var(--text-subhead);
1196 margin-bottom: var(--gap-peer);
1197 font-family: var(--font-display);
1198 }
1199
1200 .card-meta {
1201 font-size: var(--text-note);
1202 color: var(--content-muted);
1203 margin-bottom: var(--gap-peer);
1204 font-family: var(--font-mono);
1205 }
1206
1207 .card-description {
1208 font-size: var(--text-note);
1209 opacity: 0.8;
1210 font-family: var(--font-sans);
1211 }
1212
1213 /* ===========================================
1214 STATS
1215 =========================================== */
1216
1217 .stats-grid {
1218 display: grid;
1219 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1220 gap: var(--gap-section);
1221 margin-bottom: var(--gap-pane);
1222 }
1223
1224 .stats-row {
1225 display: flex;
1226 gap: var(--gap-pane);
1227 margin: var(--gap-pane) 0;
1228 }
1229
1230 .stat-box {
1231 background: var(--surface-overlay);
1232 padding: var(--gap-section) var(--gap-pane);
1233 flex: 1;
1234 text-align: center;
1235 }
1236
1237 .stat-box .number {
1238 font-family: var(--font-display);
1239 font-weight: bold;
1240 font-size: var(--text-head);
1241 }
1242
1243 .stat-box .label {
1244 font-size: var(--text-fine);
1245 opacity: 0.7;
1246 }
1247
1248 /* .stat-card base recipe defined above with .card-muted */
1249
1250 .stat-label {
1251 font-size: var(--text-note);
1252 opacity: 0.7;
1253 margin-bottom: var(--gap-bound);
1254 }
1255
1256 .stat-value {
1257 font-size: var(--text-head);
1258 }
1259
1260 .stat-change {
1261 font-size: var(--text-note);
1262 opacity: 0.6;
1263 }
1264
1265 .stat-change.positive {
1266 color: var(--success);
1267 }
1268
1269 .stat-change.negative {
1270 color: var(--danger);
1271 }
1272
1273 /* ===========================================
1274 FILTER BAR
1275 =========================================== */
1276
1277 .filter-bar {
1278 display: flex;
1279 gap: var(--gap-peer);
1280 margin: var(--gap-pane) 0;
1281 flex-wrap: wrap;
1282 align-items: center;
1283 }
1284
1285 .filter-bar > .label {
1286 font-size: var(--text-note);
1287 opacity: 0.7;
1288 margin-right: var(--gap-peer);
1289 }
1290
1291 .filter-bar button {
1292 padding: var(--gap-bound) var(--gap-section);
1293 font-size: var(--text-note);
1294 }
1295
1296 /* ===========================================
1297 DISCUSSION SECTION
1298 =========================================== */
1299
1300 /* Sits inside the parent container; previously had max-width: 720px and
1301 margin: 2rem auto, which looked indented on wide pages (.library-page,
1302 .item-page) where every neighbouring card filled the container. Let the
1303 parent dictate width, article-page already constrains via
1304 .article-container; player pages have their own narrow layout. */
1305 .discussion-section {
1306 margin: var(--gap-page) 0;
1307 padding: var(--gap-pane);
1308 border: 1px solid var(--border);
1309 font-family: var(--font-mono);
1310 }
1311
1312 .discussion-section h3 {
1313 font-family: var(--font-display);
1314 font-size: var(--text-subhead);
1315 margin: 0 0 var(--gap-peer);
1316 }
1317
1318 .discussion-section p {
1319 margin: 0 0 var(--gap-section);
1320 color: var(--content-muted);
1321 font-size: var(--text-note);
1322 }
1323
1324 .discussion-section a {
1325 color: var(--action);
1326 text-decoration: none;
1327 font-size: var(--text-note);
1328 }
1329
1330 /* ===========================================
1331 NOTIFICATION SURFACES (info-box, warning-box, error-message)
1332 =========================================== */
1333
1334 /* Notification surface roles (charter: docs/design-system.md).
1335 Five distinct components. Keep them distinct, don't merge:
1336 .toast: transient, JS-dismissible, bottom-right corner.
1337 .banner: full-bleed page-top notice (sandbox, restart,
1338 founder pricing). One short sentence + optional link.
1339 .alert: inline directive callout with uppercase mono title
1340 (NOTE / TIP / IMPORTANT / WARNING / CAUTION). Used in
1341 docs and longer-form bodies.
1342 .info-box: informational headed block with h3 + bullet list
1343 inside forms / wizards (distinct register from .alert).
1344 .warning-box: loud yellow-on-yellow attention-grabber for
1345 page-level warnings (delete confirms, account
1346 warnings). Louder than .alert-warning intentionally.
1347 .error-message: inline form-field error; hidden by default, shown
1348 with .is-active. Distinct from page-level .alert. */
1349 .info-box {
1350 background: var(--surface-sunken);
1351 padding: var(--gap-section);
1352 margin-bottom: var(--gap-section);
1353 font-size: var(--text-note);
1354 font-family: var(--font-sans);
1355 }
1356
1357 .info-box h3 {
1358 font-size: var(--text-body);
1359 font-weight: normal;
1360 margin-bottom: var(--gap-peer);
1361 }
1362
1363 .info-box ul {
1364 list-style: none;
1365 margin-top: var(--gap-peer);
1366 }
1367
1368 .info-box li {
1369 padding: var(--gap-bound) 0;
1370 }
1371
1372 .info-box li::before {
1373 content: "- ";
1374 opacity: 0.5;
1375 }
1376
1377 .warning-box {
1378 background: var(--warning);
1379 color: var(--primary-light);
1380 padding: var(--gap-section);
1381 margin-bottom: var(--gap-section);
1382 font-size: var(--text-note);
1383 font-family: var(--font-sans);
1384 }
1385
1386 .error-message {
1387 font-family: var(--font-mono);
1388 color: var(--danger);
1389 background: color-mix(in oklch, var(--danger) 10%, var(--surface-page));
1390 padding: 12px;
1391 border: 1px solid var(--danger);
1392 border-radius: var(--radius-fine);
1393 text-align: center;
1394 font-size: var(--text-note);
1395 display: none;
1396 }
1397
1398 /* ===========================================
1399 ERROR PAGE
1400 =========================================== */
1401
1402 /* Full-page error layout (templates/pages/error.html). Distinct from
1403 the inline `.error-message` form-error class above; named to avoid
1404 collision. */
1405 .error-page {
1406 min-height: 100vh;
1407 display: flex;
1408 align-items: center;
1409 justify-content: center;
1410 padding: var(--gap-page);
1411 background: var(--surface-page);
1412 }
1413
1414 .error-container {
1415 text-align: center;
1416 max-width: 560px;
1417 }
1418
1419 .error-status {
1420 font-size: var(--text-note);
1421 font-weight: 600;
1422 letter-spacing: 0.08em;
1423 text-transform: uppercase;
1424 color: var(--content-muted);
1425 margin-bottom: var(--gap-section);
1426 }
1427
1428 .error-page-message {
1429 font-size: var(--text-head);
1430 line-height: 1.4;
1431 color: var(--content);
1432 margin-bottom: var(--gap-page);
1433 }
1434
1435 .error-actions {
1436 display: flex;
1437 gap: var(--gap-section);
1438 justify-content: center;
1439 }
1440
1441
1442 /* ===========================================
1443 BUY PAGE (standalone direct purchase)
1444 =========================================== */
1445
1446 /* Minimal direct-purchase page (templates/pages/buy.html). Standalone
1447 page used for link-in-bio sharing, no site chrome, just a card.
1448 Scoped under .buy-page so the rules don't bleed into the rest of
1449 the app. Tokenized; previously lived as a page-isolated <style>. */
1450 .buy-page {
1451 min-height: 100vh;
1452 display: flex;
1453 align-items: center;
1454 justify-content: center;
1455 padding: var(--gap-page);
1456 }
1457
1458 /* The one former --shadow-3 site that is not a float. The card is the whole
1459 page's content rather than something laid over it, so it takes .raised
1460 from layout.css for its surface and its edge, and keeps only the geometry
1461 the generated primitive does not carry. */
1462 .buy-page .buy-card {
1463 border-radius: var(--radius-panel);
1464 padding: var(--gap-page);
1465 max-width: 420px;
1466 width: 100%;
1467 }
1468
1469 .buy-page .cover,
1470 .buy-page .no-cover {
1471 width: 100%;
1472 aspect-ratio: 1;
1473 max-height: 280px;
1474 border-radius: var(--radius-panel);
1475 margin-bottom: var(--gap-pane);
1476 background: var(--surface-raised);
1477 }
1478
1479 .buy-page .cover {
1480 object-fit: cover;
1481 }
1482
1483 .buy-page .no-cover {
1484 display: flex;
1485 align-items: center;
1486 justify-content: center;
1487 font-size: var(--text-hero);
1488 opacity: 0.2;
1489 }
1490
1491 .buy-page h1 {
1492 text-align: left;
1493 font-size: var(--text-head);
1494 margin-bottom: var(--gap-bound);
1495 }
1496
1497 .buy-page .creator {
1498 font-family: var(--font-mono);
1499 font-size: var(--text-note);
1500 opacity: 0.6;
1501 margin-bottom: var(--gap-pane);
1502 }
1503
1504 .buy-page .creator a {
1505 color: inherit;
1506 }
1507
1508 .buy-page .price {
1509 font-family: var(--font-mono);
1510 font-size: var(--text-subhead);
1511 margin-bottom: var(--gap-pane);
1512 }
1513
1514 .buy-page .description {
1515 font-size: var(--text-note);
1516 line-height: 1.5;
1517 opacity: 0.8;
1518 margin-bottom: var(--gap-pane);
1519 max-height: 4.5em;
1520 overflow: hidden;
1521 }
1522
1523 .buy-page .buy-btn {
1524 display: block;
1525 width: 100%;
1526 padding: 14px;
1527 background: var(--action);
1528 color: var(--primary-light);
1529 border: none;
1530 border-radius: var(--radius-panel);
1531 font-size: var(--text-body);
1532 font-weight: 600;
1533 cursor: pointer;
1534 text-align: center;
1535 text-decoration: none;
1536 transition: opacity 0.2s ease;
1537 }
1538
1539 .buy-page .buy-btn:hover {
1540 opacity: 0.85;
1541 }
1542
1543 .buy-page .buy-btn:disabled {
1544 opacity: 0.6;
1545 cursor: wait;
1546 }
1547
1548 .buy-page .pwyw-input {
1549 display: flex;
1550 align-items: center;
1551 gap: var(--gap-bound);
1552 margin-bottom: var(--gap-section);
1553 }
1554
1555 .buy-page .pwyw-input input {
1556 width: 100px;
1557 padding: 8px;
1558 border: 1px solid var(--border);
1559 border-radius: var(--radius-control);
1560 font-size: var(--text-body);
1561 }
1562
1563 .buy-page .note {
1564 font-size: var(--text-fine);
1565 opacity: 0.5;
1566 text-align: center;
1567 margin-top: var(--gap-section);
1568 }
1569
1570 .buy-page .note a {
1571 color: var(--action);
1572 }
1573
1574 .buy-page .footer {
1575 text-align: center;
1576 margin-top: var(--gap-pane);
1577 padding-top: var(--gap-section);
1578 border-top: 1px solid var(--border);
1579 font-size: var(--text-fine);
1580 opacity: 0.5;
1581 }
1582
1583 .buy-page .footer a {
1584 color: inherit;
1585 }
1586
1587 /* ===========================================
1588 SECTIONED CONTENT (tabs + panels, shared by three pages)
1589 =========================================== */
1590
1591 /* Sectioned content (templates/pages/item.html, project.html,
1592 library_downloads.html). Tab+panel pattern shared by all three
1593 pages, kept global, NOT scoped under .item-page. */
1594 .section-tabs {
1595 display: flex;
1596 gap: 0;
1597 border-bottom: 1px solid var(--border);
1598 margin-bottom: var(--gap-pane);
1599 }
1600
1601 .section-tab {
1602 background: none;
1603 border: none;
1604 padding: var(--gap-group) var(--gap-section);
1605 cursor: pointer;
1606 font-size: var(--text-body);
1607 opacity: 0.6;
1608 border-bottom: 2px solid transparent;
1609 font-family: var(--font-sans);
1610 color: var(--content);
1611 }
1612
1613 .section-tab.is-selected {
1614 opacity: 1;
1615 border-bottom-color: var(--content);
1616 }
1617
1618 .section-tab:hover { opacity: 0.9; }
1619
1620 /* .section-panel.active is a visibility toggle (display: none / block),
1621 not a selection, kept as .active. */
1622 .section-panel { display: none; }
1623 .section-panel.active { display: block; }
1624 .section-panel p { margin-bottom: var(--gap-section); }
1625 .section-panel ul,
1626 .section-panel ol {
1627 margin-left: var(--gap-pane);
1628 margin-bottom: var(--gap-section);
1629 }
1630 .section-panel li { margin-bottom: var(--gap-peer); }
1631 .section-panel h1,
1632 .section-panel h2,
1633 .section-panel h3 {
1634 margin-top: var(--gap-section);
1635 margin-bottom: var(--gap-peer);
1636 }
1637 .section-panel pre {
1638 background: var(--surface-sunken);
1639 padding: var(--gap-section);
1640 overflow-x: auto;
1641 margin-bottom: var(--gap-section);
1642 }
1643 .section-panel code { font-size: var(--text-note); }
1644
1645