Skip to main content

max / makenotwork

310.4 KB · 11054 lines History Blame Raw
1 /* ============================================================================
2 MNW global stylesheet. Charter: docs/design-system.md.
3
4 Build new features by COMPOSING these primitives, in this order of preference:
5
6 1. Use a UTILITY class for one-off adjustments (.mb-section, .text-sm, .nowrap)
7 2. Use a LAYOUT PRIMITIVE to position content (.container, .stack-row, .field-row, .list-row)
8 3. Use a COMPONENT PRIMITIVE for a UI element (.card, .callout, .badge, .progress-bar, .empty-state, .input--sm, .small)
9 4. Extend a primitive with a modifier (.card--bordered, .callout--warning, .stack-row--bordered)
10 5. ONLY THEN consider a new class, and add it to the right section below
11
12 A new class is a smell, not a goal. Before writing one:
13 - grep this file for the visual shape you want; almost everything is here
14 - if you can't find it, consider whether the shape belongs in the design system
15 (then add it to the appropriate section here AND to docs/design-system.md)
16 - page-scoped rules (`.foo-page .bar`) are a last resort, not a first move
17
18 --------------------------------------------------------------------------
19 FILE STRUCTURE (search by section name for anchor)
20
21 FOUNDATIONS
22 @font-face declarations (top of file)
23 :root tokens (color, type, space, radius, shadow)
24 Element base (h1/h2/h3, p, a, button, input, table)
25
26 UTILITIES
27 Spacing (.m-0, .mt-*, .mb-*, .ml-*, .my-*)
28 Sizing (.col-*, .w-*, .maxw-*, .minw-*)
29 Text (.text-sm, .text-xs, .text-center, .text-right, .fw-bold,
30 .nowrap, .muted, .dimmed, .dimmer, .is-faded, .danger-text)
31 State (.hidden, .is-selected, .is-active, .unstyled-link)
32 Shapes (.square-cover, .scroll-x)
33
34 LAYOUT PRIMITIVES (page-scaffolding shapes)
35 .container (width set by body class)
36 .stack-row + --bordered / --tight / --top (toolbar / header bar)
37 .field-row + .form-group.is-grow (inline form row)
38 .list-row + .list-row-title (vertical item list)
39 .form-row (2-col grid form)
40 .cover-row + .cover-thumb + .cover-empty (image picker)
41
42 COMPONENT PRIMITIVES (reusable UI blocks)
43 .content-section (light-bg page section box)
44 .card / .card-muted / .card--bordered / .card--selectable
45 .form-group / .form-section / details.form-section
46 .section-header / .section-lead / .section-group-label
47 .badge + data-tone (success/warning/danger)
48 .callout + --danger / --warning / --solid-warning
49 .banner + --info / --warning (full-bleed page-top notice)
50 .alert + -note / -tip / -warning / -caution (left-border inline notice)
51 .modal + .modal-overlay
52 .empty-state + --compact / --chart / --lg
53 .progress-bar-container + .progress-bar (--slim, --rounded, --highlight)
54 .upload-status + -row + -msg.is-success / .is-error
55 .field-status / .save-status + .success / .error / .saving
56 .toast + .toast--success / --error / --warning
57 .breadcrumb / .pagination
58 .tabs + .tab.chosen (chosen is makeover's)
59
60 CONTROL MODIFIERS (compose onto buttons/inputs)
61 .small / .btn-compact / .btn-link / .btn-tiny (button sizes)
62 .btn-danger / .saved (button intents)
63 .input--xs / --sm / --mono / --upper / --numeric (input sizes/shapes)
64
65 FEATURE SUBAPPS (their own CSS scope)
66 .git-* (git source browser at /source/*)
67 Pricing calculator on /pricing
68 Markdown preview / docs-ui in /docs/*
69
70 PAGE-SCOPED LAYOUTS (when truly page-specific)
71 .item-page / .project-page / .article-page / .purchase-page / .cart-* /
72 .library-page / .feed-page / .discover-page / .landing / .creators-page /
73 .health-page / .import-page / .delete-account-page / .export-page /
74 .receipt-page / .fan-plus-page / .stripe-disclaimer-page / .buy-page /
75 .user-page / .collection-page / .changelog-page / .policy-page /
76 .tag-tree-page / .project-blog-page / .project-paywall-page /
77 .confirm-delete-page / .admin-page / .dashboard-page
78
79 DASHBOARD TABS (one section per tab partial)
80 Per-tab classes scoped under their canonical body class. Use modifier
81 composition where possible; only add tab-specific rules when the shape
82 genuinely diverges from a primitive.
83
84 RESPONSIVE (@media at end of file)
85 768px tablet
86 480px phone
87
88 --------------------------------------------------------------------------
89 When in doubt: read docs/design-system.md first. Update both files together
90 when you add a primitive.
91 ============================================================================ */
92
93 /* No `@font-face` here at all any more, and no font token either. All three
94 faces are fetched by the generated `typography.css`: Quasi Mono and Quasi
95 Body from makeover's house layer, and Young Serif from MNW's own override,
96 declared in build.rs. What left first: four blocks for IBM Plex Mono and
97 Lato, regular and bold, 101 KB of woff2 covering two weights, replaced by
98 81 KB carrying the whole 200-800 axis. What left second: the Young Serif
99 block that used to sit here, and the `--font-display` beside it.
100
101 Young Serif is still the brand tier and still MNW's alone. That has not
102 changed and is not what moved. What changed is that the exemption is a
103 mechanism now rather than a gap in the vocabulary: the face is declared
104 once, as an override of the `display` slot, and reads as one. */
105
106 /* Everything below is in the `components` cascade layer.
107
108 _head_assets.html declares the order once, `@layer makeover, base,
109 components, responsive`, and until now three of those four names were
110 empty: this file adopted none of them, so 11,788 unlayered lines outranked
111 every named layer by construction and the generated sheets lost every
112 contest they entered.
113
114 Adopting `components` changes no rendering today. `components` already sits
115 after `makeover` in the declared order, so this file wins the same contests
116 it was winning before; what changes is that it wins them by a stated rule
117 rather than by being unlayered. wizard.css and media-player.css adopt the
118 same layer, and they still beat this file on a tie because a layer resolves
119 internally by source order and they load later.
120
121 Two things stay OUT of the layer, deliberately. The @font-face rules above
122 are not style rules and take no part in the cascade. And the theme block
123 theming.rs injects into <head> is unlayered, which is load-bearing: an
124 unlayered rule outranks every layer, so a creator's chosen theme keeps
125 overriding this file's :root without needing to know the order. */
126 @layer components {
127 :root {
128 /* Typography: nothing. All three tokens come from typography.css, which
129 loads before this file.
130
131 `--font-mono` and `--font-sans` are the house slots and `--font-display`
132 is MNW's own, an override declared in build.rs. Do not redefine any of the
133 three here -- that is the divergence the unification deleted, and this
134 file is the one that would win. A product face belongs in the override,
135 where the whole tree can read it. */
136
137 /* --------------------------------------------------------------------
138 INTENT LAYER, the shared theme vocabulary (themeable)
139
140 These are the intent tokens emitted by theme_common::intent_css_vars():
141 colors declared by ROLE (surface/content/action/status/line/category),
142 plus the derived interactive states (hover/active/selection/row-stripe/
143 contrast) the crate computes once for every product. The values below
144 are the platform default (shared/themes/makenotwork.toml resolved) so
145 non-creator pages render without injection; creator profile/project/item
146 pages inject a <style> that supersedes this block with the chosen theme.
147 Generated. Keep in sync with `cargo run --example dump_intents makenotwork`.
148 -------------------------------------------------------------------- */
149 --surface-page: #ddd6c9;
150 --surface-raised: #efe9e0;
151 --surface-sunken: #cdc5b5;
152 --surface-overlay: #efe9e0;
153 /* The content surface cut into a raised one, so a list reads as content in a
154 container rather than as bands on a panel. Derived by makeover from
155 surface-raised, away from resolved content, so it goes lighter on a light
156 theme and darker on a dark one. */
157 --surface-well: #fffef5;
158 /* The two edges of a bevel. makeover derives both from the theme's raised
159 surface, so they arrive with the rest of the intent set through
160 intent_css_vars() and every creator theme gains them automatically. */
161 --bevel-light: #fffef5;
162 --bevel-dark: #b5afa6;
163 --content: #3d3530;
164 --content-secondary: #5c554f;
165 --content-muted: #8a8480;
166 --content-on-action: #ffffff;
167 --action: #6c5ce7;
168 --action-hover: #7a6cf8;
169 --danger: #c0392b;
170 --success: #27ae60;
171 --warning: #f59e0b;
172 --info: #17a2b8;
173 --border: #b3ab97;
174 --border-strong: #a39c88;
175 --focus-ring: #6c5ce7;
176 --hover-surface: #cdc5b5;
177 --category-one: #c0392b;
178 --category-two: #27ae60;
179 --category-three: #6c5ce7;
180 --category-four: #f59e0b;
181 --category-five: #a29bfe;
182 --category-six: #17a2b8;
183 --overlay: rgba(3, 1, 0, 0.5);
184 /* What a surface floating over the page casts onto it. Same tone as the
185 scrim above and pinned near-black on every theme, because a shadow is
186 absence of light rather than a tint. layout.css composes the geometry
187 into --elevation-overlay; a consumer that needs a different geometry
188 builds its own from this token rather than from a literal. */
189 --elevation: rgba(3, 1, 0, 0.18);
190
191 /* --------------------------------------------------------------------
192 APP-LOCAL CONSTANTS, not theme colors, not derivable from intents.
193
194 Component CSS references the intent tokens above directly (no brand
195 aliases). Status backgrounds derive from the intents inline via
196 color-mix(); these few stay fixed:
197 - pure black/white contrast anchors
198 - Stripe's brand color (an external constant)
199 - diff add/del, a hard readability convention that must not re-theme
200 -------------------------------------------------------------------- */
201 --primary-dark: #000000;
202 --primary-light: #ffffff;
203 --stripe: #635BFF;
204
205 /* The faint accent tint every selection and highlight recipe fills with.
206 Derived from --action, so it re-tints with the creator's theme. Named
207 because the charter names it and because a dozen rules read it. */
208 --highlight-faint: color-mix(in oklch, var(--action) 8%, transparent);
209
210 /* Diff / code review, fixed green-add / red-del convention */
211 --diff-add: #2d7d2d;
212 --diff-add-bg: rgba(45, 125, 45, 0.08);
213 --diff-del: #a83232;
214 --diff-del-bg: rgba(168, 50, 50, 0.08);
215
216 /* Radius lives in geometry.css now, generated from makeover-geometry:
217 --radius-square / -fine / -control / -panel / -round. Containers are
218 square and controls keep a corner, so the rounding survives only where a
219 thing is meant to be pressed. */
220
221 /* Syntax highlighting for the git source browser: base16 Tomorrow, used
222 whole rather than assembled colour by colour. It stays fixed across all
223 31 themes on purpose, the same argument as the diff add/del pair above:
224 a reader recognises a highlighting palette, and re-tinting it per theme
225 costs that recognition to gain nothing. Named so it is legible as one
226 palette instead of seven hex literals scattered through the git rules. */
227 --syntax-keyword: #8959a8;
228 --syntax-string: #718c00;
229 --syntax-comment: #8e908c;
230 --syntax-constant: #f5871f;
231 --syntax-entity: #4271ae;
232 --syntax-variable: #c82829;
233 --syntax-support: #3e999f;
234
235 /* Stacking order, named for what floats over what. Local rather than
236 generated: this is a claim about MNW's own furniture, not an invariant
237 like spacing. The numbers are spaced so a value can be slotted between
238 two of them without renumbering, and nothing outside this block should
239 write a z-index literal. Anything not listed here does not float. */
240 --z-raised: 1; /* lifted within the flow: a saved badge on a card */
241 --z-nav: 10; /* the nav panel that drops out of the mobile header */
242 --z-header: 11; /* the header's own toggle and search panel, both of
243 which have to stay above the panel they open */
244 --z-dropdown: 20; /* menus and suggestion lists opened from a control */
245 --z-picker: 30; /* pickers that open over a dropdown */
246 --z-context: 40; /* right-click / overflow context menus */
247 --z-overlay: 50; /* full-page scrims */
248 --z-modal: 60; /* dialogs, above their scrim */
249 --z-toast: 70; /* transient messages, above a dialog */
250 --z-banner: 80; /* page-level interruptions, above everything */
251 --z-skip-link: 90; /* the keyboard escape hatch outranks all of it */
252
253 /* Depth, and there are only two kinds of it.
254
255 A surface IN the page takes a bevel: an edge, not a distance. That is
256 --shadow-raised/-card/-inset, aliasing the pair layout.css derives from
257 each theme's own raised surface. --shadow-raised and --shadow-card
258 collapse onto one value because an edge has no heavier and lighter
259 version to pick between.
260
261 A surface floating OVER the page takes --elevation-overlay, also from
262 layout.css, derived from the theme's page colour. Menus, toasts,
263 popovers, dropdowns and pickers float. A card, a plate and a framed
264 image do not; they are .raised.
265
266 There used to be a third kind, --shadow-1/-2/-3, a blurred scale of
267 rgba-black literals picked against parchment and then applied to all 31
268 themes a creator can select. It was the same defect the bevel trio was
269 rewritten to undo, one scale later, and the elevation intent is what
270 replaced it. No literal survives here now, so every consumer re-themes
271 without being opened. Geometry stays consumer-side: a site that needs a
272 different blur composes it from var(--elevation), never from a literal. */
273 --shadow-raised: var(--bevel-raised);
274 --shadow-card: var(--bevel-raised);
275 --shadow-inset: var(--bevel-inset);
276 }
277
278 /* Reset */
279 * {
280 margin: 0;
281 padding: 0;
282 box-sizing: border-box;
283 }
284
285 /* Base styles */
286 body {
287 margin: 0;
288 background-color: var(--surface-page);
289 color: var(--content);
290 font-family: var(--font-sans);
291 line-height: 1.6;
292 }
293
294 /* Padded page layout for most content pages */
295 .padded-page {
296 padding: var(--gap-pane);
297 }
298
299 /* Centered page layout for landing, login, signup */
300 .centered-page {
301 display: flex;
302 flex-direction: column;
303 justify-content: center;
304 align-items: center;
305 min-height: 100vh;
306 gap: var(--gap-page);
307 }
308
309 .centered-page h1 {
310 font-size: var(--text-hero);
311 }
312
313 .message-container {
314 max-width: 500px;
315 text-align: center;
316 }
317
318 .centered-wrapper {
319 justify-content: center;
320 text-align: center;
321 }
322
323 h1 {
324 font-family: var(--font-display);
325 font-weight: normal;
326 color: var(--content);
327 text-align: center;
328 font-size: var(--text-display);
329 }
330
331 h2,
332 h3 {
333 font-family: var(--font-mono);
334 font-weight: normal;
335 color: var(--content);
336 }
337
338 /* Heading classes (charter: docs/design-system.md, every h1/h2 in a
339 template must carry one of these so the role is explicit).
340
341 .brand-h1: the "Makenot.work" wordmark on auth/wizard pages.
342 .page-title: page-level h1 (Young Serif, centered).
343 .subtitle-h2: page subtitle h2 used under .brand-h1 in auth/wizards.
344 .subsection-title: h2 inside dashboards, tabs, and prose partials
345 (mono, no border, the default-h2 role made explicit).
346 .section-header: h2 prose sub-section heading with bottom border
347 (existing rule, see SECTIONS block). */
348 .brand-h1,
349 .page-title {
350 font-family: var(--font-display);
351 font-weight: normal;
352 color: var(--content);
353 text-align: center;
354 font-size: var(--text-display);
355 }
356
357 .subtitle-h2 {
358 font-family: var(--font-mono);
359 font-weight: normal;
360 color: var(--content);
361 text-align: center;
362 font-size: var(--text-head);
363 }
364
365 .subsection-title {
366 font-family: var(--font-mono);
367 font-weight: normal;
368 color: var(--content);
369 }
370
371 p {
372 font-family: var(--font-sans);
373 color: var(--content);
374 }
375
376 a {
377 color: var(--content);
378 text-decoration: none;
379 }
380
381 a:hover {
382 text-decoration: underline;
383 }
384
385 /* The signature dot */
386 .dot {
387 color: var(--action);
388 }
389
390 /* Container, default 1200px max-width. Modifier classes for narrower pages,
391 or comma-grouped page-scoped overrides below for body-class-based control. */
392 .container {
393 max-width: 1200px;
394 margin: 0 auto;
395 padding: var(--gap-page);
396 }
397
398 /* Per-page container width overrides (selected via body class). One rule
399 per width tier; new pages should add their body class here rather than
400 defining a new .foo-page .container rule. */
401 .delete-account-page .container,
402 .receipt-page .container { max-width: 600px; margin: 0 auto; }
403
404 /* The profile is described, so it has no container: the measure lands on the
405 element the renderer draws the page into. The width is this page's own and
406 the rest of the block was the design system's. */
407 .user-page main { max-width: 600px; }
408
409 .import-page .container,
410 .export-page .container { max-width: 800px; margin: 0 auto; }
411
412 .fan-plus-page main,
413 .creators-page main { max-width: 900px; margin: 0 auto; }
414
415 /* READING COLUMN, the 680px measure every long-form surface uses. Narrower
416 than .container and with tighter horizontal padding, so it is its own
417 primitive rather than a .container width tier.
418
419 These four wrappers were four identical rules until 2026-08-16. They are
420 grouped rather than replaced by one class because the charter treats a new
421 class as a smell, and because grouping keeps each selector's own
422 specificity intact. A fifth long-form surface joins this list; it does not
423 copy the recipe.
424
425 .media-container's rule used to live in media-player.css. It is here now so
426 the measure has one definition; that sheet keeps its descendant rules. */
427 .article-page .article-container,
428 .doc-container,
429 .docs-index,
430 .media-container {
431 max-width: 680px;
432 margin: 0 auto;
433 padding: var(--gap-page) var(--gap-pane) var(--gap-page);
434 }
435
436 /* ===========================================
437 BUTTONS
438 =========================================== */
439
440 button {
441 color: var(--content);
442 background: var(--surface-overlay);
443 padding: 6px 12px;
444 border: 1px solid var(--content);
445 font-family: inherit;
446 cursor: pointer;
447 box-shadow: var(--shadow-raised);
448 transition: box-shadow 0.1s ease, background 0.2s ease;
449 }
450
451 button:hover {
452 background: var(--hover-surface);
453 }
454
455 button:active {
456 box-shadow: var(--shadow-inset);
457 }
458
459 /* A described act carries the generated `.button` class, and the element rule
460 above sets the same surface, bevel and tone the generated class does. This
461 file is in @layer components, which the order statement puts after
462 `makeover`, so the element rule wins whatever the specificity. Without the
463 handoff below a described act renders in --surface-overlay with
464 --shadow-raised and, worse, in --content rather than its tone: a
465 destructive act looks identical to an ordinary one. Same mechanism and same
466 remedy as `.tab.chosen` further down; read the comment there.
467
468 The split is deliberate. The app keeps padding, border and font from the
469 element rule, makeover keeps surface, bevel and tone. That is the
470 arrangement that already makes `.field` work, where the app's input rule
471 happens to set only geometry and typography.
472
473 Tone is handed back on `[data-tone]` rather than on `.button`, because an
474 untoned button has no colour in the layer to roll back to and would land on
475 the UA default instead of --content. */
476 .button,
477 .chip,
478 .segment,
479 .toggle {
480 /* respec-ok: a layer handoff, not a value. */
481 background: revert-layer;
482 box-shadow: revert-layer;
483 }
484
485 .button:hover,
486 .chip:hover,
487 .segment:hover,
488 .toggle:hover {
489 /* respec-ok: a layer handoff, not a value. */
490 background: revert-layer;
491 }
492
493 .button:active,
494 .chip:active,
495 .segment:active,
496 .toggle:active {
497 /* respec-ok: a layer handoff, not a value. */
498 background: revert-layer;
499 box-shadow: revert-layer;
500 }
501
502 .button[data-tone] {
503 /* respec-ok: a layer handoff, not a value. */
504 color: revert-layer;
505 }
506
507 /* A described link, same handoff on the `a` and `button` element rules.
508 `a { color: var(--content); text-decoration: none }` renders a described
509 link as body text -- the reader cannot tell it is a link at all, which is
510 the same defect as the lost tone above and worse for being silent.
511
512 `button.link` is the second arm: a link that posts is a button in the
513 markup, and makeover already flattens it (`background: none`, no border, no
514 padding, `font: inherit`). The element rule's chrome beat all four, so it
515 rendered as an ordinary raised button. Only the properties makeover sets on
516 this class are handed back; `font` is handed back on the button arm alone,
517 because an `a.link` has no lower-layer font to roll back to and would land
518 on the UA default face. */
519 .link {
520 /* respec-ok: a layer handoff, not a value. */
521 color: revert-layer;
522 text-decoration: revert-layer;
523 }
524
525 button.link {
526 /* respec-ok: a layer handoff, not a value. */
527 background: revert-layer;
528 border: revert-layer;
529 padding: revert-layer;
530 font: revert-layer;
531 box-shadow: revert-layer;
532 cursor: revert-layer;
533 }
534
535 /* A described facet value, the third arm of the same handoff. makeover 0.67.0
536 names the element-chrome reset, and a facet control withdraws the button's
537 fill, stroke and bevel with it: a value reads flat until it is hovered,
538 pressed or in force. The element rule above supplies all three, so without
539 this a described facet renders as an ordinary raised button and
540 `[aria-pressed="true"]` is its only correct state -- which is the whole of
541 what the panel had to say and the one thing it could not.
542
543 Wider than the `.button` group above by one property. That group keeps the
544 app's border deliberately, because a described act is still a button. Here
545 the flat rest look IS the withdrawal of the border, so it is handed back
546 with the other two. */
547 .facet-take,
548 .facet-prune {
549 /* respec-ok: a layer handoff, not a value. */
550 background: revert-layer;
551 border: revert-layer;
552 box-shadow: revert-layer;
553 }
554
555 .facet-take:hover,
556 .facet-prune:hover {
557 /* respec-ok: a layer handoff, not a value. */
558 background: revert-layer;
559 }
560
561 .facet-take:active,
562 .facet-prune:active,
563 .facet-take[aria-pressed="true"],
564 .facet-prune[aria-pressed="true"] {
565 /* respec-ok: a layer handoff, not a value. */
566 background: revert-layer;
567 box-shadow: revert-layer;
568 }
569
570 /* A described field on a `select` or a `textarea`. The input rule further down
571 sets padding and font only, which is why the shipped `.field` precedents
572 survived; these two also set `background` and `box-shadow`, so a described
573 field on either renders in --surface-sunken rather than the well. The focus
574 ring is handed back with them: makeover insets it (`calc(-1 * 2px)`) so it
575 does not collide with the bevel, and the app's outer 2px was winning. */
576 .field {
577 /* respec-ok: a layer handoff, not a value. */
578 background: revert-layer;
579 box-shadow: revert-layer;
580 }
581
582 .field:focus-visible {
583 /* respec-ok: a layer handoff, not a value. */
584 outline-offset: revert-layer;
585 }
586
587 .btn-primary {
588 background: var(--primary-dark);
589 color: var(--primary-light);
590 border: none;
591 padding: var(--gap-section) var(--gap-pane);
592 font-family: inherit;
593 font-size: var(--text-body);
594 cursor: pointer;
595 box-shadow: var(--shadow-raised);
596 transition: box-shadow 0.1s ease, background 0.2s ease;
597 display: inline-block;
598 text-decoration: none;
599 }
600
601 /* A solid-fill button shifts its own fill rather than taking --hover-surface.
602 The charter's hover rule is "one step lighter than the resting fill", and on
603 a neutral control that step is --hover-surface; on this one it is not, since
604 the label is --primary-light and a parchment fill under it is unreadable. */
605 .btn-primary:hover {
606 background: color-mix(in oklch, var(--primary-dark) 85%, var(--primary-light));
607 text-decoration: none;
608 }
609
610 .btn-primary:active {
611 box-shadow: var(--shadow-inset);
612 }
613
614 .btn-secondary {
615 background: var(--surface-sunken);
616 color: var(--content);
617 border: none;
618 padding: var(--gap-section) var(--gap-pane);
619 font-family: var(--font-mono);
620 font-size: var(--text-body);
621 cursor: pointer;
622 box-shadow: var(--shadow-raised);
623 transition: box-shadow 0.1s ease, background 0.2s ease;
624 display: inline-block;
625 text-decoration: none;
626 }
627
628 .btn-secondary:hover {
629 background: var(--hover-surface);
630 text-decoration: none;
631 }
632
633 .btn-secondary:active {
634 box-shadow: var(--shadow-inset);
635 }
636
637 .btn-danger {
638 background: var(--danger);
639 color: var(--primary-light);
640 border: none;
641 padding: var(--gap-section) var(--gap-pane);
642 font-family: var(--font-mono);
643 font-size: var(--text-body);
644 cursor: pointer;
645 box-shadow: var(--shadow-raised);
646 transition: box-shadow 0.1s ease, background 0.2s ease;
647 display: inline-block;
648 text-decoration: none;
649 }
650
651 /* Solid fill, so the same exception as .btn-primary applies. */
652 .btn-danger:hover {
653 background: color-mix(in oklch, var(--danger) 85%, var(--primary-light));
654 text-decoration: none;
655 }
656
657 .btn-danger:active {
658 box-shadow: var(--shadow-inset);
659 }
660
661 /* Disabled state for all button variants (charter rule).
662 Applies whether disabled via attribute or aria-disabled. */
663 button:disabled,
664 button[aria-disabled="true"],
665 .btn-primary:disabled,
666 .btn-secondary:disabled,
667 .btn-danger:disabled {
668 opacity: 0.5;
669 cursor: not-allowed;
670 box-shadow: none;
671 transform: none;
672 }
673
674 /* Button size + style modifiers (charter: docs/design-system.md).
675 Use these on top of .btn-primary / -secondary / -danger.
676
677 .btn--large: large CTA padding bump.
678 .btn--icon: square icon-only / micro button (small padding,
679 tight line-height).
680 .btn--link: visually a link, semantically a button (no bg/
681 border, opacity-fade hover, mono).
682
683 Some surfaces have tuned button variants that are NOT compositions
684 of these modifiers and keep their own classes:
685 .big-button: Young Serif 200×60 anchor on splash pages.
686 .order-btn: list reorder up/down (with active flash).
687 .play-button: circular media-player play control.
688 .speed-button: segment in media-player speed group.
689 .shortcuts-help-btn: 1.5rem square help glyph in toolbars.
690 .toast-retry-btn: inline bordered button inside a toast,
691 uses currentColor to inherit toast tone.
692 These are documented variants, not deprecation targets. */
693 .btn--large {
694 padding: var(--gap-section) var(--gap-page);
695 font-size: var(--text-lead);
696 }
697
698 .btn--icon {
699 padding: var(--gap-bound) var(--gap-peer);
700 font-size: var(--text-note);
701 line-height: 1;
702 min-width: 1.75rem;
703 }
704
705 .btn--link {
706 background: none;
707 color: var(--content);
708 border: none;
709 padding: 0;
710 font-family: var(--font-mono);
711 font-size: var(--text-body);
712 cursor: pointer;
713 /* Link semantics, flat, no depth. Overrides the shadow that would
714 otherwise cascade from `button` / `form button` when a .btn--link
715 happens to be a <button> inside a <form> (e.g. logout). */
716 box-shadow: none;
717 transition: opacity 0.2s ease;
718 }
719
720 .btn--link:hover {
721 opacity: 0.6;
722 box-shadow: none;
723 }
724
725 .btn--link:active {
726 box-shadow: var(--shadow-inset);
727 }
728
729 /* Menu / disclosure openers, buttons whose job is to reveal a menu,
730 filter panel, expander, or popover. They sit flat rather than raised.
731 Add new opener selectors here as they appear. */
732 .context-menu-btn,
733 .discover-filter-toggle,
734 .bundle-toggle {
735 box-shadow: none;
736 }
737
738 .context-menu-btn:hover,
739 .discover-filter-toggle:hover,
740 .bundle-toggle:hover {
741 box-shadow: none;
742 }
743
744 .context-menu-btn:active,
745 .discover-filter-toggle:active,
746 .bundle-toggle:active {
747 box-shadow: var(--shadow-inset);
748 }
749
750 /* ===========================================
751 FORMS
752 =========================================== */
753
754 form {
755 display: flex;
756 flex-direction: column;
757 gap: var(--gap-section);
758 width: 100%;
759 }
760
761 .form-container {
762 max-width: 400px;
763 background: var(--surface-overlay);
764 padding: var(--gap-page);
765 }
766
767 label {
768 font-family: var(--font-mono);
769 color: var(--content);
770 font-size: var(--text-note);
771 }
772
773 input[type="text"],
774 input[type="email"],
775 input[type="password"],
776 input[type="number"],
777 input[type="date"],
778 textarea,
779 select {
780 width: 100%;
781 background: var(--surface-sunken);
782 border: 1px solid var(--border);
783 padding: var(--gap-section);
784 font-family: var(--font-mono);
785 font-size: var(--text-note);
786 color: var(--content);
787 box-shadow: var(--shadow-inset);
788 }
789
790 input::placeholder,
791 textarea::placeholder {
792 opacity: 0.5;
793 }
794
795 input[type="text"]:focus,
796 input[type="email"]:focus,
797 input[type="password"]:focus,
798 input[type="number"]:focus,
799 textarea:focus,
800 select:focus {
801 outline: none;
802 border: 1px solid var(--action);
803 }
804
805 textarea {
806 min-height: 100px;
807 resize: vertical;
808 }
809
810 select {
811 cursor: pointer;
812 }
813
814 input[type="submit"],
815 form button {
816 font-family: var(--font-display);
817 font-size: var(--text-body);
818 color: var(--content);
819 background: var(--surface-overlay);
820 padding: 12px 24px;
821 border: 1px solid var(--content);
822 cursor: pointer;
823 box-shadow: var(--shadow-raised);
824 transition: box-shadow 0.1s ease, background 0.2s ease;
825 }
826
827 input[type="submit"]:hover,
828 form button:hover {
829 background: var(--hover-surface);
830 }
831
832 input[type="submit"]:active,
833 form button:active {
834 box-shadow: var(--shadow-inset);
835 }
836
837 .form-group {
838 margin-bottom: var(--gap-pane);
839 }
840
841 .form-group label {
842 display: block;
843 margin-bottom: var(--gap-peer);
844 font-size: var(--text-note);
845 }
846
847 .form-group input,
848 .form-group textarea,
849 .form-group select {
850 width: 100%;
851 }
852
853 .form-row {
854 display: grid;
855 grid-template-columns: 1fr 1fr;
856 gap: var(--gap-section);
857 }
858
859 .hint {
860 font-size: var(--text-note);
861 opacity: 0.6;
862 margin-top: var(--gap-peer);
863 }
864
865 /* Field-level validation error, paired with .form-group--error.
866 Canonical: `partials/_ui.html` ui::form_field macro. */
867 .form-group--error label {
868 color: var(--danger);
869 }
870
871 .form-group--error input,
872 .form-group--error textarea,
873 .form-group--error select {
874 border-color: var(--danger);
875 }
876
877 .field-error {
878 font-size: var(--text-note);
879 color: var(--danger);
880 margin-top: var(--gap-peer);
881 }
882
883 /* Checkbox group */
884 .checkbox-group {
885 display: flex;
886 align-items: start;
887 gap: var(--gap-section);
888 cursor: pointer;
889 transition: background 0.2s ease;
890 margin-bottom: var(--gap-peer);
891 }
892
893 .checkbox-group input[type="checkbox"] {
894 width: auto;
895 margin-top: var(--gap-bound);
896 margin-bottom: var(--gap-bound);
897 cursor: pointer;
898 }
899 /* Radio group */
900 .radio-group {
901 display: flex;
902 flex-direction: column;
903 gap: var(--gap-section);
904 margin-top: var(--gap-peer);
905 }
906 /* ===========================================
907 TABS
908 =========================================== */
909
910 .tabs {
911 display: flex;
912 flex-wrap: nowrap;
913 gap: 0;
914 margin-bottom: 0;
915 }
916
917 /* Geometry and type only. The surface is layout.css's: `.tab` is sunken,
918 `:hover` steps to --hover-surface on a fine pointer, `:active` insets. This
919 rule used to restate the sunken background verbatim and then carry a second,
920 redundant selection signal on top of it — opacity 0.6 unselected, 1 selected
921 — so a tab said "not chosen" twice and the generated surface step did
922 nothing. The opacity idiom is gone and the surface contrast is the signal. */
923 .tab {
924 color: var(--content);
925 border: none;
926 padding: var(--gap-section) var(--gap-page);
927 font-family: var(--font-mono);
928 font-size: var(--text-body);
929 cursor: pointer;
930 /* Tabs navigate, they don't commit, stay flat. Overrides the base
931 `button` shadow if a tab is a <button>. */
932 box-shadow: none;
933 transition: background 0.2s ease;
934 white-space: nowrap;
935 flex-shrink: 0;
936 }
937
938 /* A tab's selected state is `.tab.chosen` in layout.css and nowhere else now.
939 This file used to carry a `.tab.is-selected` rule setting the same two
940 declarations byte for byte, on the argument that `.is-selected` is the
941 site's own idiom across git nav links, editor tabs, section tabs, issue tabs
942 and filter buttons, so renaming the tabs alone would split it. That is true
943 and it is still the wrong trade: a second name for a state the design system
944 already names is invisible only while both say the same thing, and stops
945 being invisible the moment makeover changes what a chosen tab looks like.
946 The other components keep `.is-selected`, because makeover has no primitive
947 for a git nav link; the tab has one.
948
949 The chosen arm's box-shadow comes back through the layer rather than being
950 restated. `.tab` below zeroes the base `button` shadow, and this file is in
951 @layer components, which the order statement puts after `makeover`, so it
952 outranks it whatever the specificity: without the revert, the generated
953 bevel never applies and a chosen tab reads flat.
954 `revert-layer` hands the property back instead of naming a value, which is
955 the difference between deferring to the design system and copying it. */
956 .tab.chosen {
957 /* respec-ok: a layer handoff, not a value. See the comment above. */
958 box-shadow: revert-layer;
959 }
960
961 .tab-more-wrap {
962 position: relative;
963 flex-shrink: 0;
964 }
965
966 .tab-overflow-menu {
967 position: absolute;
968 top: 100%;
969 right: 0;
970 z-index: var(--z-dropdown);
971 background: var(--surface-page);
972 border: 1px solid var(--border);
973 min-width: 180px;
974 box-shadow: var(--elevation-overlay);
975 }
976
977 /* The overflow menu restates the tab as a menu row: block, full width, left
978 aligned, tighter. It carried the same opacity pair the strip did and it goes
979 for the same reason; hover and selection are the generated surface steps. */
980 .tab-overflow-menu .tab {
981 display: block;
982 width: 100%;
983 text-align: left;
984 padding: var(--gap-group) var(--gap-section);
985 }
986
987
988 .tab-content {
989 display: none;
990 background: var(--surface-overlay);
991 padding: var(--gap-page);
992 }
993
994 .tab-content.active {
995 display: block;
996 }
997
998 /* ===========================================
999 TABLES
1000 =========================================== */
1001
1002 .data-table {
1003 width: 100%;
1004 border-collapse: collapse;
1005 font-size: var(--text-note);
1006 font-family: var(--font-sans);
1007 margin-bottom: var(--gap-section);
1008 }
1009
1010 .data-table thead {
1011 background: var(--surface-sunken);
1012 }
1013
1014 .data-table th {
1015 text-align: left;
1016 padding: var(--gap-section);
1017 font-weight: normal;
1018 }
1019
1020 /* `.data-table th.sortable` and its four rules were here and had no consumer:
1021 nothing in templates/ ever put `sortable` on a `th`. The one sortable table
1022 in the app is templates/partials/tabs/project_content.html, which used
1023 `.sortable-th` further down this file. Both are gone; the affordance and the
1024 caret are `.table-heading[data-sortable]` in the generated static/layout.css.
1025 See the block that replaced `.sortable-th` for what stayed behind. */
1026
1027 .data-table tbody tr {
1028 transition: background 0.1s ease;
1029 }
1030
1031 /* No zebra. There were two rules here, :nth-child(odd) and (even), and both
1032 set --surface-overlay — the same value, so the stripe never existed and the
1033 effect was every row painted the panel's own colour. The table is a .well
1034 now and the rows sit on it unpainted, which is what the banding was
1035 reaching for. makeover derives no row-stripe intent, so if one is ever
1036 wanted it is a makeover-side question, not a literal here. */
1037
1038 .data-table tbody tr:hover {
1039 background: var(--surface-sunken);
1040 }
1041
1042 .data-table td {
1043 padding: var(--gap-section);
1044 border-top: 1px solid var(--border);
1045 }
1046
1047 /* Marks the row a table is about, used across the dashboard and library
1048 tables. Lived in the pricing block until the fee calculator replaced it. */
1049 .data-table .highlight {
1050 background: var(--surface-overlay);
1051 border-left: 3px solid var(--action);
1052 }
1053
1054 /* ===========================================
1055 COMPACT TABLES (dashboard/admin, lighter than .data-table)
1056 =========================================== */
1057
1058 .compact-table {
1059 width: 100%;
1060 border-collapse: collapse;
1061 }
1062
1063 .compact-table thead tr {
1064 font-size: var(--text-note);
1065 opacity: 0.7;
1066 }
1067
1068 .compact-table th {
1069 padding: var(--gap-peer) var(--gap-section);
1070 text-align: left;
1071 }
1072
1073 .compact-table td {
1074 padding: var(--gap-section);
1075 }
1076
1077 .compact-table tbody tr {
1078 border-bottom: 1px solid var(--border);
1079 }
1080
1081 /* ===========================================
1082 UTILITIES
1083 =========================================== */
1084
1085 .text-sm { font-size: var(--text-note); }
1086 .text-xs { font-size: var(--text-fine); }
1087 .muted { opacity: 0.7; }
1088 .dimmed { opacity: 0.6; }
1089 .dimmer { opacity: 0.5; }
1090 .meta { font-size: var(--text-note); opacity: 0.7; }
1091 .nowrap { white-space: nowrap; }
1092 .scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }
1093 .text-center { text-align: center; }
1094 .text-right { text-align: right; }
1095 .fw-bold { font-weight: bold; }
1096 .unstyled-link { text-decoration: none; color: inherit; }
1097 .square-cover { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; }
1098
1099 /* Spacing utilities.
1100 mt = margin-top, mb = margin-bottom, my = vertical, py = vertical padding.
1101 Named for the relationship, same vocabulary as the --gap-* tokens they
1102 read. Use sparingly; prefer parent layout primitives. */
1103 .m-0 { margin: 0; }
1104 .mt-0 { margin-top: 0; }
1105 .mb-0 { margin-bottom: 0; }
1106 .mt-peer { margin-top: var(--gap-peer); }
1107 .mt-section { margin-top: var(--gap-section); }
1108 .mt-pane { margin-top: var(--gap-pane); }
1109 .mt-page { margin-top: var(--gap-page); }
1110 .mb-peer { margin-bottom: var(--gap-peer); }
1111 .mb-section { margin-bottom: var(--gap-section); }
1112 .mb-pane { margin-bottom: var(--gap-pane); }
1113 .my-peer { margin-block: var(--gap-peer); }
1114 .my-section { margin-block: var(--gap-section); }
1115 .py-section { padding-block: var(--gap-section); }
1116
1117 /* Min-width utilities for tables inside .scroll-x wrappers. */
1118 .minw-300 { min-width: 300px; }
1119 .minw-400 { min-width: 400px; }
1120 .minw-500 { min-width: 500px; }
1121 .minw-600 { min-width: 600px; }
1122 .minw-700 { min-width: 700px; }
1123 .minw-800 { min-width: 800px; }
1124
1125 /* Compact button sizes. Canonical: .small (broad use, 0.25rem 0.6rem 0.8rem).
1126 The tiny / row-btn / cart-row-btn aliases are visually equivalent, within
1127 sub-pixel of each other in the original templates, and folded into .small. */
1128 .small { padding: var(--gap-bound) var(--gap-group); font-size: var(--text-fine); }
1129
1130 /* Canonical empty-state primitive. Use modifiers for tight/sized contexts.
1131 Markup: <div class="empty-state">…</div> (optionally with .empty-state--compact
1132 inside dropdowns or .empty-state--chart inside a fixed-height chart slot). */
1133 .empty-state { text-align: center; padding: var(--gap-page); opacity: 0.6; }
1134 .empty-state--compact { padding: var(--gap-section); font-size: var(--text-note); }
1135 .empty-state--chart { height: 200px; padding: 0; display: flex; align-items: center; justify-content: center; opacity: 0.5; }
1136 .empty-state-hint { font-family: var(--font-mono); font-size: var(--text-fine); margin-top: var(--gap-peer); opacity: 0.6; }
1137
1138 .tab-docs {
1139 display: flex;
1140 justify-content: flex-end;
1141 margin-bottom: var(--gap-section);
1142 }
1143
1144 .tab-docs a {
1145 font-family: var(--font-mono);
1146 font-size: var(--text-fine);
1147 opacity: 0.5;
1148 transition: opacity 0.2s ease;
1149 }
1150
1151 .tab-docs a:hover {
1152 opacity: 1;
1153 }
1154
1155 /* ===========================================
1156 BADGES
1157 =========================================== */
1158
1159 /* Geometry and type only. The colour is layout.css's: a badge is coloured
1160 text on the page, and its status colour comes from `data-tone` — see
1161 src/types/badge.rs, which maps every status enum onto a tone name.
1162
1163 This used to be a filled pill (near-black ground, --primary-light label)
1164 with a local `.badge--live/pending/failed` set painting the fill per
1165 status. All of it went in wave 2 tier B2: the fill was carrying exactly
1166 what the generated `color` carries, so it was the same thing said in a
1167 different channel, and keeping it meant the tone attribute could never do
1168 anything. */
1169 .badge {
1170 display: inline-block;
1171 padding: var(--gap-bound) var(--gap-peer);
1172 font-size: var(--text-fine);
1173 font-family: var(--font-mono);
1174 }
1175
1176 /* AI disclosure tier badges. See site-docs/public/about/generative-ai.md.
1177 Colours track the brand palette: violet for the cleanest case, warm-tan for
1178 disclosed AI use, charcoal for primarily generated.
1179
1180 respec-ok: this is a taxonomy makeover has no vocabulary for. Its four
1181 tones are info / success / warning / danger, which all read as alarm, and
1182 disclosure is not alarm — a handmade item is not a "success" and a
1183 generated one is not a "danger". So the three hues stay local. They set
1184 `color` rather than a background, because the badge form is the generated
1185 one now and only the hue is ours. */
1186 .badge.ai-tier {
1187 letter-spacing: 0.02em;
1188 }
1189 .badge.ai-tier-handmade {
1190 color: var(--action);
1191 }
1192 .badge.ai-tier-assisted {
1193 color: var(--warning);
1194 }
1195 .badge.ai-tier-generated {
1196 color: var(--content);
1197 }
1198
1199 .item-ai-tier {
1200 margin: var(--gap-bound) 0 var(--gap-section);
1201 }
1202
1203 .ai-disclosure {
1204 margin: var(--gap-section) 0;
1205 padding: var(--gap-section);
1206 background: var(--surface-sunken);
1207 border-left: 3px solid var(--warning);
1208 }
1209 .ai-disclosure-label {
1210 font-family: var(--font-mono);
1211 font-size: var(--text-fine);
1212 text-transform: uppercase;
1213 letter-spacing: 0.05em;
1214 color: var(--content-muted);
1215 margin-bottom: var(--gap-bound);
1216 }
1217 .ai-disclosure-text {
1218 font-size: var(--text-note);
1219 color: var(--content);
1220 }
1221
1222 /* Discover row: plain text mention per design decision 2026-06-03:
1223 no pill in the row, so fans skim the listing without disclosure noise
1224 and use the explicit filter when they care. */
1225 .discover-row-ai-tier {
1226 font-family: var(--font-mono);
1227 font-size: var(--text-fine);
1228 color: var(--content-muted);
1229 }
1230
1231 /* ===========================================
1232 TAGS
1233 =========================================== */
1234
1235 .tag {
1236 display: inline-block;
1237 background: var(--primary-dark);
1238 color: var(--primary-light);
1239 padding: var(--gap-bound) var(--gap-peer);
1240 font-size: var(--text-fine);
1241 margin-right: var(--gap-bound);
1242 }
1243
1244 .tag-input {
1245 background: var(--surface-sunken);
1246 padding: var(--gap-peer);
1247 margin-bottom: var(--gap-peer);
1248 display: flex;
1249 flex-wrap: wrap;
1250 gap: var(--gap-peer);
1251 min-height: 40px;
1252 }
1253
1254 .tag-input .tag {
1255 display: flex;
1256 align-items: center;
1257 gap: var(--gap-peer);
1258 padding: var(--gap-bound) var(--gap-peer);
1259 font-size: var(--text-note);
1260 }
1261
1262 .tag-input .tag button {
1263 background: none;
1264 border: none;
1265 color: var(--primary-light);
1266 cursor: pointer;
1267 padding: 0;
1268 font-size: var(--text-body);
1269 }
1270
1271 /* ===========================================
1272 CARDS
1273 =========================================== */
1274
1275 /* Canonical card primitive (charter: docs/design-system.md).
1276 Variants:
1277 .card: filled, hover-step (default content card).
1278 .card.card-muted: surface-muted fill, no hover (dashboard stat / analytics blocks).
1279 .card.card--bordered: bordered, padded, no fill (marketing cards: tier, fork).
1280 .card.card--selectable: radio-card pattern; pair with .is-selected.
1281 .card.card--grid: grid-cell card (discover grid).
1282 No aliases. A template names the modifier it wants and adds its own
1283 component class beside it; a recipe reached only through a per-page name is
1284 a recipe nobody can find from the charter. */
1285 /* Fill and depth come from layout.css, generated by makeover-webview: the card
1286 is `--surface-raised` plus `--bevel-raised`, and `:hover` goes to
1287 `--hover-surface` on a fine pointer only. Declaring either here would take
1288 them back, since style.css is in @layer components, which the order
1289 statement puts after `makeover`, whatever the specificity. The box-shadow this used to carry was
1290 `--shadow-card`, which already aliases `--bevel-raised`, so only the fill
1291 actually moved: `--surface-overlay` to `--surface-raised`. Those are equal on
1292 the platform default and a theme may set them apart. */
1293 .card {
1294 padding: var(--gap-pane);
1295 margin-bottom: var(--gap-section);
1296 text-decoration: none;
1297 color: var(--content);
1298 display: block;
1299 border: 1px solid var(--border);
1300 transition: background 0.2s ease;
1301 }
1302
1303 /* Muted card primitive. */
1304 .card-muted {
1305 background: var(--surface-sunken);
1306 padding: var(--gap-pane);
1307 }
1308 /* Bordered card. Templates put this modifier on the element alongside any
1309 component class of their own (.tier-card, .fork-card), so the name the
1310 charter teaches is the name in the markup. A described screen names none of
1311 them: a card there is a region, and the design system draws it. */
1312 .card--bordered {
1313 background: transparent;
1314 padding: var(--gap-section);
1315 border: 1px solid var(--border);
1316 margin-bottom: 0;
1317 }
1318 .fork-card { padding: var(--gap-pane); display: flex; flex-direction: column; }
1319
1320 .card-title {
1321 font-size: var(--text-subhead);
1322 margin-bottom: var(--gap-peer);
1323 font-family: var(--font-display);
1324 }
1325
1326 .card-meta {
1327 font-size: var(--text-note);
1328 color: var(--content-muted);
1329 margin-bottom: var(--gap-peer);
1330 font-family: var(--font-mono);
1331 }
1332
1333 .card-description {
1334 font-size: var(--text-note);
1335 opacity: 0.8;
1336 font-family: var(--font-sans);
1337 }
1338
1339 /* ===========================================
1340 STATS
1341 =========================================== */
1342
1343 .stats-grid {
1344 display: grid;
1345 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1346 gap: var(--gap-section);
1347 margin-bottom: var(--gap-pane);
1348 }
1349
1350 .stats-row {
1351 display: flex;
1352 gap: var(--gap-pane);
1353 margin: var(--gap-pane) 0;
1354 }
1355
1356 .stat-box {
1357 background: var(--surface-overlay);
1358 padding: var(--gap-section) var(--gap-pane);
1359 flex: 1;
1360 text-align: center;
1361 }
1362
1363 .stat-box .number {
1364 font-family: var(--font-display);
1365 font-weight: bold;
1366 font-size: var(--text-head);
1367 }
1368
1369 .stat-box .label {
1370 font-size: var(--text-fine);
1371 opacity: 0.7;
1372 }
1373
1374 /* .stat-card base recipe defined above with .card-muted */
1375
1376 .stat-label {
1377 font-size: var(--text-note);
1378 opacity: 0.7;
1379 margin-bottom: var(--gap-bound);
1380 }
1381
1382 .stat-value {
1383 font-size: var(--text-head);
1384 }
1385
1386 .stat-change {
1387 font-size: var(--text-note);
1388 opacity: 0.6;
1389 }
1390
1391 .stat-change.positive {
1392 color: var(--success);
1393 }
1394
1395 .stat-change.negative {
1396 color: var(--danger);
1397 }
1398
1399 /* ===========================================
1400 FILTER BAR
1401 =========================================== */
1402
1403 .filter-bar {
1404 display: flex;
1405 gap: var(--gap-peer);
1406 margin: var(--gap-pane) 0;
1407 flex-wrap: wrap;
1408 align-items: center;
1409 }
1410
1411 .filter-bar > .label {
1412 font-size: var(--text-note);
1413 opacity: 0.7;
1414 margin-right: var(--gap-peer);
1415 }
1416
1417 .filter-bar button {
1418 padding: var(--gap-bound) var(--gap-section);
1419 font-size: var(--text-note);
1420 }
1421
1422 /* ===========================================
1423 DISCUSSION SECTION
1424 =========================================== */
1425
1426 /* Sits inside the parent container; previously had max-width: 720px and
1427 margin: 2rem auto, which looked indented on wide pages (.library-page,
1428 .item-page) where every neighbouring card filled the container. Let the
1429 parent dictate width, article-page already constrains via
1430 .article-container; player pages have their own narrow layout. */
1431 .discussion-section {
1432 margin: var(--gap-page) 0;
1433 padding: var(--gap-pane);
1434 border: 1px solid var(--border);
1435 font-family: var(--font-mono);
1436 }
1437
1438 .discussion-section h3 {
1439 font-family: var(--font-display);
1440 font-size: var(--text-subhead);
1441 margin: 0 0 var(--gap-peer);
1442 }
1443
1444 .discussion-section p {
1445 margin: 0 0 var(--gap-section);
1446 color: var(--content-muted);
1447 font-size: var(--text-note);
1448 }
1449
1450 .discussion-section a {
1451 color: var(--action);
1452 text-decoration: none;
1453 font-size: var(--text-note);
1454 }
1455
1456 /* ===========================================
1457 NOTIFICATION SURFACES (info-box, warning-box, error-message)
1458 =========================================== */
1459
1460 /* Notification surface roles (charter: docs/design-system.md).
1461 Five distinct components. Keep them distinct, don't merge:
1462 .toast: transient, JS-dismissible, bottom-right corner.
1463 .banner: full-bleed page-top notice (sandbox, restart,
1464 founder pricing). One short sentence + optional link.
1465 .alert: inline directive callout with uppercase mono title
1466 (NOTE / TIP / IMPORTANT / WARNING / CAUTION). Used in
1467 docs and longer-form bodies.
1468 .info-box: informational headed block with h3 + bullet list
1469 inside forms / wizards (distinct register from .alert).
1470 .warning-box: loud yellow-on-yellow attention-grabber for
1471 page-level warnings (delete confirms, account
1472 warnings). Louder than .alert-warning intentionally.
1473 .error-message: inline form-field error; hidden by default, shown
1474 with .is-active. Distinct from page-level .alert. */
1475 .info-box {
1476 background: var(--surface-sunken);
1477 padding: var(--gap-section);
1478 margin-bottom: var(--gap-section);
1479 font-size: var(--text-note);
1480 font-family: var(--font-sans);
1481 }
1482
1483 .info-box h3 {
1484 font-size: var(--text-body);
1485 font-weight: normal;
1486 margin-bottom: var(--gap-peer);
1487 }
1488
1489 .info-box ul {
1490 list-style: none;
1491 margin-top: var(--gap-peer);
1492 }
1493
1494 .info-box li {
1495 padding: var(--gap-bound) 0;
1496 }
1497
1498 .info-box li::before {
1499 content: "- ";
1500 opacity: 0.5;
1501 }
1502
1503 .warning-box {
1504 background: var(--warning);
1505 color: var(--primary-light);
1506 padding: var(--gap-section);
1507 margin-bottom: var(--gap-section);
1508 font-size: var(--text-note);
1509 font-family: var(--font-sans);
1510 }
1511
1512 .error-message {
1513 font-family: var(--font-mono);
1514 color: var(--danger);
1515 background: color-mix(in oklch, var(--danger) 10%, var(--surface-page));
1516 padding: 12px;
1517 border: 1px solid var(--danger);
1518 border-radius: var(--radius-fine);
1519 text-align: center;
1520 font-size: var(--text-note);
1521 display: none;
1522 }
1523
1524 /* ===========================================
1525 ERROR PAGE
1526 =========================================== */
1527
1528 /* Full-page error layout (templates/pages/error.html). Distinct from
1529 the inline `.error-message` form-error class above; named to avoid
1530 collision. */
1531 .error-page {
1532 min-height: 100vh;
1533 display: flex;
1534 align-items: center;
1535 justify-content: center;
1536 padding: var(--gap-page);
1537 background: var(--surface-page);
1538 }
1539
1540 .error-container {
1541 text-align: center;
1542 max-width: 560px;
1543 }
1544
1545 .error-status {
1546 font-size: var(--text-note);
1547 font-weight: 600;
1548 letter-spacing: 0.08em;
1549 text-transform: uppercase;
1550 color: var(--content-muted);
1551 margin-bottom: var(--gap-section);
1552 }
1553
1554 .error-page-message {
1555 font-size: var(--text-head);
1556 line-height: 1.4;
1557 color: var(--content);
1558 margin-bottom: var(--gap-page);
1559 }
1560
1561 .error-actions {
1562 display: flex;
1563 gap: var(--gap-section);
1564 justify-content: center;
1565 }
1566
1567
1568 /* ===========================================
1569 BUY PAGE (standalone direct purchase)
1570 =========================================== */
1571
1572 /* Minimal direct-purchase page (templates/pages/buy.html). Standalone
1573 page used for link-in-bio sharing, no site chrome, just a card.
1574 Scoped under .buy-page so the rules don't bleed into the rest of
1575 the app. Tokenized; previously lived as a page-isolated <style>. */
1576 .buy-page {
1577 min-height: 100vh;
1578 display: flex;
1579 align-items: center;
1580 justify-content: center;
1581 padding: var(--gap-page);
1582 }
1583
1584 /* The one former --shadow-3 site that is not a float. The card is the whole
1585 page's content rather than something laid over it, so it takes .raised
1586 from layout.css for its surface and its edge, and keeps only the geometry
1587 the generated primitive does not carry. */
1588 .buy-page .buy-card {
1589 border-radius: var(--radius-panel);
1590 padding: var(--gap-page);
1591 max-width: 420px;
1592 width: 100%;
1593 }
1594
1595 .buy-page .cover,
1596 .buy-page .no-cover {
1597 width: 100%;
1598 aspect-ratio: 1;
1599 max-height: 280px;
1600 border-radius: var(--radius-panel);
1601 margin-bottom: var(--gap-pane);
1602 background: var(--surface-raised);
1603 }
1604
1605 .buy-page .cover {
1606 object-fit: cover;
1607 }
1608
1609 .buy-page .no-cover {
1610 display: flex;
1611 align-items: center;
1612 justify-content: center;
1613 font-size: var(--text-hero);
1614 opacity: 0.2;
1615 }
1616
1617 .buy-page h1 {
1618 text-align: left;
1619 font-size: var(--text-head);
1620 margin-bottom: var(--gap-bound);
1621 }
1622
1623 .buy-page .creator {
1624 font-family: var(--font-mono);
1625 font-size: var(--text-note);
1626 opacity: 0.6;
1627 margin-bottom: var(--gap-pane);
1628 }
1629
1630 .buy-page .creator a {
1631 color: inherit;
1632 }
1633
1634 .buy-page .price {
1635 font-family: var(--font-mono);
1636 font-size: var(--text-subhead);
1637 margin-bottom: var(--gap-pane);
1638 }
1639
1640 .buy-page .description {
1641 font-size: var(--text-note);
1642 line-height: 1.5;
1643 opacity: 0.8;
1644 margin-bottom: var(--gap-pane);
1645 max-height: 4.5em;
1646 overflow: hidden;
1647 }
1648
1649 .buy-page .buy-btn {
1650 display: block;
1651 width: 100%;
1652 padding: 14px;
1653 background: var(--action);
1654 color: var(--primary-light);
1655 border: none;
1656 border-radius: var(--radius-panel);
1657 font-size: var(--text-body);
1658 font-weight: 600;
1659 cursor: pointer;
1660 text-align: center;
1661 text-decoration: none;
1662 transition: opacity 0.2s ease;
1663 }
1664
1665 .buy-page .buy-btn:hover {
1666 opacity: 0.85;
1667 }
1668
1669 .buy-page .buy-btn:disabled {
1670 opacity: 0.6;
1671 cursor: wait;
1672 }
1673
1674 .buy-page .pwyw-input {
1675 display: flex;
1676 align-items: center;
1677 gap: var(--gap-bound);
1678 margin-bottom: var(--gap-section);
1679 }
1680
1681 .buy-page .pwyw-input input {
1682 width: 100px;
1683 padding: 8px;
1684 border: 1px solid var(--border);
1685 border-radius: var(--radius-control);
1686 font-size: var(--text-body);
1687 }
1688
1689 .buy-page .note {
1690 font-size: var(--text-fine);
1691 opacity: 0.5;
1692 text-align: center;
1693 margin-top: var(--gap-section);
1694 }
1695
1696 .buy-page .note a {
1697 color: var(--action);
1698 }
1699
1700 .buy-page .footer {
1701 text-align: center;
1702 margin-top: var(--gap-pane);
1703 padding-top: var(--gap-section);
1704 border-top: 1px solid var(--border);
1705 font-size: var(--text-fine);
1706 opacity: 0.5;
1707 }
1708
1709 .buy-page .footer a {
1710 color: inherit;
1711 }
1712
1713 /* ===========================================
1714 SECTIONED CONTENT (tabs + panels, shared by three pages)
1715 =========================================== */
1716
1717 /* Sectioned content (templates/pages/item.html, project.html,
1718 library_downloads.html). Tab+panel pattern shared by all three
1719 pages, kept global, NOT scoped under .item-page. */
1720 .section-tabs {
1721 display: flex;
1722 gap: 0;
1723 border-bottom: 1px solid var(--border);
1724 margin-bottom: var(--gap-pane);
1725 }
1726
1727 .section-tab {
1728 background: none;
1729 border: none;
1730 padding: var(--gap-group) var(--gap-section);
1731 cursor: pointer;
1732 font-size: var(--text-body);
1733 opacity: 0.6;
1734 border-bottom: 2px solid transparent;
1735 font-family: var(--font-sans);
1736 color: var(--content);
1737 }
1738
1739 .section-tab.is-selected {
1740 opacity: 1;
1741 border-bottom-color: var(--content);
1742 }
1743
1744 .section-tab:hover { opacity: 0.9; }
1745
1746 /* .section-panel.active is a visibility toggle (display: none / block),
1747 not a selection, kept as .active. */
1748 .section-panel { display: none; }
1749 .section-panel.active { display: block; }
1750 .section-panel p { margin-bottom: var(--gap-section); }
1751 .section-panel ul,
1752 .section-panel ol {
1753 margin-left: var(--gap-pane);
1754 margin-bottom: var(--gap-section);
1755 }
1756 .section-panel li { margin-bottom: var(--gap-peer); }
1757 .section-panel h1,
1758 .section-panel h2,
1759 .section-panel h3 {
1760 margin-top: var(--gap-section);
1761 margin-bottom: var(--gap-peer);
1762 }
1763 .section-panel pre {
1764 background: var(--surface-sunken);
1765 padding: var(--gap-section);
1766 overflow-x: auto;
1767 margin-bottom: var(--gap-section);
1768 }
1769 .section-panel code { font-size: var(--text-note); }
1770
1771 /* ===========================================
1772 ITEM PAGE
1773 =========================================== */
1774
1775 /* Item detail page (templates/pages/item.html). Scoped under .item-page
1776 body class so the .item-title / .item-meta / .item-price / .item-footer
1777 names don't collide with the store-front item-card on project.html. */
1778 .item-page .item-layout {
1779 display: grid;
1780 grid-template-columns: 400px 1fr;
1781 gap: var(--gap-page);
1782 margin-bottom: var(--gap-page);
1783 }
1784
1785 .item-page .item-layout.no-media { grid-template-columns: 1fr; }
1786
1787 .item-page .item-title {
1788 font-size: var(--text-title);
1789 margin-bottom: var(--gap-peer);
1790 }
1791
1792 .item-page .item-creator {
1793 font-size: var(--text-body);
1794 opacity: 0.7;
1795 margin-bottom: var(--gap-pane);
1796 }
1797
1798 .item-page .item-creator a { color: var(--content); }
1799
1800 .item-page .item-price {
1801 font-size: var(--text-display);
1802 margin-bottom: var(--gap-pane);
1803 }
1804
1805 .item-page .item-meta {
1806 display: grid;
1807 grid-template-columns: 1fr 1fr;
1808 gap: var(--gap-section);
1809 margin-bottom: var(--gap-pane);
1810 padding-bottom: var(--gap-pane);
1811 border-bottom: 1px solid var(--border);
1812 }
1813
1814 .item-page .meta-item { font-size: var(--text-note); }
1815 .item-page .meta-label { opacity: 0.7; margin-bottom: var(--gap-bound); }
1816 .item-page .meta-value { font-weight: bold; }
1817
1818 .item-page .item-tags { margin-bottom: var(--gap-pane); }
1819
1820 /* purchase-box uses .card-muted for the box; only its h3/ul/li specifics remain. */
1821 .purchase-box h3 {
1822 font-size: var(--text-body);
1823 font-weight: normal;
1824 margin-bottom: var(--gap-section);
1825 }
1826 .purchase-box ul { list-style: none; margin-bottom: var(--gap-section); }
1827 .purchase-box li { padding: var(--gap-bound) 0; }
1828 .purchase-box li::before {
1829 content: "";
1830 opacity: 0.5;
1831 }
1832
1833 .item-page .btn-primary {
1834 width: 100%;
1835 padding: var(--gap-section) var(--gap-page);
1836 font-size: var(--text-lead);
1837 }
1838
1839 .item-page .btn-secondary {
1840 width: 100%;
1841 margin-top: var(--gap-peer);
1842 }
1843
1844 .item-page .payment-note {
1845 font-size: var(--text-note);
1846 opacity: 0.6;
1847 text-align: center;
1848 margin-top: var(--gap-section);
1849 }
1850
1851 /* bundle-child uses .list-row co-class; only the gap-4 override remains. */
1852 .bundle-child { gap: var(--gap-section); padding: var(--gap-section) 0; }
1853
1854 .item-page .bundle-child-type {
1855 font-size: var(--text-fine);
1856 padding: var(--gap-bound) var(--gap-group);
1857 background: var(--surface-sunken);
1858 white-space: nowrap;
1859 }
1860
1861 .item-page .bundle-child-title { flex: 1; }
1862 .item-page .bundle-child-title a { color: var(--content); }
1863 .item-page .bundle-child-price { font-size: var(--text-note); opacity: 0.7; }
1864
1865 .item-page .bundle-notice {
1866 background: var(--surface-sunken);
1867 padding: var(--gap-pane);
1868 margin-bottom: var(--gap-pane);
1869 text-align: center;
1870 }
1871
1872 .item-page .bundle-notice a { color: var(--content); }
1873
1874 .item-page .item-description p { margin-bottom: var(--gap-section); text-align: left; }
1875 .item-page .item-description ul { margin-left: var(--gap-pane); margin-bottom: var(--gap-section); }
1876 .item-page .item-description li { margin-bottom: var(--gap-peer); }
1877
1878 .item-page .features-grid {
1879 display: grid;
1880 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1881 gap: var(--gap-pane);
1882 }
1883 .item-page .version-number { font-size: var(--text-lead); }
1884 .item-page .item-footer {
1885 margin-top: var(--gap-page);
1886 padding-top: var(--gap-page);
1887 border-top: 1px solid var(--border);
1888 text-align: center;
1889 opacity: 0.6;
1890 font-size: var(--text-note);
1891 }
1892
1893 .item-page .item-footer a { color: var(--content); }
1894
1895 @media (max-width: 839px) {
1896 .item-page .item-layout { grid-template-columns: 1fr; }
1897 }
1898
1899 @media (max-width: 599px) {
1900 .item-page .item-title { font-size: var(--text-head); }
1901 .item-page .item-price { font-size: var(--text-title); }
1902 .item-page .item-meta { grid-template-columns: 1fr; gap: var(--gap-peer); }
1903 .item-page .item-media,
1904 .item-page .item-details { padding: var(--gap-section); }
1905 .item-page .item-description { padding: var(--gap-section); }
1906 .item-page .item-description h2 { font-size: var(--text-subhead); }
1907 .item-page .purchase-box { padding: var(--gap-section); }
1908 }
1909
1910 /* ===========================================
1911 PROJECT PAGE (store front)
1912 ===========================================
1913
1914 The store front is described (`src/quasi/project.rs`), so the header, the
1915 item cards, the two view copies, the tab strip and the tier cards are all
1916 the design system's markup now: a rule restating one of them fails the
1917 drift check in build.rs. What is this page's rather than the system's is
1918 how wide it runs, which is the measure on the body, and nothing else was
1919 left. */
1920
1921 /* ===========================================
1922 LIBRARY DOWNLOADS PAGE
1923 =========================================== */
1924
1925 /* Library downloads page (templates/pages/library_downloads.html).
1926 Scoped under .library-page body class. Class names like .item-footer,
1927 .bundle-child, etc. overlap with .item-page; the scope keeps them
1928 distinct. */
1929 .library-page .library-header {
1930 display: grid;
1931 grid-template-columns: 200px 1fr;
1932 gap: var(--gap-pane);
1933 margin-bottom: var(--gap-page);
1934 align-items: center;
1935 }
1936
1937 .library-page .library-header.no-cover { grid-template-columns: 1fr; }
1938
1939 .library-page .library-cover img {
1940 width: 100%;
1941 aspect-ratio: 1 / 1;
1942 object-fit: cover;
1943 display: block;
1944 }
1945
1946 .library-page .library-title {
1947 font-size: var(--text-title);
1948 margin-bottom: var(--gap-bound);
1949 }
1950
1951 /* Byline sits under the centered H1 (global `h1 { text-align: center }`),
1952 so center it too, otherwise it floats flush-left while the title is
1953 centered above. Same for the back-link breadcrumb. When a cover is
1954 present the grid scope below restores left-alignment in the text column. */
1955 .library-page .library-creator {
1956 font-size: var(--text-body);
1957 opacity: 0.7;
1958 margin-bottom: var(--gap-section);
1959 text-align: center;
1960 }
1961
1962 .library-page .library-creator a { color: var(--content); }
1963
1964 .library-page .library-back {
1965 font-size: var(--text-note);
1966 opacity: 0.7;
1967 text-align: center;
1968 }
1969 .library-page .library-back a { color: var(--content); }
1970
1971 /* When a cover is present, the title block sits in the right grid column;
1972 left-align in that context so the column reads as a coherent block next
1973 to the cover instead of a centered island. */
1974 .library-page .library-header:not(.no-cover) .library-title,
1975 .library-page .library-header:not(.no-cover) .library-creator {
1976 text-align: left;
1977 }
1978
1979 .library-page .downloads-hero h2 {
1980 font-size: var(--text-head);
1981 margin-bottom: var(--gap-section);
1982 padding-bottom: var(--gap-peer);
1983 border-bottom: 1px solid var(--border);
1984 }
1985
1986 .library-page .download-row {
1987 display: flex;
1988 align-items: center;
1989 gap: var(--gap-section);
1990 padding: var(--gap-group) 0;
1991 border-bottom: 1px solid var(--border);
1992 }
1993
1994 .library-page .download-row:last-child { border-bottom: none; }
1995
1996 .library-page .download-version {
1997 min-width: 4em;
1998 font-family: var(--font-mono);
1999 font-size: var(--text-note);
2000 }
2001
2002 .library-page .download-label { flex: 1; }
2003
2004 .library-page .download-size {
2005 font-size: var(--text-note);
2006 opacity: 0.6;
2007 min-width: 5em;
2008 text-align: right;
2009 }
2010
2011 .library-page .download-notice {
2012 background: var(--surface-sunken);
2013 padding: var(--gap-pane);
2014 margin-bottom: var(--gap-page);
2015 font-size: var(--text-note);
2016 opacity: 0.8;
2017 }
2018
2019 .library-page .download-notice strong { font-family: var(--font-mono); }
2020
2021 /* Inline download list on library_audio.html / library_video.html
2022 (below the media player). */
2023
2024 .library-page .library-downloads h3 {
2025 font-size: var(--text-lead);
2026 margin-bottom: var(--gap-section);
2027 }
2028
2029 .library-page .library-section h2 {
2030 font-size: var(--text-subhead);
2031 margin-bottom: var(--gap-section);
2032 padding-bottom: var(--gap-peer);
2033 border-bottom: 1px solid var(--border);
2034 }
2035
2036 .library-page .bundle-child {
2037 display: flex;
2038 align-items: center;
2039 gap: var(--gap-section);
2040 padding: var(--gap-section) 0;
2041 border-bottom: 1px solid var(--border);
2042 }
2043
2044 .library-page .bundle-child:last-child { border-bottom: none; }
2045
2046 .library-page .bundle-child-type {
2047 font-size: var(--text-fine);
2048 padding: var(--gap-bound) var(--gap-group);
2049 background: var(--surface-sunken);
2050 white-space: nowrap;
2051 }
2052
2053 .library-page .bundle-child-title { flex: 1; }
2054 .library-page .bundle-child-title a { color: var(--content); }
2055
2056 .library-page .item-footer {
2057 margin-top: var(--gap-page);
2058 padding-top: var(--gap-page);
2059 border-top: 1px solid var(--border);
2060 text-align: center;
2061 opacity: 0.6;
2062 font-size: var(--text-note);
2063 }
2064
2065 .library-page .item-footer a { color: var(--content); }
2066
2067 @media (max-width: 599px) {
2068 .library-page .library-header { grid-template-columns: 1fr; }
2069 .library-page .library-cover img { max-width: 200px; }
2070 }
2071
2072 /* ===========================================
2073 ARTICLE & BLOG READER
2074 =========================================== */
2075
2076 /* Article reader pages (templates/pages/text_reader.html, blog_post.html,
2077 library_text.html). Long-form article layout, centered narrow column,
2078 author header, large heading, magazine-style body type. Scoped under
2079 .article-page body class. The container itself takes the shared reading
2080 column near the top of this file. */
2081
2082 .article-page .author-header {
2083 display: flex;
2084 align-items: center;
2085 gap: var(--gap-section);
2086 margin-bottom: var(--gap-page);
2087 }
2088
2089 .article-page .author-avatar {
2090 width: 48px;
2091 height: 48px;
2092 background: var(--surface-sunken);
2093 border-radius: var(--radius-round);
2094 display: flex;
2095 align-items: center;
2096 justify-content: center;
2097 font-family: var(--font-display);
2098 font-size: var(--text-subhead);
2099 color: var(--content);
2100 }
2101
2102 .article-page .author-info { flex: 1; }
2103
2104 .article-page .author-name {
2105 font-family: var(--font-mono);
2106 font-size: var(--text-body);
2107 color: var(--content);
2108 }
2109
2110
2111 .article-page .author-name a:hover { text-decoration: underline; }
2112
2113 .article-page .article-meta {
2114 font-family: var(--font-mono);
2115 font-size: var(--text-note);
2116 color: var(--content-muted);
2117 }
2118
2119 .article-page .article-title {
2120 font-family: var(--font-display);
2121 font-size: var(--text-display);
2122 font-weight: normal;
2123 line-height: 1.2;
2124 margin-bottom: var(--gap-pane);
2125 color: var(--content);
2126 text-align: left;
2127 }
2128
2129 .article-page .article-deck {
2130 font-family: var(--font-mono);
2131 font-size: var(--text-subhead);
2132 color: var(--content-muted);
2133 margin-bottom: var(--gap-page);
2134 line-height: 1.6;
2135 }
2136
2137 .article-page .article-body {
2138 font-family: var(--font-sans);
2139 font-size: var(--text-lead);
2140 line-height: 1.9;
2141 }
2142
2143 .article-page .article-body p { margin-bottom: var(--gap-pane); }
2144
2145 .article-page .article-body h1 {
2146 font-family: var(--font-display);
2147 font-size: var(--text-title);
2148 font-weight: normal;
2149 margin-top: var(--gap-page);
2150 margin-bottom: var(--gap-section);
2151 color: var(--content);
2152 text-align: left;
2153 }
2154
2155 .article-page .article-body h2 {
2156 font-family: var(--font-mono);
2157 font-size: var(--text-head);
2158 font-weight: normal;
2159 margin-top: var(--gap-page);
2160 margin-bottom: var(--gap-section);
2161 color: var(--content);
2162 }
2163
2164 .article-page .article-body h3 {
2165 font-family: var(--font-mono);
2166 font-size: var(--text-subhead);
2167 font-weight: normal;
2168 margin-top: var(--gap-page);
2169 margin-bottom: var(--gap-section);
2170 color: var(--content);
2171 }
2172
2173 .article-page .article-body blockquote {
2174 border-left: 3px solid var(--action);
2175 padding-left: var(--gap-pane);
2176 margin: var(--gap-page) 0;
2177 font-style: italic;
2178 color: var(--content-muted);
2179 }
2180
2181 .article-page .article-body ul,
2182 .article-page .article-body ol {
2183 margin-bottom: var(--gap-pane);
2184 padding-left: var(--gap-pane);
2185 }
2186
2187 .article-page .article-body li { margin-bottom: var(--gap-peer); }
2188
2189 .article-page .article-body a {
2190 color: var(--action);
2191 text-decoration: underline;
2192 text-decoration-color: color-mix(in oklch, var(--action) 40%, transparent);
2193 text-underline-offset: 2px;
2194 }
2195
2196 .article-page .article-body a:hover {
2197 text-decoration-color: var(--action);
2198 }
2199
2200 .article-page .article-body strong { font-weight: bold; }
2201 .article-page .article-body em { font-style: italic; }
2202
2203 .article-page .article-body code {
2204 font-family: var(--font-mono);
2205 background: var(--surface-sunken);
2206 padding: var(--gap-bound) var(--gap-peer);
2207 font-size: var(--text-body);
2208 }
2209
2210 .article-page .article-body pre {
2211 background: var(--surface-sunken);
2212 padding: var(--gap-section);
2213 overflow-x: auto;
2214 margin-bottom: var(--gap-pane);
2215 }
2216
2217 .article-page .article-body pre code {
2218 background: none;
2219 padding: 0;
2220 }
2221
2222 .article-page .article-body hr {
2223 border: none;
2224 text-align: center;
2225 margin: var(--gap-page) 0;
2226 }
2227
2228 .article-page .article-body hr::before {
2229 content: "* * *";
2230 color: var(--content-muted);
2231 letter-spacing: 1rem;
2232 }
2233
2234 .article-page .article-footer {
2235 margin-top: var(--gap-page);
2236 padding-top: var(--gap-page);
2237 border-top: 1px solid var(--border);
2238 }
2239
2240 .article-page .author-bio {
2241 display: flex;
2242 gap: var(--gap-section);
2243 padding: var(--gap-pane);
2244 background: var(--surface-overlay);
2245 }
2246
2247 .article-page .author-bio .author-avatar {
2248 width: 64px;
2249 height: 64px;
2250 font-size: var(--text-head);
2251 flex-shrink: 0;
2252 }
2253
2254 .article-page .author-bio-content { flex: 1; }
2255
2256 .article-page .author-bio-name {
2257 font-family: var(--font-mono);
2258 font-size: var(--text-lead);
2259 margin-bottom: var(--gap-peer);
2260 }
2261
2262
2263 .article-page .author-bio-name a:hover { text-decoration: underline; }
2264
2265 .article-page .article-tags {
2266 margin-top: var(--gap-page);
2267 display: flex;
2268 gap: var(--gap-peer);
2269 flex-wrap: wrap;
2270 }
2271
2272 .article-page .article-tag {
2273 font-family: var(--font-mono);
2274 background: var(--surface-sunken);
2275 color: var(--content);
2276 padding: var(--gap-peer) var(--gap-section);
2277 font-size: var(--text-fine);
2278 text-decoration: none;
2279 }
2280
2281 /* Share controls at the end of the article, inside .article-footer beside
2282 the tags or the blog crumb. Was a page-bottom footer below the comments
2283 until the site-footer consolidation; the control belongs with the piece
2284 it copies, not under the discussion. */
2285 .article-page .reader-actions {
2286 font-family: var(--font-mono);
2287 font-size: var(--text-note);
2288 margin-top: var(--gap-section);
2289 }
2290
2291 .article-page .reader-actions a { color: var(--content); text-decoration: none; }
2292 .article-page .reader-actions a:hover { text-decoration: underline; }
2293
2294 /* Blog-specific: nav crumb above the article. */
2295 .article-page .blog-nav {
2296 font-family: var(--font-mono);
2297 font-size: var(--text-note);
2298 margin-bottom: var(--gap-page);
2299 }
2300
2301 .article-page .blog-nav a { color: var(--content); text-decoration: none; }
2302 .article-page .blog-nav a:hover { text-decoration: underline; }
2303
2304 @media (max-width: 599px) {
2305 .article-page .article-title { font-size: var(--text-title); }
2306 .article-page .article-deck { font-size: var(--text-lead); }
2307 .article-page .article-body { font-size: var(--text-body); }
2308 }
2309
2310 /* ===========================================
2311 ADMIN & HEALTH DASHBOARDS
2312 =========================================== */
2313
2314 /* Admin dashboards (templates/dashboards/admin-*.html). Every admin
2315 page had a duplicate `h1 { font-size: 2rem; margin-bottom: 0.5rem; }`
2316 inline, consolidated here. */
2317
2318 /* System health dashboard (templates/pages/health.html). */
2319 .health-page {
2320 padding: var(--gap-page);
2321 }
2322
2323 .health-page .health-container {
2324 max-width: 900px;
2325 margin: 0 auto;
2326 }
2327
2328 .health-page .health-grid {
2329 display: grid;
2330 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
2331 gap: var(--gap-pane);
2332 margin-bottom: var(--gap-page);
2333 }
2334
2335 .health-page .health-card {
2336 background: var(--surface-sunken);
2337 padding: var(--gap-pane);
2338 }
2339
2340 .health-page .health-card h3 {
2341 font-family: var(--font-mono);
2342 font-size: var(--text-note);
2343 margin-bottom: var(--gap-section);
2344 display: flex;
2345 align-items: center;
2346 gap: var(--gap-peer);
2347 }
2348
2349 .health-page .status-indicator {
2350 width: 10px;
2351 height: 10px;
2352 border-radius: var(--radius-round);
2353 display: inline-block;
2354 }
2355
2356 .health-page .status-ok { background: var(--success); }
2357 .health-page .status-warn { background: var(--warning); }
2358 .health-page .status-error { background: var(--danger); }
2359 .health-page .status-unknown { background: var(--content-muted); }
2360
2361 .health-page .health-card dl {
2362 display: grid;
2363 grid-template-columns: auto 1fr;
2364 gap: var(--gap-peer) var(--gap-section);
2365 font-size: var(--text-note);
2366 }
2367
2368 .health-page .health-card dt { opacity: 0.7; }
2369 .health-page .health-card dd {
2370 font-family: var(--font-mono);
2371 text-align: right;
2372 }
2373
2374 .health-page .section-title {
2375 font-family: var(--font-mono);
2376 font-size: var(--text-body);
2377 margin: var(--gap-page) 0 var(--gap-section);
2378 padding-bottom: var(--gap-peer);
2379 border-bottom: 1px solid var(--border);
2380 }
2381 .health-page .test-list {
2382 list-style: none;
2383 padding: 0;
2384 margin: 0;
2385 }
2386
2387 .health-page .test-list li {
2388 display: flex;
2389 justify-content: space-between;
2390 padding: var(--gap-peer) 0;
2391 border-bottom: 1px solid var(--border);
2392 font-size: var(--text-note);
2393 }
2394
2395 .health-page .test-list li:last-child { border-bottom: none; }
2396
2397 .health-page .test-name { font-family: var(--font-mono); }
2398
2399 .health-page .test-pass { color: var(--success); }
2400 .health-page .test-fail { color: var(--danger); }
2401 .health-page .summary-bar {
2402 display: flex;
2403 gap: var(--gap-page);
2404 padding: var(--gap-section);
2405 background: var(--surface-sunken);
2406 margin-bottom: var(--gap-page);
2407 font-family: var(--font-mono);
2408 font-size: var(--text-note);
2409 }
2410
2411 .health-page .summary-item {
2412 display: flex;
2413 align-items: center;
2414 gap: var(--gap-peer);
2415 }
2416
2417 .health-page .timestamp {
2418 font-size: var(--text-fine);
2419 opacity: 0.6;
2420 text-align: right;
2421 margin-top: var(--gap-page);
2422 }
2423
2424 .health-page details.check-list {
2425 background: var(--surface-overlay);
2426 padding: var(--gap-pane);
2427 margin-bottom: var(--gap-page);
2428 }
2429
2430 .health-page details.check-list summary {
2431 font-family: var(--font-mono);
2432 font-size: var(--text-note);
2433 cursor: pointer;
2434 list-style: none;
2435 display: flex;
2436 align-items: center;
2437 gap: var(--gap-peer);
2438 }
2439
2440 .health-page details.check-list summary::-webkit-details-marker { display: none; }
2441
2442 .health-page details.check-list summary::before {
2443 content: "";
2444 font-size: var(--text-fine);
2445 transition: transform 0.15s;
2446 }
2447
2448 .health-page details.check-list[open] summary::before {
2449 transform: rotate(90deg);
2450 }
2451
2452 .health-page details.check-list .test-list {
2453 margin-top: var(--gap-section);
2454 }
2455 /* ===========================================
2456 IMPORT WIZARD
2457 =========================================== */
2458
2459 /* Import data wizard (templates/dashboards/dashboard-import.html).
2460 Page-specific upload + column-mapping + progress flow. Migration also
2461 fixed token bypasses: --border-color → --border, --accent → --highlight,
2462 --success-background → --success-bg, --error-background → --error-bg. */
2463
2464 .import-page .back-link {
2465 display: inline-block;
2466 margin-bottom: var(--gap-pane);
2467 font-size: var(--text-note);
2468 opacity: 0.7;
2469 }
2470
2471 .import-page .back-link:hover { opacity: 1; }
2472
2473 .import-page .form-group { margin-bottom: var(--gap-pane); }
2474
2475 .import-page .form-group label {
2476 display: block;
2477 font-family: var(--font-mono);
2478 font-size: var(--text-note);
2479 font-weight: bold;
2480 margin-bottom: var(--gap-peer);
2481 }
2482
2483 .import-page .form-group select,
2484 .import-page .form-group input[type="file"] {
2485 width: 100%;
2486 padding: var(--gap-peer);
2487 font-size: var(--text-note);
2488 border: 1px solid var(--border);
2489 background: var(--surface-page);
2490 }
2491
2492 .import-page .csv-preview {
2493 margin-top: var(--gap-section);
2494 overflow-x: auto;
2495 }
2496
2497 .import-page .csv-preview table {
2498 width: 100%;
2499 border-collapse: collapse;
2500 font-size: var(--text-note);
2501 }
2502
2503 .import-page .csv-preview th,
2504 .import-page .csv-preview td {
2505 padding: var(--gap-peer) var(--gap-group);
2506 border: 1px solid var(--border);
2507 text-align: left;
2508 }
2509
2510 .import-page .csv-preview th {
2511 background: var(--surface-sunken);
2512 font-family: var(--font-mono);
2513 }
2514
2515 .import-page .column-mapping { margin-top: var(--gap-section); }
2516
2517 .import-page .mapping-row {
2518 display: flex;
2519 align-items: center;
2520 gap: var(--gap-section);
2521 margin-bottom: var(--gap-peer);
2522 }
2523
2524 .import-page .mapping-row label {
2525 min-width: 100px;
2526 font-size: var(--text-note);
2527 font-family: var(--font-mono);
2528 margin-bottom: 0;
2529 }
2530
2531 .import-page .mapping-row select {
2532 flex: 1;
2533 padding: var(--gap-bound);
2534 font-size: var(--text-note);
2535 }
2536
2537 .import-page .progress-stats {
2538 display: flex;
2539 gap: var(--gap-page);
2540 font-size: var(--text-note);
2541 font-family: var(--font-mono);
2542 }
2543
2544 .import-page .progress-stats .stat-value { font-weight: bold; }
2545
2546 .import-page .import-result {
2547 padding: var(--gap-section);
2548 margin-bottom: var(--gap-section);
2549 font-size: var(--text-note);
2550 }
2551
2552 .import-page .import-result.success { background: color-mix(in oklch, var(--success) 12%, var(--surface-page)); }
2553 .import-page .import-result.error { background: color-mix(in oklch, var(--danger) 10%, var(--surface-page)); }
2554
2555 .import-page .error-log {
2556 margin-top: var(--gap-section);
2557 padding: var(--gap-peer);
2558 background: var(--surface-sunken);
2559 font-family: var(--font-mono);
2560 font-size: var(--text-fine);
2561 max-height: 200px;
2562 overflow-y: auto;
2563 white-space: pre-wrap;
2564 }
2565
2566 .import-page .import-history { margin-top: var(--gap-page); }
2567
2568 .import-page .history-table {
2569 width: 100%;
2570 border-collapse: collapse;
2571 font-size: var(--text-note);
2572 }
2573
2574 .import-page .history-table th,
2575 .import-page .history-table td {
2576 padding: var(--gap-peer);
2577 border-bottom: 1px solid var(--border);
2578 text-align: left;
2579 }
2580
2581 .import-page .history-table th {
2582 font-family: var(--font-mono);
2583 font-weight: bold;
2584 }
2585
2586 .import-page .import-note {
2587 background: var(--surface-sunken);
2588 padding: var(--gap-section);
2589 margin-top: var(--gap-page);
2590 font-size: var(--text-note);
2591 }
2592
2593 .import-page .import-note h3 {
2594 font-family: var(--font-mono);
2595 font-size: var(--text-note);
2596 margin-bottom: var(--gap-peer);
2597 }
2598
2599 /* ===========================================
2600 DELETE ACCOUNT PAGE
2601 =========================================== */
2602
2603 /* Delete account page (templates/dashboards/dashboard-delete-account.html).
2604 Overrides .warning-box to use --danger (delete is danger, not warning)
2605 and the .form-status partial styling. Scoped to keep the global versions
2606 intact for other pages. */
2607
2608 .delete-account-page .warning-box {
2609 background: var(--danger);
2610 color: var(--primary-light);
2611 padding: var(--gap-pane);
2612 margin-bottom: var(--gap-page);
2613 }
2614
2615 .delete-account-page .warning-box h3 {
2616 font-family: var(--font-mono);
2617 font-size: var(--text-body);
2618 margin-bottom: var(--gap-peer);
2619 }
2620
2621 .delete-account-page .warning-box ul {
2622 margin: var(--gap-peer) 0 0 var(--gap-pane);
2623 font-size: var(--text-note);
2624 }
2625
2626 .delete-account-page .warning-box li { margin-bottom: var(--gap-bound); }
2627
2628 .delete-account-page .export-prompt {
2629 background: var(--surface-sunken);
2630 padding: var(--gap-pane);
2631 margin-bottom: var(--gap-page);
2632 }
2633
2634 .delete-account-page .export-prompt h3 {
2635 font-family: var(--font-mono);
2636 font-size: var(--text-body);
2637 margin-bottom: var(--gap-peer);
2638 }
2639
2640 .delete-account-page .export-prompt p {
2641 font-size: var(--text-note);
2642 margin-bottom: var(--gap-section);
2643 }
2644
2645 .delete-account-page .delete-form {
2646 background: var(--surface-overlay);
2647 padding: var(--gap-pane);
2648 }
2649
2650 .delete-account-page .delete-form .form-group { margin-bottom: var(--gap-pane); }
2651
2652 .delete-account-page .delete-form label {
2653 display: block;
2654 margin-bottom: var(--gap-peer);
2655 font-family: var(--font-mono);
2656 font-size: var(--text-note);
2657 }
2658
2659 .delete-account-page .delete-form .hint {
2660 font-size: var(--text-note);
2661 opacity: 0.7;
2662 margin-top: var(--gap-bound);
2663 }
2664
2665 .delete-account-page .delete-form input[type="text"] {
2666 width: 100%;
2667 padding: var(--gap-section);
2668 font-size: var(--text-body);
2669 border: 2px solid var(--border);
2670 background: var(--surface-sunken);
2671 }
2672
2673 .delete-account-page .delete-form input[type="text"]:focus {
2674 border-color: var(--danger);
2675 outline: none;
2676 }
2677
2678 .delete-account-page .back-link {
2679 display: inline-block;
2680 margin-bottom: var(--gap-pane);
2681 font-size: var(--text-note);
2682 opacity: 0.7;
2683 }
2684
2685 .delete-account-page .back-link:hover { opacity: 1; }
2686
2687 .delete-account-page .form-actions {
2688 display: flex;
2689 gap: var(--gap-section);
2690 align-items: center;
2691 }
2692
2693 .delete-account-page .form-status { font-size: var(--text-note); }
2694 .delete-account-page .form-status.error { color: var(--danger); }
2695 .delete-account-page .form-status.success { color: var(--success); }
2696
2697 /* ===========================================
2698 EXPORT PAGE
2699 =========================================== */
2700
2701 /* Export data page (templates/dashboards/dashboard-export.html).
2702 `.export-card` and its inner classes (`-info`, `-title`, `-desc`, `-meta`)
2703 are intentionally page-scoped: this is a row-layout card (info on left,
2704 action button on right, flex justify-between) that doesn't fit `.card-muted`
2705 cleanly. Background uses `--light-background` rather than `--surface-muted`
2706 for visual distinction from the surrounding dashboard surface. */
2707
2708 .export-page .export-cards {
2709 display: grid;
2710 gap: var(--gap-section);
2711 }
2712
2713 .export-page .export-card {
2714 background: var(--surface-overlay);
2715 padding: var(--gap-pane);
2716 display: flex;
2717 justify-content: space-between;
2718 align-items: flex-start;
2719 gap: var(--gap-section);
2720 }
2721
2722 .export-page .export-card-info { flex: 1; }
2723
2724 .export-page .export-card-title {
2725 font-family: var(--font-mono);
2726 font-weight: bold;
2727 font-size: var(--text-lead);
2728 margin-bottom: var(--gap-peer);
2729 }
2730
2731 .export-page .export-card-desc {
2732 font-size: var(--text-note);
2733 opacity: 0.8;
2734 margin-bottom: var(--gap-peer);
2735 }
2736
2737 .export-page .export-card-meta {
2738 font-size: var(--text-fine);
2739 opacity: 0.6;
2740 }
2741
2742 .export-page .export-card button { white-space: nowrap; }
2743
2744 .export-page .export-note {
2745 background: var(--surface-sunken);
2746 padding: var(--gap-section);
2747 margin-top: var(--gap-page);
2748 font-size: var(--text-note);
2749 }
2750
2751 .export-page .export-note h3 {
2752 font-family: var(--font-mono);
2753 font-size: var(--text-note);
2754 margin-bottom: var(--gap-peer);
2755 }
2756
2757 .export-page .back-link {
2758 display: inline-block;
2759 margin-bottom: var(--gap-pane);
2760 font-size: var(--text-note);
2761 opacity: 0.7;
2762 }
2763
2764 .export-page .back-link:hover { opacity: 1; }
2765
2766 .export-page .export-status {
2767 margin-top: var(--gap-peer);
2768 font-size: var(--text-note);
2769 }
2770
2771 .export-page .export-status.success { color: var(--success); }
2772 .export-page .export-status.error { color: var(--danger); }
2773
2774 /* ===========================================
2775 RECEIPT PAGE
2776 =========================================== */
2777
2778 /* Receipt page (templates/pages/receipt.html). */
2779
2780 .receipt-page .receipt-header {
2781 display: flex;
2782 justify-content: space-between;
2783 align-items: flex-start;
2784 margin-bottom: var(--gap-page);
2785 padding-bottom: var(--gap-pane);
2786 border-bottom: 1px solid var(--border);
2787 }
2788
2789 .receipt-page .receipt-row {
2790 display: flex;
2791 justify-content: space-between;
2792 padding: var(--gap-peer) 0;
2793 font-size: var(--text-body);
2794 }
2795
2796 .receipt-page .receipt-row.total {
2797 font-weight: bold;
2798 font-size: var(--text-lead);
2799 border-top: 1px solid var(--border);
2800 padding-top: var(--gap-section);
2801 margin-top: var(--gap-peer);
2802 }
2803
2804 .receipt-page .receipt-label { opacity: 0.7; }
2805
2806 .receipt-page .receipt-footer {
2807 margin-top: var(--gap-page);
2808 padding-top: var(--gap-pane);
2809 border-top: 1px solid var(--border);
2810 font-size: var(--text-note);
2811 opacity: 0.6;
2812 }
2813
2814 .receipt-page .back-link {
2815 display: inline-block;
2816 margin-bottom: var(--gap-pane);
2817 font-size: var(--text-note);
2818 opacity: 0.7;
2819 }
2820
2821 .receipt-page .back-link:hover { opacity: 1; }
2822
2823 @media print {
2824 .receipt-page .back-link,
2825 .receipt-page .chrome-band,
2826 .receipt-page .print-btn { display: none !important; }
2827 .receipt-page .receipt-box { border: 1px solid var(--border); }
2828 }
2829
2830 /* ===========================================
2831 FAN+ PAGE
2832 =========================================== */
2833
2834 /* The Fan+ page moved to the description layer. Its headings, its section
2835 spacing, its price callout, its subscribe button, its status box and its
2836 success banner are all drawn by the design system now: the banner is a
2837 `Node::banner` with `Tone::Success`, the price is a `Figure`, and the button
2838 is an `Act`. Nothing here is left to target.
2839
2840 The measure is on `.fan-plus-page main` beside `.creators-page main`. */
2841
2842 /* ===========================================
2843 PURCHASE PAGE
2844 =========================================== */
2845
2846 /* Purchase confirmation page (templates/pages/purchase.html). */
2847 .purchase-page .container {
2848 max-width: 800px;
2849 margin: var(--gap-page) auto;
2850 padding: 0 var(--gap-page);
2851 }
2852
2853 .purchase-page .tagline {
2854 font-size: var(--text-note);
2855 opacity: 0.7;
2856 margin-bottom: var(--gap-page);
2857 text-align: center;
2858 }
2859
2860 .purchase-page .checkout-box { margin-bottom: var(--gap-pane); }
2861
2862 .purchase-page h2 { font-size: var(--text-title); margin-bottom: var(--gap-peer); }
2863
2864 .purchase-page .step-indicator {
2865 font-size: var(--text-note);
2866 opacity: 0.6;
2867 margin-bottom: var(--gap-pane);
2868 }
2869
2870 .purchase-page .item-summary {
2871 background: var(--surface-page);
2872 padding: var(--gap-pane);
2873 margin-bottom: var(--gap-pane);
2874 display: flex;
2875 gap: var(--gap-pane);
2876 }
2877
2878 .purchase-page .item-thumbnail {
2879 width: 120px;
2880 height: 120px;
2881 background: var(--surface-sunken);
2882 flex-shrink: 0;
2883 display: flex;
2884 align-items: center;
2885 justify-content: center;
2886 font-size: var(--text-hero);
2887 overflow: hidden;
2888 }
2889
2890 .purchase-page .item-thumbnail img {
2891 width: 100%;
2892 height: 100%;
2893 object-fit: cover;
2894 display: block;
2895 }
2896
2897 /* See the storefront card: the dimming is the placeholder's, not the slot's. */
2898 .purchase-page .item-thumbnail-empty { opacity: 0.3; }
2899
2900 .purchase-page .item-details { flex: 1; }
2901
2902 .purchase-page .item-title {
2903 font-size: var(--text-subhead);
2904 margin-bottom: var(--gap-peer);
2905 font-family: var(--font-display);
2906 font-weight: bold;
2907 }
2908
2909 .purchase-page .item-creator {
2910 font-size: var(--text-note);
2911 opacity: 0.7;
2912 margin-bottom: var(--gap-section);
2913 }
2914
2915 .purchase-page .item-creator a { color: var(--content); }
2916 .purchase-page .item-description { font-size: var(--text-note); opacity: 0.8; }
2917
2918 .purchase-page .price-breakdown {
2919 background: var(--surface-page);
2920 padding: var(--gap-pane);
2921 margin-bottom: var(--gap-pane);
2922 }
2923
2924 .purchase-page .price-breakdown h2 { font-size: var(--text-lead); margin-bottom: var(--gap-section); }
2925
2926 .purchase-page .price-row {
2927 display: flex;
2928 justify-content: space-between;
2929 padding: var(--gap-peer) 0;
2930 border-bottom: 1px solid var(--border);
2931 }
2932
2933 .purchase-page .price-row:last-child {
2934 border-bottom: none;
2935 margin-top: var(--gap-peer);
2936 font-size: var(--text-subhead);
2937 padding-top: var(--gap-section);
2938 border-top: 2px solid var(--border);
2939 }
2940
2941 .purchase-page .price-row.total { font-weight: bold; }
2942
2943 .purchase-page .payment-section { margin-bottom: var(--gap-pane); }
2944 .purchase-page .payment-section h2 { font-size: var(--text-lead); margin-bottom: var(--gap-section); }
2945
2946 .purchase-page .btn-primary { width: 100%; }
2947 .purchase-page .btn-secondary { width: 100%; margin-top: var(--gap-section); }
2948
2949 .purchase-page .security-note {
2950 font-size: var(--text-note);
2951 opacity: 0.6;
2952 text-align: center;
2953 margin-top: var(--gap-section);
2954 }
2955
2956 /* Replaces inline styles in pages/purchase.html. */
2957 .purchase-page h1 { text-align: center; }
2958
2959 .purchase-page .price-breakdown-note {
2960 margin-top: var(--gap-peer);
2961 font-size: var(--text-fine);
2962 opacity: 0.6;
2963 }
2964
2965 .purchase-page .creator-breakdown {
2966 background: var(--surface-page);
2967 padding: var(--gap-section) var(--gap-pane);
2968 margin-bottom: var(--gap-pane);
2969 font-size: var(--text-note);
2970 }
2971 .purchase-page .creator-breakdown h2 {
2972 font-size: var(--text-body);
2973 margin-bottom: var(--gap-section);
2974 opacity: 0.7;
2975 }
2976 .purchase-page .creator-breakdown .price-row { opacity: 0.8; }
2977 .purchase-page .creator-breakdown .price-row.is-total {
2978 opacity: 1;
2979 font-weight: bold;
2980 border-top: 1px solid var(--border);
2981 margin-top: var(--gap-peer);
2982 padding-top: var(--gap-peer);
2983 /* override .price-row:last-child upsize */
2984 font-size: inherit;
2985 border-bottom: none;
2986 }
2987 .purchase-page .creator-breakdown .platform-fee-amount { color: var(--content); }
2988 .purchase-page .creator-breakdown-note {
2989 margin-top: var(--gap-section);
2990 font-size: var(--text-note);
2991 opacity: 0.6;
2992 }
2993
2994 .purchase-page .payment-section-lead {
2995 margin-bottom: var(--gap-pane);
2996 opacity: 0.8;
2997 }
2998
2999 .purchase-page .pending-checkout {
3000 margin-bottom: var(--gap-pane);
3001 background: var(--surface-sunken);
3002 padding: var(--gap-section);
3003 }
3004 .purchase-page .pending-checkout p { margin: 0 0 var(--gap-section); }
3005 .purchase-page .pending-checkout p.is-sub {
3006 font-size: var(--text-note);
3007 opacity: 0.8;
3008 }
3009 .purchase-page .pending-checkout form { margin: 0; }
3010
3011 /* Checkout form field wrappers. */
3012 .purchase-page .checkout-field { margin-bottom: var(--gap-section); }
3013 .purchase-page .checkout-field-label {
3014 font-size: var(--text-note);
3015 display: block;
3016 margin-bottom: var(--gap-bound);
3017 }
3018 .purchase-page .pwyw-row {
3019 display: flex;
3020 align-items: center;
3021 gap: var(--gap-bound);
3022 }
3023 .purchase-page .pwyw-symbol { font-size: var(--text-lead); }
3024 .purchase-page .pwyw-input { width: 120px; }
3025 .purchase-page .pwyw-min-note {
3026 font-size: var(--text-fine);
3027 opacity: 0.6;
3028 margin-top: var(--gap-bound);
3029 }
3030 .purchase-page .promo-input {
3031 width: 200px;
3032 text-transform: uppercase;
3033 }
3034 .purchase-page .share-contact-label {
3035 font-size: var(--text-note);
3036 display: flex;
3037 align-items: center;
3038 gap: var(--gap-peer);
3039 cursor: pointer;
3040 }
3041 .purchase-page .share-contact-checkbox { width: auto; }
3042
3043 .purchase-page .cart-alt-link {
3044 text-align: center;
3045 margin-top: var(--gap-section);
3046 font-size: var(--text-note);
3047 opacity: 0.7;
3048 }
3049 .purchase-page .guest-hint {
3050 font-size: var(--text-fine);
3051 opacity: 0.6;
3052 margin-top: var(--gap-section);
3053 text-align: center;
3054 }
3055 .purchase-page .guest-login-prompt {
3056 font-size: var(--text-fine);
3057 margin-top: var(--gap-peer);
3058 text-align: center;
3059 }
3060
3061 /* ===========================================
3062 PROJECT SETTINGS TAB
3063 (replaces inline styles in partials/tabs/project_settings.html and
3064 related innerHTML in static/project-sections.js)
3065 =========================================== */
3066
3067 .proj-image-hint {
3068 font-size: var(--text-note);
3069 opacity: 0.7;
3070 margin: 0 0 var(--gap-peer);
3071 }
3072
3073 /* Canonical inline form-status text following a Save / Change button.
3074 Two class names because templates evolved with both. They're aliases.
3075 States via .success / .error / .saving modifier. */
3076 .field-status,
3077 .save-status {
3078 margin-left: var(--gap-peer);
3079 font-size: var(--text-note);
3080 }
3081 .field-status.success, .save-status.success { color: var(--success); }
3082 .field-status.error, .save-status.error { color: var(--danger); }
3083 .field-status.saving, .save-status.saving { opacity: 0.6; }
3084
3085 /* Muted lead paragraph above a form button (monetization / features / delete). */
3086 .section-lead {
3087 margin-bottom: var(--gap-section);
3088 opacity: 0.7;
3089 text-align: left;
3090 }
3091
3092 /* Large intro paragraph under a page h1 (creators, policy, changelog, fan_plus). */
3093 .page-intro {
3094 font-size: var(--text-lead);
3095 margin-bottom: var(--gap-page);
3096 line-height: 1.6;
3097 }
3098
3099 .subscription-note p {
3100 opacity: 0.7;
3101 font-size: var(--text-note);
3102 }
3103
3104 .features-grid-status {
3105 margin-top: var(--gap-peer);
3106 display: inline-block;
3107 }
3108
3109 /* "Pages" disclosure (project-level markdown sections). */
3110 .pages-toggle { margin-top: var(--gap-page); }
3111 .pages-toggle > summary { cursor: pointer; }
3112 .pages-toggle > summary h2 { display: inline; }
3113 .pages-intro {
3114 font-size: var(--text-note);
3115 opacity: 0.7;
3116 margin: var(--gap-section) 0 var(--gap-section);
3117 }
3118
3119 /* psection-* mirrors section-mgmt-* (used by item_details) but psection
3120 class hooks are also queried by static/project-sections.js. */
3121 .psection-row-title { flex: 1; font-weight: bold; }
3122 .psection-row-anchor { font-size: var(--text-fine); opacity: 0.6; }
3123 .psection-row-length { font-size: var(--text-fine); opacity: 0.6; }
3124
3125 .psection-edit-btn,
3126 .psection-del-btn {
3127 padding: var(--gap-bound) var(--gap-group);
3128 font-size: var(--text-fine);
3129 }
3130
3131 .psection-add-details { margin-top: var(--gap-section); }
3132 .psection-add-details > summary {
3133 cursor: pointer;
3134 font-size: var(--text-body);
3135 }
3136 .psection-add-fields { margin-top: var(--gap-section); }
3137
3138 .psection-edit-modal {
3139 margin-top: var(--gap-section);
3140 padding: var(--gap-section);
3141 background: var(--surface-sunken);
3142 }
3143 .psection-edit-actions {
3144 display: flex;
3145 gap: var(--gap-peer);
3146 }
3147
3148 /* ===========================================
3149 CART PAGE (replaces inline styles in pages/cart.html)
3150 =========================================== */
3151
3152 .cart-empty {
3153 text-align: center;
3154 padding: var(--gap-page) var(--gap-section);
3155 }
3156 .cart-empty-hint {
3157 font-size: var(--text-note);
3158 opacity: 0.7;
3159 margin-bottom: var(--gap-pane);
3160 }
3161
3162 /* Multi-creator summary bar (only renders when seller_groups.len() > 1). */
3163 .cart-multi-bar-note {
3164 opacity: 0.7;
3165 margin-left: var(--gap-peer);
3166 }
3167 .cart-multi-bar-form {
3168 display: flex;
3169 align-items: center;
3170 gap: var(--gap-section);
3171 }
3172 .cart-share-label {
3173 font-size: var(--text-note);
3174 display: flex;
3175 align-items: center;
3176 gap: var(--gap-peer);
3177 }
3178
3179 /* Per-seller group card. */
3180 .cart-group { margin-bottom: var(--gap-page); }
3181 .cart-group-title { margin-bottom: var(--gap-bound); }
3182 .cart-group-count {
3183 font-size: var(--text-note);
3184 opacity: 0.6;
3185 margin-bottom: var(--gap-section);
3186 }
3187
3188 /* PWYW editable price cell. */
3189 .cart-pwyw-cell {
3190 display: flex;
3191 align-items: center;
3192 justify-content: flex-end;
3193 gap: var(--gap-bound);
3194 }
3195 .cart-pwyw-symbol { font-size: var(--text-note); }
3196
3197 /* Compact action button used in cart/wishlist tables. */
3198
3199 /* Bottom summary row of each seller group. */
3200 .cart-group-summary {
3201 margin-top: var(--gap-section);
3202 display: flex;
3203 justify-content: space-between;
3204 align-items: center;
3205 flex-wrap: wrap;
3206 gap: var(--gap-section);
3207 }
3208 .cart-subtotal {
3209 font-family: var(--font-display);
3210 font-size: var(--text-lead);
3211 }
3212 .cart-fee-line {
3213 font-size: var(--text-note);
3214 opacity: 0.6;
3215 }
3216 .cart-savings {
3217 font-size: var(--text-note);
3218 color: var(--action);
3219 margin-top: var(--gap-bound);
3220 }
3221 .cart-stripe-not-ready {
3222 font-size: var(--text-note);
3223 opacity: 0.6;
3224 }
3225
3226 /* Checkout form within a seller group. */
3227 .cart-promo-row { margin-bottom: var(--gap-peer); }
3228 .cart-checkout-share {
3229 display: flex;
3230 align-items: center;
3231 gap: var(--gap-peer);
3232 font-size: var(--text-note);
3233 margin-bottom: var(--gap-peer);
3234 }
3235
3236 /* Wishlist suggestions section below the cart. */
3237 .cart-wishlist { margin-top: var(--gap-page); }
3238
3239 /* Generic "faded out" row state, used by Add-to-Cart click handler in cart.html
3240 to indicate the wishlist row has been moved into the cart. */
3241 .is-faded { opacity: 0.4; }
3242
3243 /* ===========================================
3244 PROFILE TAB (replaces inline styles in partials/tabs/user_profile.html)
3245 =========================================== */
3246
3247 /* Inline form row: input(s) + button laid out horizontally, bottom-aligned.
3248 Distinct from .form-row (which is a 1fr 1fr grid). */
3249 .field-row {
3250 display: flex;
3251 gap: var(--gap-section);
3252 align-items: flex-end;
3253 }
3254 .field-row > .form-group { margin-bottom: 0; }
3255 .field-row > .form-group.is-grow { flex: 1; }
3256 .field-row > .form-group.is-grow-2 { flex: 2; }
3257 .field-row > button { margin-bottom: 0; }
3258
3259 /* Custom domain section. */
3260 .profile-domain-header { margin-bottom: var(--gap-section); }
3261 .profile-domain-status {
3262 margin-left: var(--gap-peer);
3263 }
3264 .profile-domain-status.is-verified { color: var(--success); }
3265 .profile-domain-status.is-pending { color: var(--warning); }
3266
3267 .dns-row {
3268 display: flex;
3269 align-items: center;
3270 gap: var(--gap-peer);
3271 margin-bottom: var(--gap-peer);
3272 }
3273 .dns-row:last-of-type { margin-bottom: 0; }
3274 .dns-row-label {
3275 font-size: var(--text-fine);
3276 opacity: 0.7;
3277 min-width: 70px;
3278 }
3279 .dns-row-value {
3280 font-size: var(--text-note);
3281 flex: 1;
3282 }
3283 .dns-row-copy {
3284 padding: var(--gap-bound) var(--gap-peer);
3285 font-size: var(--text-fine);
3286 }
3287 .profile-domain-actions {
3288 display: flex;
3289 gap: var(--gap-section);
3290 }
3291 .profile-domain-result { margin-top: var(--gap-section); }
3292 .profile-domain-lead {
3293 font-size: var(--text-note);
3294 opacity: 0.8;
3295 }
3296 .profile-domain-add-result { margin-top: var(--gap-section); }
3297
3298 /* Personal feed row: URL input + Copy button. */
3299 .profile-feed-row {
3300 display: flex;
3301 gap: var(--gap-peer);
3302 align-items: center;
3303 }
3304 .profile-feed-input {
3305 flex: 1;
3306 font-size: var(--text-note);
3307 font-family: var(--font-mono);
3308 opacity: 0.8;
3309 }
3310 .profile-feed-lead { margin-bottom: var(--gap-section); }
3311
3312 .purchase-page .checkout-footer {
3313 margin-top: var(--gap-page);
3314 padding-top: var(--gap-pane);
3315 border-top: 1px solid var(--border);
3316 opacity: 0.6;
3317 text-align: center;
3318 font-size: var(--text-note);
3319 }
3320
3321 .purchase-page .checkout-footer a { color: var(--content); }
3322
3323 @media (max-width: 599px) {
3324 .purchase-page .container { padding: 0 var(--gap-section); }
3325 .purchase-page .item-summary { flex-direction: column; }
3326 .purchase-page .item-thumbnail { width: 100%; height: 160px; }
3327 .purchase-page .checkout-box { padding: var(--gap-pane); }
3328 .purchase-page h2 { font-size: var(--text-head); }
3329 }
3330
3331 /* Feed page (src/quasi/feeds.rs). Compact tabular layout of items from
3332 followed users / projects / tags.
3333
3334 The page is described rather than templated as of 2026-08-31, so the
3335 selectors below name what quasi and makeover emit. It has no .container:
3336 a described document is <body> > <main> > the screen's region, so the 900px
3337 measure the container carried sits on <main> here. */
3338 .feed-page main { max-width: 900px; margin: 0 auto; padding: var(--gap-page); }
3339
3340 .feed-page main h1 { font-size: var(--text-head); margin-bottom: var(--gap-section); }
3341
3342 /* The "Showing X-Y of N items" line, which the description states as a plain
3343 text node above the table. */
3344 .feed-page .pane > p.text {
3345 font-size: var(--text-fine);
3346 opacity: 0.6;
3347 margin-bottom: var(--gap-section);
3348 }
3349
3350 .feed-page .table-head {
3351 /* respec-ok: the same five-column grid the rows carry, on the heading row
3352 that has to line up with them. */
3353 display: grid;
3354 grid-template-columns: 50px 1fr 100px 70px 70px;
3355 gap: var(--gap-peer);
3356 padding: var(--gap-peer) var(--gap-section);
3357 background: var(--surface-overlay);
3358 font-size: var(--text-fine);
3359 opacity: 0.7;
3360 text-transform: uppercase;
3361 letter-spacing: 0.03em;
3362 }
3363
3364 .feed-page .table {
3365 border: 1px solid var(--border);
3366 }
3367
3368 .feed-page .table-row {
3369 /* respec-ok: a five-column grid template. The generated `.table-row` sets a
3370 display but has no vocabulary for column tracks, so the tracks and the
3371 display that carries them are stated together here. */
3372 display: grid;
3373 grid-template-columns: 50px 1fr 100px 70px 70px;
3374 gap: var(--gap-peer);
3375 padding: var(--gap-peer) var(--gap-section);
3376 align-items: center;
3377 text-decoration: none;
3378 color: var(--content);
3379 font-size: var(--text-note);
3380 border-bottom: 1px solid var(--border);
3381 transition: background 0.1s ease;
3382 }
3383
3384 .feed-page .table-row:last-child { border-bottom: none; }
3385 .feed-page .table-row:hover { background: var(--surface-sunken); }
3386
3387 /* The two-line name cell: `Cell::part` puts the creator under the name in one
3388 cell, and each part is emitted as its own `.cell-value`. */
3389 .feed-page .col-Name {
3390 display: flex;
3391 flex-direction: column;
3392 gap: var(--gap-bound);
3393 min-width: 0;
3394 }
3395
3396 .feed-page .table-row .col-Name .cell-value:first-child {
3397 white-space: nowrap;
3398 overflow: hidden;
3399 text-overflow: ellipsis;
3400 }
3401
3402 .feed-page .table-row .col-Name .cell-value:last-child {
3403 font-size: var(--text-fine);
3404 opacity: 0.5;
3405 }
3406
3407 /* The feed's badges shout a little: caps and tracking. The sunken fill that
3408 used to come with them went with the filled-badge form in B2. */
3409 .feed-page .badge {
3410 text-transform: uppercase;
3411 letter-spacing: 0.03em;
3412 }
3413
3414 /* The empty state and the numbered strip carry no page-specific rules any
3415 more: `Node::empty` and `Rest` emit their own classes, styled once for every
3416 described screen rather than again per page. */
3417
3418 @media (max-width: 599px) {
3419 .feed-page main { padding: var(--gap-section); }
3420 .feed-page .table-head,
3421 .feed-page .table-row { grid-template-columns: 1fr 70px; }
3422 /* Name and Date survive the narrow viewport, which is what the five-column
3423 header dropped to before the conversion. */
3424 .feed-page .col-Type,
3425 .feed-page .col-Tag,
3426 .feed-page .col-Price { display: none; }
3427 }
3428
3429 /* (Stripe Connect disclaimer page rules defined in the canonical
3430 STRIPE DISCLAIMER PAGE section near end of file.) */
3431
3432 /* The creators page moved to the description layer. Its headings, its ordered
3433 list, its stat box, its four-column table and its call-to-action box are the
3434 design system's now: the count is a `Figure`, the tiers are a `Node::Table`
3435 with stated column widths, and the CTA is one or two `Act`s. `.wave-table`
3436 went with it and had no other user. */
3437
3438 /* Tag tree browse page (templates/pages/tag_tree.html). */
3439 .tag-tree-page .breadcrumbs {
3440 font-size: var(--text-note);
3441 margin-bottom: var(--gap-pane);
3442 opacity: 0.7;
3443 }
3444 .tag-tree-page .breadcrumbs a:hover { opacity: 0.6; }
3445 .tag-tree-page .breadcrumbs .sep { margin: 0 var(--gap-bound); opacity: 0.4; }
3446
3447 .tag-tree-page .tag-grid {
3448 display: grid;
3449 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
3450 gap: var(--gap-section);
3451 }
3452
3453 .tag-tree-page .tag-card {
3454 background: var(--surface-sunken);
3455 padding: var(--gap-section);
3456 text-decoration: none;
3457 color: var(--content);
3458 border: 1px solid var(--border);
3459 transition: background 0.1s ease;
3460 }
3461
3462 .tag-tree-page .tag-card:hover {
3463 background: var(--surface-sunken);
3464 text-decoration: none;
3465 }
3466
3467 .tag-tree-page .tag-card-name {
3468 font-family: var(--font-display);
3469 font-weight: bold;
3470 font-size: var(--text-body);
3471 }
3472
3473 .tag-tree-page .tag-card-meta {
3474 font-size: var(--text-fine);
3475 opacity: 0.5;
3476 margin-top: var(--gap-bound);
3477 }
3478
3479 .tag-tree-page .empty-state {
3480 opacity: 0.5;
3481 font-size: var(--text-note);
3482 padding: var(--gap-page) 0;
3483 }
3484
3485 /* Confirm delete prompt (templates/pages/confirm_delete.html). Overrides
3486 the global .warning-box to use --danger-bg (matches the delete-account
3487 semantic). Also tokenized previously-broken fallbacks. */
3488 .confirm-delete-page .warning-box {
3489 background: color-mix(in oklch, var(--danger) 10%, var(--surface-page));
3490 border: 1px solid var(--danger);
3491 border-radius: var(--radius-panel);
3492 padding: var(--gap-pane);
3493 margin-bottom: var(--gap-pane);
3494 }
3495
3496 .confirm-delete-page .warning-box p {
3497 margin: 0;
3498 color: var(--danger);
3499 }
3500
3501 /* Project paywall page (templates/pages/project_paywall.html). */
3502 .project-paywall-page .paywall-cover { margin-bottom: var(--gap-pane); }
3503 .project-paywall-page .paywall-cover img {
3504 width: 120px;
3505 height: 120px;
3506 border-radius: var(--radius-panel);
3507 object-fit: cover;
3508 }
3509
3510 .project-paywall-page .paywall-title {
3511 font-family: var(--font-display);
3512 font-size: var(--text-hero);
3513 margin-bottom: var(--gap-peer);
3514 }
3515
3516 .project-paywall-page .paywall-creator {
3517 font-size: var(--text-body);
3518 opacity: 0.7;
3519 margin-bottom: var(--gap-pane);
3520 }
3521
3522 .project-paywall-page .paywall-creator a { color: var(--content); }
3523
3524 .project-paywall-page .paywall-description {
3525 font-size: var(--text-lead);
3526 max-width: 800px;
3527 margin: 0 auto var(--gap-page);
3528 text-align: left;
3529 }
3530
3531 .project-paywall-page .paywall-item-count {
3532 font-size: var(--text-body);
3533 opacity: 0.7;
3534 margin-bottom: var(--gap-peer);
3535 }
3536
3537 .project-paywall-page .paywall-tiers {
3538 display: grid;
3539 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
3540 gap: var(--gap-pane);
3541 margin-top: var(--gap-pane);
3542 text-align: left;
3543 }
3544
3545 .project-paywall-page .paywall-tiers .tier-card {
3546 background: var(--surface-overlay);
3547 padding: var(--gap-page);
3548 display: flex;
3549 flex-direction: column;
3550 }
3551
3552 .project-paywall-page .paywall-tiers .tier-card h3 {
3553 font-family: var(--font-display);
3554 font-weight: bold;
3555 font-size: var(--text-subhead);
3556 margin-bottom: var(--gap-peer);
3557 }
3558
3559 .project-paywall-page .paywall-tiers .tier-price {
3560 font-size: var(--text-head);
3561 margin-bottom: var(--gap-section);
3562 }
3563
3564 .project-paywall-page .paywall-tiers .tier-card form,
3565 .project-paywall-page .paywall-tiers .tier-card .button { margin-top: auto; }
3566
3567 .project-paywall-page .paywall-tiers .tier-card button,
3568 .project-paywall-page .paywall-tiers .tier-card .button { width: 100%; }
3569
3570 /* Collection detail page (templates/pages/collection.html). */
3571 /* The collection page moved to the description layer. Its header, its owner
3572 line, its per-item rows and its empty state are the design system's now: the
3573 items are a `Node::Table` whose meta line became three droppable columns
3574 rather than one string that wraps, and the empty state is `Node::empty`.
3575 What is left is the measure, on <main>. */
3576 .collection-page main { max-width: 700px; }
3577
3578 /* Project blog index page. Twelve rules stood here for markup the template
3579 wrote by hand -- `.blog-header`, `.post-entry`, `.post-title` and the rest.
3580 The page is a described screen (`crate::quasi::project_blog`) and its
3581 heading, list and rows are makeover's, which is what the design system is
3582 for. What is left is the one thing that is this page's rather than the
3583 system's. */
3584 .project-blog-page main { max-width: 800px; }
3585
3586 /* Canonical "page-level h1", left-aligned, smaller than the global centered
3587 default. Used by dashboards, admin, health, import, delete-account, export,
3588 receipt. Aliased via comma-grouped selectors so each page picks it up via
3589 its body class without redefining. */
3590 .dashboard-page h1,
3591 .admin-page h1,
3592 .health-page h1,
3593 .import-page h1,
3594 .delete-account-page h1,
3595 .export-page h1,
3596 .receipt-page h1 {
3597 font-size: var(--text-title);
3598 margin-bottom: var(--gap-peer);
3599 text-align: left;
3600 }
3601 .delete-account-page h1 { color: var(--danger); }
3602
3603 /* Canonical subtitle under a page h1, muted, with bottom space. */
3604 .health-page .subtitle,
3605 .import-page .subtitle,
3606 .delete-account-page .subtitle,
3607 .export-page .subtitle {
3608 opacity: 0.7;
3609 margin-bottom: var(--gap-page);
3610 text-align: left;
3611 }
3612 .health-page .subtitle { text-align: center; }
3613
3614 /* User Settings tab (templates/partials/tabs/user_settings.html).
3615 Left-rail nav layout for the Settings dashboard tab. Selection uses the
3616 canonical .is-selected modifier. */
3617 .settings-layout { display: flex; gap: var(--gap-page); }
3618 .settings-nav { flex-shrink: 0; min-width: 140px; }
3619 .settings-nav a {
3620 display: block;
3621 padding: var(--gap-peer) var(--gap-section);
3622 text-decoration: none;
3623 color: var(--content);
3624 opacity: 0.6;
3625 font-family: var(--font-mono);
3626 font-size: var(--text-note);
3627 transition: opacity 0.15s ease;
3628 }
3629 .settings-nav a:hover { opacity: 1; }
3630 .settings-nav a.is-selected { opacity: 1; background: var(--surface-sunken); }
3631 .settings-body { flex: 1; min-width: 0; }
3632 @media (max-width: 839px) {
3633 .settings-layout { flex-direction: column; gap: var(--gap-section); }
3634 .settings-nav { display: flex; flex-wrap: wrap; gap: 0; min-width: 0; }
3635 .settings-nav a { padding: var(--gap-peer) var(--gap-group); font-size: var(--text-note); }
3636 }.library-page .feed-table-header {
3637 display: grid;
3638 grid-template-columns: 50px 1fr 100px 70px 70px;
3639 gap: var(--gap-peer);
3640 padding: var(--gap-peer) var(--gap-section);
3641 background: var(--surface-overlay);
3642 font-size: var(--text-fine);
3643 opacity: 0.7;
3644 text-transform: uppercase;
3645 letter-spacing: 0.03em;
3646 }
3647 .library-page .feed-col-right { text-align: right; }
3648 .library-page .feed-results-table { border: 1px solid var(--border); border-top: none; }
3649 .library-page .feed-table-row {
3650 display: grid;
3651 grid-template-columns: 50px 1fr 100px 70px 70px;
3652 gap: var(--gap-peer);
3653 padding: var(--gap-peer) var(--gap-section);
3654 align-items: center;
3655 text-decoration: none;
3656 color: var(--content);
3657 font-size: var(--text-note);
3658 border-bottom: 1px solid var(--border);
3659 transition: background 0.1s ease;
3660 }
3661 .library-page .feed-table-row:last-child { border-bottom: none; }
3662 .library-page .feed-table-row:hover { background: var(--surface-sunken); }
3663 .library-page .feed-item-name-cell {
3664 display: flex;
3665 flex-direction: column;
3666 gap: var(--gap-bound);
3667 min-width: 0;
3668 }
3669 .library-page .feed-item-name {
3670 white-space: nowrap;
3671 overflow: hidden;
3672 text-overflow: ellipsis;
3673 }
3674 .library-page .feed-item-creator { font-size: var(--text-fine); opacity: 0.5; }
3675 .library-page .feed-pagination {
3676 display: flex;
3677 gap: var(--gap-bound);
3678 justify-content: center;
3679 margin-top: var(--gap-section);
3680 }
3681 .library-page .feed-pagination a,
3682 .library-page .feed-pagination span {
3683 padding: var(--gap-peer) var(--gap-section);
3684 font-size: var(--text-note);
3685 text-decoration: none;
3686 color: var(--content);
3687 border: 1px solid var(--border);
3688 }
3689 .library-page .feed-pagination span.current {
3690 background: var(--primary-dark);
3691 color: var(--primary-light);
3692 border-color: var(--primary-dark);
3693 }
3694 .library-page .feed-pagination a:hover { background: var(--surface-sunken); }
3695 @media (max-width: 599px) {
3696 .library-page .feed-table-header,
3697 .library-page .feed-table-row {
3698 grid-template-columns: 1fr 70px;
3699 }
3700 .library-page .feed-table-header span:nth-child(1),
3701 .library-page .feed-table-row .badge:first-child,
3702 .library-page .feed-table-header span:nth-child(3),
3703 .library-page .feed-table-header span:nth-child(4),
3704 .library-page .feed-table-row span:nth-child(3),
3705 .library-page .feed-table-row span:nth-child(4) {
3706 display: none;
3707 }
3708 }
3709
3710 /* User-level dashboard (templates/dashboards/dashboard-user.html).
3711 Scoped under .dashboard-user-page to avoid colliding with .project-card
3712 / .project-title / .project-meta on the public profile (user.html) and
3713 the user_projects tab partial. */
3714 .dashboard-user-page .stat-value { font-size: var(--text-title); margin-bottom: var(--gap-bound); }
3715
3716 .dashboard-user-page .project-card {
3717 background: var(--surface-overlay);
3718 padding: var(--gap-pane);
3719 margin-bottom: var(--gap-section);
3720 display: flex;
3721 justify-content: space-between;
3722 align-items: flex-start;
3723 }
3724
3725 .dashboard-user-page .project-info { flex: 1; }
3726
3727 .dashboard-user-page .project-title {
3728 font-family: var(--font-display);
3729 font-weight: bold;
3730 font-size: var(--text-subhead);
3731 margin-bottom: var(--gap-bound);
3732 }
3733
3734 .dashboard-user-page .project-meta {
3735 font-size: var(--text-note);
3736 opacity: 0.7;
3737 margin-bottom: var(--gap-peer);
3738 }
3739
3740 .dashboard-user-page .project-stats { font-size: var(--text-note); }
3741 .dashboard-user-page .project-actions {
3742 display: flex;
3743 gap: var(--gap-peer);
3744 }
3745
3746 .dashboard-user-page .project-actions button {
3747 padding: var(--gap-peer) var(--gap-section);
3748 font-size: var(--text-note);
3749 }
3750 @media (max-width: 839px) {
3751 .dashboard-user-page .project-card { flex-direction: column; }
3752 .dashboard-user-page .project-actions { margin-top: var(--gap-section); }
3753 }
3754
3755 /* Project dashboard (templates/dashboards/dashboard-project.html). */
3756 .dashboard-project-page .project-meta { font-size: var(--text-note); opacity: 0.7; }
3757 .dashboard-project-page .stat-value { font-size: var(--text-title); margin-bottom: var(--gap-bound); }
3758
3759 .dashboard-project-page .content-header {
3760 display: flex;
3761 justify-content: space-between;
3762 align-items: center;
3763 margin-bottom: var(--gap-pane);
3764 }
3765
3766 .dashboard-project-page .content-header h2 { font-size: var(--text-subhead); }
3767
3768 .dashboard-project-page .item-thumbnail {
3769 width: 40px;
3770 height: 40px;
3771 background: var(--border);
3772 display: inline-block;
3773 vertical-align: middle;
3774 margin-right: var(--gap-peer);
3775 }
3776
3777 .dashboard-project-page .item-actions {
3778 display: flex;
3779 gap: var(--gap-peer);
3780 }
3781
3782 .dashboard-project-page .item-actions button {
3783 padding: var(--gap-peer) var(--gap-section);
3784 font-size: var(--text-note);
3785 }
3786
3787 .dashboard-project-page .data-section { margin-bottom: var(--gap-page); }
3788
3789 .dashboard-project-page .data-section h2 {
3790 font-size: var(--text-subhead);
3791 margin-bottom: var(--gap-section);
3792 padding-bottom: var(--gap-peer);
3793 border-bottom: 1px solid var(--border);
3794 }
3795
3796 /* Item dashboard (templates/dashboards/dashboard-item.html). */
3797 .dashboard-item-page .item-meta {
3798 font-size: var(--text-note);
3799 opacity: 0.7;
3800 display: flex;
3801 gap: var(--gap-section);
3802 align-items: center;
3803 }
3804
3805 .dashboard-item-page .stat-value { font-size: var(--text-head); }
3806
3807 .dashboard-item-page .section-header {
3808 display: flex;
3809 justify-content: space-between;
3810 align-items: center;
3811 margin-bottom: var(--gap-pane);
3812 padding-bottom: var(--gap-section);
3813 border-bottom: 1px solid var(--border);
3814 }
3815
3816 .dashboard-item-page .section-header h2 { font-size: var(--text-subhead); }
3817
3818 @media (max-width: 839px) {
3819 .dashboard-item-page .section-header {
3820 flex-direction: column;
3821 align-items: flex-start;
3822 gap: var(--gap-section);
3823 }
3824 .dashboard-item-page .section-header .action-buttons { width: 100%; }
3825 }
3826
3827 /* Changelog page (templates/pages/changelog.html). */
3828 .changelog-page .container { max-width: 900px; margin: 0 auto; }
3829 .changelog-page h1 { font-size: var(--text-display); margin-bottom: var(--gap-peer); }
3830 .changelog-page .changelog-entry {
3831 margin-bottom: var(--gap-page);
3832 padding-bottom: var(--gap-page);
3833 border-bottom: 1px solid var(--border);
3834 }
3835 .changelog-page .changelog-entry:last-child { border-bottom: none; }
3836 .changelog-page .changelog-date {
3837 font-family: var(--font-mono);
3838 font-size: var(--text-note);
3839 opacity: 0.6;
3840 margin-bottom: var(--gap-peer);
3841 }
3842 .changelog-page .changelog-entry h2 { font-size: var(--text-subhead); margin-bottom: var(--gap-section); }
3843 .changelog-page .changelog-entry ul {
3844 padding-left: var(--gap-pane);
3845 margin: var(--gap-peer) 0;
3846 line-height: 1.7;
3847 }
3848 .changelog-page .changelog-entry li { margin-bottom: var(--gap-bound); }
3849
3850 /* Policy page (templates/pages/policy.html). */
3851 /* The content policy moved to the description layer. Its headings, its section
3852 spacing and its list indents are the design system's now; the prose itself
3853 goes through docengine, the same renderer `/docs/*` uses. What is left is the
3854 measure, on <main> rather than on a `.container`, as every converted document
3855 does. */
3856 .policy-page main { max-width: 900px; margin: 0 auto; }
3857
3858 /* Admin page extras (in addition to global .admin-page h1). */
3859 .admin-page .metrics-grid {
3860 display: grid;
3861 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
3862 gap: var(--gap-section);
3863 margin-bottom: var(--gap-page);
3864 }
3865
3866 .admin-page .metric-card {
3867 padding: var(--gap-section);
3868 border: 1px solid var(--border);
3869 border-radius: var(--radius-square);
3870 }
3871
3872 .admin-page .metric-card .value {
3873 font-size: var(--text-title);
3874 font-family: var(--font-mono);
3875 font-weight: 600;
3876 }
3877
3878 .admin-page .metric-card .label {
3879 font-size: var(--text-note);
3880 color: var(--content-muted);
3881 margin-top: var(--gap-bound);
3882 }
3883
3884 .admin-page .metric-card.warn .value { color: var(--warning); }
3885 .admin-page .metric-card.error .value { color: var(--danger); }
3886
3887 .admin-page .section-heading {
3888 font-size: var(--text-subhead);
3889 margin: var(--gap-pane) 0 var(--gap-section);
3890 font-family: var(--font-mono);
3891 }
3892
3893 /* Scan pipeline dashboard. See docs/scan-pipeline-audit.md § 5. */
3894
3895 .admin-scan-section {
3896 margin-top: var(--gap-page);
3897 }
3898
3899 .admin-section-header {
3900 display: flex;
3901 justify-content: space-between;
3902 align-items: baseline;
3903 margin-bottom: var(--gap-section);
3904 gap: var(--gap-section);
3905 }
3906
3907 /* Typeface comes from .subsection-title on the heading itself; this sets only
3908 what the flex row needs. */
3909 .admin-section-header h2 {
3910 font-size: var(--text-lead);
3911 margin: 0;
3912 }
3913
3914 .layer-health-grid {
3915 display: grid;
3916 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
3917 gap: var(--gap-section);
3918 }
3919
3920 .layer-health-card {
3921 padding: var(--gap-section);
3922 border: 1px solid var(--border);
3923 border-radius: var(--radius-square);
3924 border-left-width: 4px;
3925 font-size: var(--text-note);
3926 }
3927
3928 .layer-health-card .layer-name {
3929 font-family: var(--font-mono);
3930 font-weight: 600;
3931 font-size: var(--text-body);
3932 margin-bottom: var(--gap-peer);
3933 }
3934
3935 .layer-health-card .layer-stat {
3936 display: flex;
3937 justify-content: space-between;
3938 margin-bottom: 2px;
3939 }
3940
3941 .layer-health-card .layer-stat .dimmed {
3942 color: var(--content-muted);
3943 }
3944
3945 .layer-health-card.layer-ok { border-left-color: var(--success); }
3946 .layer-health-card.layer-degraded { border-left-color: var(--warning); }
3947 .layer-health-card.layer-down { border-left-color: var(--danger); }
3948
3949 .layer-chips {
3950 display: flex;
3951 flex-wrap: wrap;
3952 gap: 4px;
3953 max-width: 360px;
3954 }
3955
3956 .layer-chip {
3957 display: inline-block;
3958 font-family: var(--font-mono);
3959 font-size: var(--text-fine);
3960 padding: 1px 6px;
3961 border-radius: var(--radius-control);
3962 border: 1px solid transparent;
3963 cursor: help;
3964 line-height: 1.4;
3965 }
3966
3967 .layer-chip-pass { background: color-mix(in oklch, var(--success) 12%, var(--surface-page)); border-color: var(--success); color: var(--success); }
3968 .layer-chip-skip { background: var(--surface-page); border-color: var(--border); color: var(--content-muted); }
3969 .layer-chip-fail { background: color-mix(in oklch, var(--danger) 10%, var(--surface-page)); border-color: var(--danger); color: var(--danger); font-weight: 600; }
3970 .layer-chip-error { background: color-mix(in oklch, var(--warning) 14%, var(--surface-page)); border-color: var(--warning); color: var(--warning); font-weight: 600; }
3971
3972 .scan-history-grid table { font-size: var(--text-note); }
3973
3974 .scan-history-grid summary {
3975 cursor: pointer;
3976 font-family: var(--font-mono);
3977 padding: var(--gap-peer) 0;
3978 user-select: none;
3979 }
3980
3981 .scan-row-meta {
3982 font-size: var(--text-fine);
3983 color: var(--content-muted);
3984 margin-top: 2px;
3985 }
3986
3987 .audit-filter-form {
3988 display: flex;
3989 flex-wrap: wrap;
3990 align-items: flex-end;
3991 gap: var(--gap-section);
3992 padding: var(--gap-section) 0;
3993 margin-bottom: var(--gap-section);
3994 border-bottom: 1px solid var(--border);
3995 }
3996
3997 .audit-filter-form label {
3998 display: flex;
3999 flex-direction: column;
4000 gap: 2px;
4001 }
4002
4003 .audit-filter-form input,
4004 .audit-filter-form select {
4005 font-size: var(--text-note);
4006 }
4007
4008 .layer-chips-cell { vertical-align: top; }
4009
4010 .layer-detail-disclosure {
4011 margin-top: var(--gap-peer);
4012 }
4013
4014 .layer-detail-disclosure summary {
4015 cursor: pointer;
4016 user-select: none;
4017 padding: 2px 0;
4018 }
4019
4020 .layer-detail-list {
4021 margin: var(--gap-peer) 0 0;
4022 display: grid;
4023 grid-template-columns: max-content 1fr;
4024 gap: 2px var(--gap-section);
4025 font-size: var(--text-fine);
4026 }
4027
4028 .layer-detail-list dt {
4029 font-family: var(--font-mono);
4030 }
4031
4032 .layer-detail-list dd {
4033 margin: 0;
4034 color: var(--content-muted);
4035 word-break: break-word;
4036 }
4037
4038 .admin-page table {
4039 width: 100%;
4040 border-collapse: collapse;
4041 font-size: var(--text-note);
4042 }
4043
4044 .admin-page th,
4045 .admin-page td {
4046 text-align: left;
4047 padding: var(--gap-peer) var(--gap-group);
4048 border-bottom: 1px solid var(--border);
4049 }
4050
4051 .admin-page th {
4052 font-family: var(--font-mono);
4053 font-size: var(--text-fine);
4054 text-transform: uppercase;
4055 color: var(--content-muted);
4056 }
4057
4058 .admin-page td.mono { font-family: var(--font-mono); }
4059
4060 .admin-page .refresh-note {
4061 font-size: var(--text-fine);
4062 color: var(--content-muted);
4063 margin-bottom: var(--gap-section);
4064 font-family: var(--font-mono);
4065 }
4066
4067 .admin-page .appeal-count {
4068 font-size: var(--text-note);
4069 opacity: 0.7;
4070 margin-bottom: var(--gap-pane);
4071 }
4072
4073 .admin-page .decide-form {
4074 display: flex;
4075 gap: var(--gap-peer);
4076 align-items: flex-end;
4077 flex-wrap: wrap;
4078 }
4079
4080 .admin-page .decide-form textarea {
4081 min-width: 200px;
4082 font-size: var(--text-note);
4083 }
4084
4085 .admin-page .suspend-form {
4086 display: flex;
4087 gap: var(--gap-peer);
4088 align-items: flex-end;
4089 }
4090
4091 .admin-page .suspend-form input[type="text"] {
4092 min-width: 200px;
4093 font-size: var(--text-note);
4094 }
4095
4096 .admin-page .lottery-form {
4097 background: var(--surface-overlay);
4098 padding: var(--gap-pane);
4099 margin: var(--gap-pane) 0;
4100 display: flex;
4101 gap: var(--gap-section);
4102 align-items: flex-end;
4103 }
4104
4105 .admin-page .lottery-form .form-group { margin-bottom: 0; }
4106
4107 .admin-page .lottery-form label {
4108 font-size: var(--text-note);
4109 display: block;
4110 margin-bottom: var(--gap-bound);
4111 }
4112
4113 .admin-page .lottery-form input[type="number"] { width: 80px; }
4114
4115 /* Page-top notice (charter primitive, replaces #restart-banner and
4116 the inline sandbox banner in dashboard-project). Full-bleed across
4117 the viewport, monospace, one short sentence + optional action.
4118 Hero callouts on the landing page use `.alert` instead (multi-paragraph). */
4119 .banner {
4120 padding: var(--gap-section) var(--gap-pane);
4121 font-family: var(--font-mono);
4122 font-size: var(--text-note);
4123 text-align: center;
4124 color: var(--content);
4125 }
4126
4127 .banner--info {
4128 background: var(--highlight-faint);
4129 border-bottom: 2px solid var(--focus-ring);
4130 }
4131
4132 .banner--warning {
4133 background: color-mix(in oklch, var(--warning) 14%, var(--surface-page));
4134 border-bottom: 2px solid var(--warning);
4135 }
4136
4137 /* A described `Node::Notice` carries its tone as an attribute rather than in
4138 the class, which is what `quasi-webview` emits and what `makeover` keys every
4139 other tone off. The two `banner--` modifiers above are this site's own
4140 full-bleed notices and are untouched. */
4141 .banner[data-tone="danger"] {
4142 background: color-mix(in oklch, var(--danger) 14%, var(--surface-page));
4143 border-bottom: 2px solid var(--danger);
4144 }
4145
4146 .banner[data-tone="success"] {
4147 background: color-mix(in oklch, var(--success) 14%, var(--surface-page));
4148 border-bottom: 2px solid var(--success);
4149 }
4150
4151 .banner[data-tone="info"] {
4152 background: var(--highlight-faint);
4153 border-bottom: 2px solid var(--focus-ring);
4154 }
4155
4156 .banner a {
4157 color: inherit;
4158 text-decoration: underline;
4159 }
4160
4161 /* ===========================================
4162 SECTIONS
4163 =========================================== */
4164
4165 .section-header {
4166 font-size: var(--text-head);
4167 font-weight: normal;
4168 margin-bottom: var(--gap-section);
4169 padding-bottom: var(--gap-peer);
4170 border-bottom: 1px solid var(--border);
4171 font-family: var(--font-mono);
4172 }
4173
4174 /* Canonical "page section box", light-background card with padding + bottom
4175 space. One of these in the codebase. Aliased to the per-page class names
4176 that were just relabels of this same shape. Templates can use the alias
4177 name OR add `.content-section` as a co-class. */
4178 /* The canonical panel. Every one of these carries `.raised` in the markup
4179 now, so the fill and the bevel come from layout.css and this rule is the
4180 spacing only. The background it used to declare was --surface-overlay,
4181 which .raised spells --surface-raised; equal on the platform default and a
4182 theme may set them apart, the same move .card and .buy-card made. */
4183 .content-section,
4184 .import-page .import-form,
4185 .import-page .import-progress,
4186 .library-page .library-section,
4187 .library-page .library-downloads,
4188 .library-page .downloads-hero,
4189 .receipt-page .receipt-box,
4190 .stripe-disclaimer-page .disclaimer-box {
4191 padding: var(--gap-page);
4192 margin-bottom: var(--gap-page);
4193 }
4194
4195 /* Variants of the canonical shape that want top-spacing instead of bottom,
4196 or a tighter --gap-pane padding. */
4197 .library-page .library-downloads,
4198 .receipt-page .receipt-box { margin-bottom: 0; margin-top: var(--gap-pane); }
4199 .import-page .import-progress { padding: var(--gap-pane); margin-bottom: var(--gap-section); }
4200 .stripe-disclaimer-page .disclaimer-box { margin-bottom: var(--gap-pane); }
4201
4202 .form-section {
4203 margin-bottom: var(--gap-page);
4204 }
4205
4206 .section-group-label {
4207 font-family: var(--font-mono);
4208 font-size: var(--text-fine);
4209 font-weight: bold;
4210 text-transform: uppercase;
4211 letter-spacing: 0.05em;
4212 color: var(--content-muted);
4213 margin: var(--gap-page) 0 var(--gap-section) 0;
4214 padding-top: var(--gap-pane);
4215 border-top: 2px solid var(--border);
4216 }
4217
4218 .form-section h2 {
4219 font-size: var(--text-subhead);
4220 font-weight: normal;
4221 margin-bottom: var(--gap-section);
4222 padding-bottom: var(--gap-peer);
4223 border-bottom: 1px solid var(--border);
4224 }
4225
4226 details.form-section > summary {
4227 cursor: pointer;
4228 list-style: disclosure-closed;
4229 }
4230
4231 details.form-section[open] > summary {
4232 list-style: disclosure-open;
4233 margin-bottom: var(--gap-section);
4234 }
4235
4236 details.form-section > summary > h2 {
4237 display: inline;
4238 }
4239
4240 details.form-section > summary:hover > h2 {
4241 color: var(--content);
4242 }
4243
4244 /* ===========================================
4245 NAVIGATION
4246 =========================================== */
4247
4248 nav {
4249 margin-bottom: var(--gap-page);
4250 }
4251
4252 .nav-links {
4253 display: flex;
4254 gap: var(--gap-page);
4255 flex-wrap: wrap;
4256 }
4257
4258 .nav-links a {
4259 color: var(--content);
4260 text-decoration: none;
4261 font-family: var(--font-mono);
4262 transition: opacity 0.2s ease;
4263 }
4264
4265 .nav-links a:hover {
4266 opacity: 0.6;
4267 }
4268
4269 .shortcuts-help-btn {
4270 font-family: var(--font-mono);
4271 color: var(--content-muted);
4272 font-size: var(--text-note);
4273 width: 1.5rem;
4274 height: 1.5rem;
4275 border: 1px solid var(--border);
4276 border-radius: var(--radius-control);
4277 display: inline-flex;
4278 align-items: center;
4279 justify-content: center;
4280 cursor: pointer;
4281 transition: opacity 0.2s ease;
4282 padding: 0;
4283 background: none;
4284 }
4285
4286 .shortcuts-help-btn:hover {
4287 opacity: 0.6;
4288 }
4289
4290 .breadcrumb {
4291 font-size: var(--text-note);
4292 color: var(--content-muted);
4293 margin-bottom: var(--gap-peer);
4294 font-family: var(--font-mono);
4295 }
4296
4297 .breadcrumb a {
4298 color: var(--content);
4299 text-decoration: none;
4300 }
4301
4302 .breadcrumb a:hover {
4303 text-decoration: underline;
4304 }
4305
4306 /* ===========================================
4307 HEADER & FOOTER
4308 =========================================== */
4309
4310 header {
4311 background: transparent;
4312 border-bottom: none;
4313 padding: 0;
4314 margin-bottom: var(--gap-page);
4315 }
4316
4317 footer {
4318 text-align: center;
4319 font-family: var(--font-mono);
4320 color: var(--content-muted);
4321 }
4322
4323 /* Site-wide header.
4324 The class names are quasi-webview's, not this stylesheet's: the header is a
4325 `quasi_router::Chrome::band` and `crate::shell::site_header` builds the
4326 description rather than the markup. A rule here has to match what that
4327 renderer emits, and `quasi_webview::vocabulary` is the list of what it may. */
4328 .chrome-band {
4329 display: flex;
4330 justify-content: space-between;
4331 align-items: center;
4332 padding: var(--gap-section) var(--gap-pane);
4333 margin-bottom: var(--gap-pane);
4334 }
4335
4336 .chrome-band .chrome-nav {
4337 margin: 0;
4338 }
4339
4340 /* The places are a `ul` of `li`, which is what a nav of addresses is. The
4341 markers and the indent go; the row is the layout. */
4342 .chrome-nav ul {
4343 display: flex;
4344 gap: var(--gap-pane);
4345 flex-wrap: wrap;
4346 list-style: none;
4347 margin: 0;
4348 padding: 0;
4349 }
4350
4351 .chrome-place {
4352 color: var(--content);
4353 text-decoration: none;
4354 font-family: var(--font-mono);
4355 cursor: pointer;
4356 transition: opacity 0.2s ease;
4357 }
4358
4359 .chrome-place:hover {
4360 opacity: 0.6;
4361 }
4362
4363 /* The cart place is offered on every signed-in page and drawn only when the
4364 cart holds something. How many items that is is not known when the document
4365 is rendered, so `core/cart-badge.js` asks `/api/cart/count` and writes the
4366 answer onto the place as `data-count`; the presence is that answer, rather
4367 than a class the markup ships hidden and a script takes off.
4368 The `li` is what hides, not the anchor: an empty item in a flex row still
4369 pays its gap. */
4370 .chrome-nav li:has(> [data-place="cart"]) {
4371 display: none;
4372 }
4373
4374 .chrome-nav li:has(> [data-place="cart"][data-count]) {
4375 display: block;
4376 }
4377
4378 .chrome-brand {
4379 font-family: var(--font-display);
4380 font-size: var(--text-subhead);
4381 text-decoration: none;
4382 color: var(--content);
4383 }
4384
4385 .chrome-brand:hover {
4386 opacity: 0.8;
4387 }
4388
4389 /* Header search */
4390 .chrome-search {
4391 flex: 0 1 220px;
4392 margin: 0 var(--gap-section);
4393 }
4394
4395 /* The box carries a real `<label>` now, where the hand-written header carried
4396 an `aria-label`. Hidden rather than dropped: the same name is read out, and a
4397 description that stopped naming its own question would be a worse
4398 description for the sake of a stylesheet. */
4399 .chrome-search .form-label {
4400 position: absolute;
4401 width: 1px;
4402 height: 1px;
4403 padding: 0;
4404 margin: -1px;
4405 overflow: hidden;
4406 clip: rect(0, 0, 0, 0);
4407 white-space: nowrap;
4408 border: 0;
4409 }
4410
4411 .chrome-search .form-group {
4412 margin: 0;
4413 }
4414
4415 /* Only what makeover does not already say about a `.field`. The surface, the
4416 text colour and the focus ring are the design system's, and the drift check
4417 in `build.rs` fails the build for restating one of them here. The old
4418 `.header-search input` rules did restate all four; they were written before
4419 the box was a described field. */
4420 .chrome-search .field {
4421 width: 100%;
4422 font-size: var(--text-note);
4423 font-family: var(--font-mono);
4424 }
4425
4426 /* The disclosure, hidden by default and shown at 768px. The checkbox holds the
4427 state and the label is the control: `Disclose::Narrow` says the places are
4428 worth hiding when there is no room, and this is the browser's answer to it. */
4429 .chrome-disclose-state {
4430 display: none;
4431 }
4432
4433 .chrome-disclose {
4434 display: none;
4435 cursor: pointer;
4436 padding: var(--gap-peer);
4437 z-index: var(--z-header);
4438 }
4439
4440 .chrome-disclose span {
4441 display: block;
4442 width: 22px;
4443 height: 2px;
4444 background: var(--content);
4445 margin: 5px 0;
4446 transition: transform 0.2s ease, opacity 0.2s ease;
4447 }
4448
4449 /* ===========================================
4450 LAYOUT UTILITIES
4451 =========================================== */
4452
4453 .action-buttons {
4454 display: flex;
4455 gap: var(--gap-section);
4456 flex-wrap: wrap;
4457 }
4458 /* ===========================================
4459 SPECIFIC COMPONENTS
4460 =========================================== */
4461
4462 /* Login container.
4463 The id as well as the class: the four auth pages are described screens now
4464 (`crate::quasi::auth_pages`) and a description names a region, never a class,
4465 so the region's id is what a rule can key off. The class stays for the six
4466 templated pages that still write it. */
4467 #login-container,
4468 .login-container {
4469 display: flex;
4470 flex-direction: column;
4471 max-width: 400px;
4472 width: 100%;
4473 margin: 0 auto;
4474 background: var(--surface-overlay);
4475 padding: var(--gap-page);
4476 }
4477 /* Logo */
4478 .logo {
4479 font-family: var(--font-display);
4480 color: var(--content);
4481 font-size: var(--text-head);
4482 }
4483
4484 .tagline {
4485 font-family: var(--font-mono);
4486 color: var(--content-muted);
4487 text-align: center;
4488 }
4489
4490 .centered-page .tagline {
4491 margin-bottom: var(--gap-bound);
4492 }
4493 /* Skip link */
4494 .skip-link {
4495 text-align: center;
4496 margin-top: var(--gap-section);
4497 }
4498
4499 .skip-link a {
4500 color: var(--primary-dark);
4501 text-decoration: none;
4502 font-size: var(--text-note);
4503 opacity: 0.7;
4504 }
4505
4506 .skip-link a:hover {
4507 text-decoration: underline;
4508 }
4509
4510 /* Footer link */
4511 .foot-link {
4512 margin-top: 0.5em;
4513 }
4514
4515 /* The described auth pages have no `.foot-link` wrapper: a description says
4516 there is a link, and a `div` around it to make it a block is markup with no
4517 meaning in it. Stacking them is this stylesheet's answer, which is where a
4518 layout question a description declines belongs. */
4519 #login-container > .link {
4520 display: block;
4521 margin-top: 0.5em;
4522 }
4523
4524 /* Status indicators */
4525 .status-indicator {
4526 display: inline-block;
4527 width: 8px;
4528 height: 8px;
4529 background: var(--primary-dark);
4530 margin-right: var(--gap-peer);
4531 }
4532
4533 .status-indicator.active {
4534 background: var(--success);
4535 }
4536
4537 /* Amount styling */
4538 .amount-in {
4539 color: var(--success);
4540 }
4541
4542 .amount-out {
4543 opacity: 0.7;
4544 }
4545
4546 /* File upload */
4547 .file-upload-area {
4548 background: var(--surface-sunken);
4549 padding: var(--gap-page);
4550 text-align: center;
4551 cursor: pointer;
4552 transition: background 0.2s ease;
4553 margin-bottom: var(--gap-section);
4554 }
4555
4556 .file-upload-area:hover,
4557 .file-upload-area.dragover {
4558 background: var(--border);
4559 }
4560 /* Chart placeholder */
4561 .chart-container {
4562 background: var(--surface-sunken);
4563 padding: var(--gap-pane);
4564 margin-bottom: var(--gap-pane);
4565 }
4566
4567 .chart-header {
4568 display: flex;
4569 justify-content: space-between;
4570 align-items: center;
4571 margin-bottom: var(--gap-section);
4572 }
4573
4574 .chart-header h2 {
4575 font-size: var(--text-subhead);
4576 font-weight: normal;
4577 }
4578
4579 .chart-bars {
4580 display: flex;
4581 align-items: flex-end;
4582 gap: 2px;
4583 height: 200px;
4584 padding: 0 var(--gap-peer);
4585 }
4586
4587 .chart-bar-col {
4588 flex: 1;
4589 display: flex;
4590 flex-direction: column;
4591 align-items: center;
4592 min-width: 0;
4593 cursor: default;
4594 position: relative;
4595 }
4596
4597 .chart-bar-col[data-tooltip]:hover::before {
4598 content: attr(data-tooltip);
4599 position: absolute;
4600 bottom: 100%;
4601 left: 50%;
4602 transform: translateX(-50%);
4603 background: var(--primary-dark);
4604 color: var(--primary-light);
4605 font-size: var(--text-fine);
4606 font-family: var(--font-mono);
4607 padding: var(--gap-bound) var(--gap-peer);
4608 white-space: nowrap;
4609 z-index: var(--z-dropdown);
4610 pointer-events: none;
4611 margin-bottom: var(--gap-bound);
4612 }
4613
4614 .chart-bar {
4615 width: 100%;
4616 height: var(--fill, 0%);
4617 background: var(--primary-dark);
4618 min-height: 2px;
4619 transition: opacity 0.15s;
4620 }
4621
4622 .chart-bar-col:hover .chart-bar {
4623 opacity: 0.8;
4624 }
4625
4626 .chart-bar-label {
4627 font-size: var(--text-fine);
4628 opacity: 0.5;
4629 margin-top: var(--gap-bound);
4630 white-space: nowrap;
4631 overflow: hidden;
4632 text-overflow: ellipsis;
4633 max-width: 100%;
4634 }
4635
4636 /* Time selector */
4637 .time-selector {
4638 display: flex;
4639 gap: var(--gap-peer);
4640 }
4641
4642 /* The range buttons are makeover's `.segment`, which is what a segmented
4643 control is called there, and B3's question about whether this codebase has
4644 an analogue: it does, and this is it. Fill, bevel, hover, active and focus
4645 are the generated ones; this rule is geometry and type.
4646
4647 Selection was a --primary-dark fill with a --primary-light label, which is
4648 a second thing meaning "chosen" beside the generated `.chosen` (the segment
4649 presses IN — well fill, inset bevel). Taking the generated one; only the
4650 name stays local, same as the tabs. */
4651 .time-selector button {
4652 border: none;
4653 padding: var(--gap-peer) var(--gap-section);
4654 font-family: inherit;
4655 font-size: var(--text-note);
4656 cursor: pointer;
4657 }
4658
4659 .time-selector button.is-selected {
4660 background: var(--surface-well, var(--surface-page));
4661 box-shadow: var(--bevel-inset);
4662 }
4663
4664 /* Analytics */
4665 .analytics-grid {
4666 display: grid;
4667 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
4668 gap: var(--gap-pane);
4669 margin-bottom: var(--gap-pane);
4670 }
4671 .analytics-list {
4672 list-style: none;
4673 }
4674
4675 .analytics-list li {
4676 padding: var(--gap-peer) 0;
4677 border-bottom: 1px solid var(--border);
4678 display: flex;
4679 justify-content: space-between;
4680 }
4681
4682 .analytics-list li:last-child {
4683 border-bottom: none;
4684 }
4685
4686 /* Filter controls */
4687 .filter-controls {
4688 display: flex;
4689 gap: var(--gap-section);
4690 margin-bottom: var(--gap-section);
4691 flex-wrap: wrap;
4692 align-items: center;
4693 }
4694
4695 .filter-group {
4696 display: flex;
4697 gap: var(--gap-peer);
4698 align-items: center;
4699 }
4700 /* UUID styling */
4701 .uuid {
4702 font-family: inherit;
4703 opacity: 0.5;
4704 font-size: var(--text-note);
4705 }
4706 /* ===========================================
4707 HTMX LOADING INDICATORS
4708 =========================================== */
4709
4710 .htmx-indicator {
4711 display: none;
4712 animation: htmx-spin 1s ease-in-out infinite;
4713 }
4714
4715 .htmx-request .htmx-indicator {
4716 display: inline;
4717 }
4718
4719 .htmx-request.htmx-indicator {
4720 display: inline;
4721 }
4722
4723 /* Loading spinner animation. Applied on .htmx-indicator above. */
4724 @keyframes htmx-spin {
4725 0% { opacity: 0.3; }
4726 50% { opacity: 1; }
4727 100% { opacity: 0.3; }
4728 }
4729
4730 /* Disable buttons during request */
4731 .htmx-request button[type="submit"] {
4732 opacity: 0.6;
4733 cursor: wait;
4734 }
4735
4736 /* ===========================================
4737 TOAST NOTIFICATIONS
4738 =========================================== */
4739
4740 .toast-container {
4741 position: fixed;
4742 bottom: 1.5rem;
4743 right: 1.5rem;
4744 z-index: var(--z-toast);
4745 display: flex;
4746 flex-direction: column;
4747 gap: var(--gap-peer);
4748 }
4749
4750 .toast {
4751 background: var(--primary-dark);
4752 color: var(--primary-light);
4753 padding: var(--gap-section);
4754 font-family: var(--font-mono);
4755 font-size: var(--text-note);
4756 box-shadow: var(--elevation-overlay);
4757 animation: toast-in var(--motion-fade) ease-out;
4758 max-width: 300px;
4759 display: flex;
4760 align-items: center;
4761 gap: var(--gap-section);
4762 }
4763
4764 .toast-dismiss {
4765 background: none;
4766 border: none;
4767 color: inherit;
4768 font-size: var(--text-lead);
4769 cursor: pointer;
4770 opacity: 0.7;
4771 padding: 0;
4772 line-height: 1;
4773 flex-shrink: 0;
4774 }
4775
4776 .toast-dismiss:hover {
4777 opacity: 1;
4778 }
4779
4780 .toast-success {
4781 background: var(--success);
4782 }
4783
4784 .toast-error {
4785 background: var(--danger);
4786 }
4787
4788 .toast-warning {
4789 background: var(--warning);
4790 }
4791
4792 .toast-info {
4793 background: var(--primary-dark);
4794 }
4795
4796 .toast-retry-btn {
4797 background: none;
4798 border: 1px solid currentColor;
4799 color: inherit;
4800 font-family: var(--font-mono);
4801 font-size: var(--text-fine);
4802 font-weight: 600;
4803 padding: var(--gap-bound) var(--gap-peer);
4804 margin-left: var(--gap-section);
4805 cursor: pointer;
4806 }
4807
4808 .toast-retry-btn:hover {
4809 background: color-mix(in oklch, var(--primary-light) 15%, transparent);
4810 }
4811
4812 .toast.fade-out {
4813 /* `Motion::Fade`, the same number the script waits before removing the node.
4814 A literal here and a token there is the two parting silently, and reduced
4815 motion zeroes the token. */
4816 animation: toast-out var(--motion-fade) ease-in forwards;
4817 }
4818
4819 @keyframes toast-in {
4820 from {
4821 transform: translateX(100%);
4822 opacity: 0;
4823 }
4824 to {
4825 transform: translateX(0);
4826 opacity: 1;
4827 }
4828 }
4829
4830 @keyframes toast-out {
4831 from {
4832 transform: translateX(0);
4833 opacity: 1;
4834 }
4835 to {
4836 transform: translateX(100%);
4837 opacity: 0;
4838 }
4839 }
4840
4841 /* ===========================================
4842 RESTART WARNING BANNER
4843 =========================================== */
4844
4845 /* Page-top behavior extensions on top of .banner.banner--warning.
4846 Color / typography come from the .banner primitive. */
4847 #restart-banner {
4848 position: fixed;
4849 top: 0;
4850 left: 0;
4851 right: 0;
4852 z-index: var(--z-banner);
4853 padding: var(--gap-group) var(--gap-section);
4854 animation: banner-slide-down 0.3s ease-out;
4855 }
4856
4857 @keyframes banner-slide-down {
4858 from {
4859 transform: translateY(-100%);
4860 }
4861 to {
4862 transform: translateY(0);
4863 }
4864 }
4865
4866 /* ===========================================
4867 SAVE STATUS INDICATORS
4868 =========================================== */
4869
4870 /* Username availability status */
4871 .username-status {
4872 display: block;
4873 margin-top: var(--gap-bound);
4874 font-size: var(--text-note);
4875 }
4876
4877 .username-status.available {
4878 color: var(--success);
4879 }
4880
4881 .username-status.taken {
4882 color: var(--danger);
4883 }
4884
4885 /* ===========================================
4886 HTMX TRANSITIONS
4887 =========================================== */
4888
4889 /* Fade in for swapped content */
4890 .htmx-added {
4891 animation: htmx-fade-in 0.2s ease-out;
4892 }
4893
4894 @keyframes htmx-fade-in {
4895 from {
4896 opacity: 0;
4897 }
4898 to {
4899 opacity: 1;
4900 }
4901 }
4902
4903 /* Settling animation for swapped content */
4904 .htmx-settling {
4905 opacity: 0.8;
4906 }
4907
4908 /* Swap animation */
4909 .htmx-swapping {
4910 opacity: 0.5;
4911 transition: opacity 0.2s ease-out;
4912 }
4913
4914 /* ===========================================
4915 ACCESSIBILITY - FOCUS STYLES
4916 =========================================== */
4917
4918 /* Focus-visible for keyboard navigation */
4919 *:focus-visible {
4920 outline: 2px solid var(--focus-ring);
4921 outline-offset: 2px;
4922 }
4923
4924 /* Remove default focus outline since we use focus-visible */
4925 *:focus:not(:focus-visible) {
4926 outline: none;
4927 }
4928
4929 /* Ensure buttons and links are keyboard accessible */
4930 button:focus-visible,
4931 a:focus-visible,
4932 input:focus-visible,
4933 select:focus-visible,
4934 textarea:focus-visible {
4935 outline: 2px solid var(--focus-ring);
4936 outline-offset: 2px;
4937 }
4938
4939 /* Tab focus is layout.css's, same ring as everything else at outline-offset
4940 2px. The local rule inset it to -2px, which is only worth doing where a
4941 strip clips its overflow; `.tabs` is flex/nowrap with no overflow set, so
4942 there was nothing to clip and the tabs read differently from every other
4943 focusable thing on the page for no reason. */
4944
4945 /* Canonical "selected" recipe (charter: docs/design-system.md).
4946 Apply `.is-selected` to any interactive container to mark it as the
4947 currently-selected option. A badge never takes it: a badge reports a status
4948 and nobody selects "Enabled", so badges take a `data-tone` instead.
4949 Visibility toggles (`.tab-content.active`, `.section-panel.active`) also
4950 keep `.active` since they're not selection state. */
4951 .is-selected {
4952 background: var(--highlight-faint);
4953 border-color: var(--focus-ring);
4954 }
4955
4956 /* Skip to main content link (for screen readers) */
4957 .skip-to-main {
4958 position: absolute;
4959 top: -100px;
4960 left: 0;
4961 background: var(--primary-dark);
4962 color: var(--primary-light);
4963 padding: 8px;
4964 z-index: var(--z-skip-link);
4965 transition: top 0.2s ease;
4966 }
4967
4968 .skip-to-main:focus {
4969 top: 0;
4970 }
4971
4972 /* Visually hidden but accessible to screen readers */
4973 .sr-only {
4974 position: absolute;
4975 width: 1px;
4976 height: 1px;
4977 padding: 0;
4978 margin: -1px;
4979 overflow: hidden;
4980 clip: rect(0, 0, 0, 0);
4981 white-space: nowrap;
4982 border: 0;
4983 }
4984
4985 /* Status indicator with text for accessibility */
4986 .status-indicator::after {
4987 content: attr(data-status);
4988 position: absolute;
4989 width: 1px;
4990 height: 1px;
4991 padding: 0;
4992 margin: -1px;
4993 overflow: hidden;
4994 clip: rect(0, 0, 0, 0);
4995 white-space: nowrap;
4996 border: 0;
4997 }
4998
4999 /* ===========================================
5000 PAYWALL
5001 =========================================== */
5002
5003 .paywall-section {
5004 margin: var(--gap-page) 0;
5005 padding: var(--gap-page);
5006 background: var(--surface-sunken);
5007 text-align: center;
5008 }
5009
5010 .paywall-box h2 {
5011 font-family: var(--font-display);
5012 font-weight: normal;
5013 margin-bottom: var(--gap-peer);
5014 }
5015
5016 .paywall-price {
5017 font-family: var(--font-mono);
5018 font-size: var(--text-head);
5019 margin-bottom: var(--gap-peer);
5020 }
5021
5022 .paywall-desc {
5023 font-family: var(--font-sans);
5024 color: var(--content-muted);
5025 margin-bottom: var(--gap-pane);
5026 }
5027
5028 .paywall-note {
5029 font-family: var(--font-mono);
5030 font-size: var(--text-note);
5031 color: var(--content-muted);
5032 margin-top: var(--gap-section);
5033 }
5034
5035 .paywall-note a {
5036 color: var(--action);
5037 }
5038
5039 /* ===========================================
5040 MODALS
5041 =========================================== */
5042
5043 .modal-overlay {
5044 position: fixed;
5045 inset: 0;
5046 background: var(--overlay);
5047 display: flex;
5048 align-items: center;
5049 justify-content: center;
5050 z-index: var(--z-modal);
5051 }
5052
5053 .modal {
5054 background: var(--surface-page);
5055 padding: var(--gap-page);
5056 max-width: 500px;
5057 width: 90%;
5058 max-height: 90vh;
5059 overflow-y: auto;
5060 }
5061
5062 .modal h2 {
5063 margin-bottom: var(--gap-pane);
5064 }
5065
5066 .modal .form-actions {
5067 display: flex;
5068 gap: var(--gap-section);
5069 justify-content: flex-end;
5070 margin-top: var(--gap-pane);
5071 }
5072
5073 /* ===========================================
5074 LINK ROWS (Dashboard Links Section)
5075 =========================================== */
5076
5077 .link-row {
5078 display: flex;
5079 gap: var(--gap-section);
5080 align-items: center;
5081 margin-bottom: var(--gap-section);
5082 padding: var(--gap-section);
5083 background: var(--surface-sunken);
5084 transition: opacity 0.2s ease, background 0.2s ease;
5085 }
5086
5087 .link-row input[type="text"],
5088 .link-row input[type="url"] {
5089 flex: 1;
5090 min-width: 0;
5091 }
5092
5093 .link-row .btn-danger {
5094 padding: var(--gap-peer) var(--gap-section);
5095 font-size: var(--text-note);
5096 }
5097
5098 /* Link order buttons */
5099 .link-order-buttons {
5100 display: flex;
5101 flex-direction: column;
5102 gap: 2px;
5103 }
5104
5105 .order-btn {
5106 background: var(--surface-sunken);
5107 border: 1px solid var(--border);
5108 padding: var(--gap-bound) var(--gap-peer);
5109 font-size: var(--text-fine);
5110 line-height: 1;
5111 cursor: pointer;
5112 opacity: 0.6;
5113 transition: opacity 0.15s ease, background 0.15s ease;
5114 }
5115
5116 .order-btn:hover {
5117 opacity: 1;
5118 background: var(--border);
5119 }
5120
5121 .order-btn:active {
5122 box-shadow: var(--shadow-inset);
5123 }
5124
5125 /* ===========================================
5126 TEXT EDITOR
5127 =========================================== */
5128
5129 .text-editor {
5130 width: 100%;
5131 }
5132
5133 .editor-footer {
5134 display: flex;
5135 justify-content: space-between;
5136 padding: var(--gap-peer) var(--gap-section);
5137 background: var(--surface-sunken);
5138 font-size: var(--text-note);
5139 opacity: 0.7;
5140 }
5141
5142 /* ===========================================
5143 RICH FIELD (crate::quasi::rich_field)
5144 =========================================== */
5145
5146 /* The wrapper is this app's; the group inside it is the renderer's, and
5147 `static/layout.css` carries every rule that decides what is showing. What is
5148 left for this file is the two things a description does not say: how tall the
5149 editor stands, and how the rendered preview reads.
5150
5151 Selected through the `[data-format="markdown"]` mark rather than through
5152 makeover's class names, so nothing here overlaps the generated sheet. */
5153
5154 .rich-field [data-format="markdown"] > textarea {
5155 padding: var(--gap-section);
5156 font-family: var(--font-mono);
5157 font-size: var(--text-note);
5158 line-height: 1.6;
5159 resize: vertical;
5160 border: none;
5161 background: var(--surface-sunken);
5162 }
5163
5164 /* Three sizes for four surfaces: the item editor and the blog editor were
5165 already the same surface written two ways (400px of CSS, and rows="20"). */
5166 .rich-field--compact [data-format="markdown"] > textarea,
5167 .rich-field--compact [data-editor-preview] {
5168 min-height: 9rem;
5169 }
5170
5171 .rich-field--standard [data-format="markdown"] > textarea,
5172 .rich-field--standard [data-editor-preview] {
5173 min-height: 15rem;
5174 }
5175
5176 .rich-field--tall [data-format="markdown"] > textarea,
5177 .rich-field--tall [data-editor-preview] {
5178 min-height: 400px;
5179 }
5180
5181 /* The pane stands where the control stood, so it reads as prose rather than as
5182 source. The rules below are the retired `.markdown-preview` set, unchanged
5183 except for what selects them. */
5184 .rich-field [data-editor-preview] {
5185 padding: var(--gap-pane);
5186 font-family: var(--font-sans);
5187 line-height: 1.8;
5188 }
5189
5190 .rich-field [data-editor-preview] h1,
5191 .rich-field [data-editor-preview] h2,
5192 .rich-field [data-editor-preview] h3 {
5193 margin-top: var(--gap-pane);
5194 margin-bottom: var(--gap-peer);
5195 }
5196
5197 .rich-field [data-editor-preview] h1 {
5198 font-size: var(--text-title);
5199 }
5200
5201 .rich-field [data-editor-preview] h2 {
5202 font-size: var(--text-head);
5203 }
5204
5205 .rich-field [data-editor-preview] h3 {
5206 font-size: var(--text-lead);
5207 }
5208
5209 .rich-field [data-editor-preview] p {
5210 margin-bottom: var(--gap-section);
5211 }
5212
5213 .rich-field [data-editor-preview] code {
5214 background: var(--surface-sunken);
5215 padding: var(--gap-bound);
5216 font-family: var(--font-mono);
5217 font-size: var(--text-note);
5218 }
5219
5220 /* ===========================================
5221 AUDIO UPLOAD
5222 =========================================== */
5223
5224 .audio-upload {
5225 width: 100%;
5226 }
5227
5228 .current-audio {
5229 display: flex;
5230 justify-content: space-between;
5231 align-items: center;
5232 padding: var(--gap-section);
5233 background: var(--surface-sunken);
5234 margin-bottom: var(--gap-section);
5235 }
5236
5237 .audio-info {
5238 display: flex;
5239 align-items: center;
5240 gap: var(--gap-section);
5241 }
5242
5243 .audio-icon {
5244 font-size: var(--text-head);
5245 opacity: 0.6;
5246 }
5247
5248 .audio-details {
5249 display: flex;
5250 flex-direction: column;
5251 }
5252
5253 .audio-filename {
5254 font-family: var(--font-mono);
5255 font-size: var(--text-note);
5256 }
5257
5258 .audio-duration {
5259 font-size: var(--text-note);
5260 opacity: 0.6;
5261 }
5262
5263 .upload-area .file-upload-area {
5264 padding: var(--gap-page);
5265 }
5266
5267 .upload-icon {
5268 font-size: var(--text-title);
5269 opacity: 0.4;
5270 margin-bottom: var(--gap-peer);
5271 }
5272
5273 .upload-text {
5274 font-size: var(--text-lead);
5275 margin-bottom: var(--gap-peer);
5276 }
5277
5278 .upload-hint {
5279 font-size: var(--text-note);
5280 opacity: 0.6;
5281 }
5282
5283 .upload-progress {
5284 padding: var(--gap-pane);
5285 background: var(--surface-sunken);
5286 }
5287
5288 .progress-info {
5289 display: flex;
5290 justify-content: space-between;
5291 margin-bottom: var(--gap-section);
5292 font-size: var(--text-note);
5293 }
5294
5295 /* Canonical progress bar primitive. One of these in the codebase.
5296 - .progress-bar-container is the track (8px default; --slim = 6px)
5297 - .progress-bar is the fill (success-green default; --highlight = purple)
5298 - Add .progress-bar-container--rounded for the 3px-radius pill variant
5299 Spacing (mb-4 / mt-2 etc.) is added by the caller, not baked in. */
5300 .progress-bar-container {
5301 height: 8px;
5302 background: var(--border);
5303 overflow: hidden;
5304 }
5305 .progress-bar-container--slim { height: 6px; }
5306 .progress-bar-container--rounded { border-radius: var(--radius-control); }
5307
5308 /* Fill proportion comes from the template as --fill, because it is
5309 per-render data. The property that reads it lives here. */
5310 .progress-bar {
5311 width: var(--fill, 0%);
5312 height: 100%;
5313 background: var(--success);
5314 transition: width 0.2s ease;
5315 }
5316 .progress-bar--highlight { background: var(--action); }
5317 .progress-bar--warn { background: var(--warning); }
5318 .progress-bar--danger { background: var(--danger); }
5319
5320 /* Upload status block (filename + percent label + progress bar). One of these
5321 in the codebase. Used by wizard upload flows and standalone audio/video upload partials. */
5322 .upload-status { margin-top: var(--gap-peer); }
5323 .upload-status-row {
5324 display: flex;
5325 align-items: center;
5326 gap: var(--gap-peer);
5327 }
5328 .upload-status-row span { font-size: var(--text-note); }
5329 .upload-status-msg { margin-top: var(--gap-peer); }
5330 .upload-status-msg.is-success { color: var(--success); }
5331 .upload-status-msg.is-error { color: var(--danger); }
5332
5333 /* Inline progress cell rendered inside a table-row status cell (batch uploads). */
5334 .upload-progress-row {
5335 display: flex;
5336 align-items: center;
5337 gap: var(--gap-peer);
5338 }
5339 .upload-progress-row .progress-bar-container { flex: 1; min-width: 60px; }
5340 .upload-progress-pct {
5341 font-size: var(--text-fine);
5342 min-width: 2.5em;
5343 text-align: right;
5344 }
5345
5346 .upload-success {
5347 padding: var(--gap-pane);
5348 background: var(--surface-sunken);
5349 text-align: center;
5350 }
5351 .upload-error {
5352 padding: var(--gap-pane);
5353 background: var(--surface-sunken);
5354 text-align: center;
5355 }
5356
5357 .hidden {
5358 display: none !important;
5359 }
5360
5361 /* ===========================================
5362 VIEW TOGGLE (Grid/List)
5363 =========================================== */
5364
5365 .view-controls {
5366 display: flex;
5367 gap: var(--gap-bound);
5368 margin-left: auto;
5369 }
5370
5371 .view-btn {
5372 background: var(--surface-sunken);
5373 border: none;
5374 padding: var(--gap-peer) var(--gap-group);
5375 font-family: var(--font-mono);
5376 font-size: var(--text-note);
5377 cursor: pointer;
5378 opacity: 0.5;
5379 transition: opacity 0.2s ease;
5380 }
5381
5382 .view-btn:hover {
5383 opacity: 0.8;
5384 }
5385
5386 .view-btn.is-selected {
5387 opacity: 1;
5388 background: var(--primary-dark);
5389 color: var(--primary-light);
5390 }
5391 .results-container.results-list .results-table {
5392 display: block;
5393 }
5394
5395 .results-container.results-list .results-grid {
5396 display: none;
5397 }
5398
5399 .results-container.results-grid .results-table {
5400 display: none;
5401 }
5402
5403 .results-container.results-grid .results-grid {
5404 display: grid;
5405 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
5406 gap: var(--gap-pane);
5407 border: 1px solid var(--border);
5408 border-top: none;
5409 padding: var(--gap-pane);
5410 background: var(--surface-overlay);
5411 }
5412
5413 /* Grid cards */
5414 .grid-card {
5415 background: var(--surface-overlay);
5416 text-decoration: none;
5417 color: var(--content);
5418 display: flex;
5419 flex-direction: column;
5420 transition: background 0.2s ease;
5421 }
5422
5423 .grid-card:hover {
5424 background: var(--surface-sunken);
5425 text-decoration: none;
5426 }
5427
5428 .grid-card-thumbnail {
5429 height: 120px;
5430 background: var(--border);
5431 display: flex;
5432 align-items: center;
5433 justify-content: center;
5434 font-size: var(--text-fine);
5435 opacity: 0.5;
5436 text-transform: uppercase;
5437 }
5438
5439 .grid-card-content {
5440 padding: var(--gap-section);
5441 display: flex;
5442 flex-direction: column;
5443 gap: var(--gap-bound);
5444 }
5445
5446 .grid-card-title {
5447 font-family: var(--font-display);
5448 font-size: var(--text-lead);
5449 }
5450
5451 .grid-card-meta {
5452 font-size: var(--text-note);
5453 opacity: 0.6;
5454 }
5455
5456 .grid-card-footer {
5457 display: flex;
5458 justify-content: space-between;
5459 align-items: center;
5460 margin-top: var(--gap-peer);
5461 font-size: var(--text-note);
5462 }
5463
5464 .grid-card-category {
5465 opacity: 0.6;
5466 }
5467
5468 .grid-card-price {
5469 font-weight: bold;
5470 }
5471
5472 .grid-card-stat {
5473 opacity: 0.6;
5474 }
5475
5476 /* ===========================================
5477 PROJECT PAGE VIEW TOGGLE
5478 =========================================== */
5479
5480 .items-header {
5481 display: flex;
5482 justify-content: space-between;
5483 align-items: center;
5484 margin-bottom: var(--gap-pane);
5485 }
5486
5487 .items-header .section-header {
5488 margin-bottom: 0;
5489 border-bottom: none;
5490 padding-bottom: 0;
5491 }
5492
5493 /* Grid view is default on project page */
5494 .items-container.items-grid-view .items-grid {
5495 display: grid;
5496 }
5497
5498 .items-container.items-grid-view .items-list {
5499 display: none;
5500 }
5501
5502 .items-container.items-list-view .items-grid {
5503 display: none;
5504 }
5505
5506 .items-container.items-list-view .items-list {
5507 display: block;
5508 }
5509
5510 /* List items styling */
5511 .items-list {
5512 display: none;
5513 }
5514
5515 .list-item {
5516 display: flex;
5517 justify-content: space-between;
5518 align-items: center;
5519 padding: var(--gap-section);
5520 background: var(--surface-overlay);
5521 margin-bottom: var(--gap-peer);
5522 text-decoration: none;
5523 color: var(--content);
5524 transition: background 0.2s ease;
5525 }
5526
5527 .list-item:hover {
5528 background: var(--surface-sunken);
5529 text-decoration: none;
5530 }
5531
5532 .list-item-info {
5533 display: flex;
5534 flex-direction: column;
5535 gap: var(--gap-bound);
5536 }
5537
5538 .list-item-title {
5539 font-family: var(--font-display);
5540 font-size: var(--text-lead);
5541 }
5542
5543 .list-item-meta {
5544 font-size: var(--text-note);
5545 opacity: 0.6;
5546 }
5547
5548 .list-item-price {
5549 font-size: var(--text-lead);
5550 font-weight: bold;
5551 }
5552
5553 /* ===========================================
5554 LANDING PAGE
5555 =========================================== */
5556
5557 .landing-hero {
5558 max-width: 720px;
5559 width: 100%;
5560 text-align: center;
5561 }
5562
5563 .landing-headline {
5564 font-family: var(--font-display);
5565 font-size: var(--text-head);
5566 margin-bottom: var(--gap-peer);
5567 }
5568
5569 .landing-sub {
5570 font-size: var(--text-body);
5571 line-height: 1.6;
5572 opacity: 0.75;
5573 margin-bottom: 0;
5574 }
5575
5576 .fork-revenue-line {
5577 font-size: var(--text-note);
5578 opacity: 0.8;
5579 margin: var(--gap-section) 0 var(--gap-peer);
5580 line-height: 1.5;
5581 }
5582
5583 .fork-revenue-line a {
5584 border-bottom: 1px solid transparent;
5585 transition: border-color 0.2s ease;
5586 }
5587
5588 .fork-revenue-line a:hover {
5589 border-bottom-color: currentColor;
5590 }
5591
5592 .landing-fork {
5593 display: grid;
5594 grid-template-columns: 1fr 1fr;
5595 gap: var(--gap-section);
5596 margin-top: var(--gap-page);
5597 text-align: left;
5598 }
5599
5600 .fork-heading {
5601 font-family: var(--font-mono);
5602 font-size: var(--text-note);
5603 font-weight: 600;
5604 text-transform: uppercase;
5605 letter-spacing: 0.05em;
5606 margin-bottom: var(--gap-section);
5607 }
5608
5609 .fork-list {
5610 list-style: none;
5611 margin: 0;
5612 padding: 0;
5613 flex: 1;
5614 }
5615
5616 .fork-list li {
5617 margin-bottom: var(--gap-group);
5618 font-size: var(--text-note);
5619 line-height: 1.5;
5620 }
5621
5622 .fork-list strong {
5623 font-family: var(--font-mono);
5624 font-size: var(--text-fine);
5625 }
5626
5627 .fork-actions {
5628 margin-top: auto;
5629 padding-top: var(--gap-section);
5630 display: flex;
5631 flex-direction: column;
5632 align-items: center;
5633 gap: var(--gap-peer);
5634 }
5635
5636 .fork-actions .big-button {
5637 width: 100%;
5638 }
5639
5640 .fork-secondary-link {
5641 font-family: var(--font-mono);
5642 font-size: var(--text-fine);
5643 opacity: 0.6;
5644 transition: opacity 0.2s ease;
5645 }
5646
5647 .fork-secondary-link:hover {
5648 opacity: 1;
5649 }
5650
5651 .fork-lede {
5652 font-size: var(--text-body);
5653 line-height: 1.55;
5654 margin: 0 0 var(--gap-section);
5655 }
5656
5657 .landing-browse-cta {
5658 text-align: center;
5659 margin: var(--gap-section) 0 0;
5660 }
5661
5662 .landing-mobile-note {
5663 text-align: center;
5664 margin: var(--gap-peer) 0 0;
5665 font-size: var(--text-fine);
5666 color: var(--content-muted);
5667 }
5668
5669 .founder-tagline {
5670 font-family: var(--font-sans);
5671 font-size: var(--text-body);
5672 line-height: 1.55;
5673 margin: var(--gap-section) auto 0;
5674 max-width: 560px;
5675 color: var(--content);
5676 display: flex;
5677 flex-direction: column;
5678 align-items: center;
5679 gap: var(--gap-peer);
5680 }
5681
5682 .landing-velocity {
5683 font-family: var(--font-mono);
5684 font-size: var(--text-fine);
5685 text-align: center;
5686 margin: var(--gap-section) auto 0;
5687 color: var(--content-muted);
5688 }
5689
5690 .landing-velocity a {
5691 color: var(--content);
5692 text-decoration: none;
5693 border-bottom: 1px solid var(--border);
5694 }
5695
5696 .landing-velocity a:hover {
5697 border-bottom-color: var(--content);
5698 }
5699
5700 .founder-tagline-detail {
5701 display: block;
5702 }
5703
5704 .founder-tagline-mark {
5705 font-family: var(--font-mono);
5706 font-size: var(--text-fine);
5707 font-weight: 600;
5708 text-transform: uppercase;
5709 letter-spacing: 0.08em;
5710 color: var(--action);
5711 padding: var(--gap-bound) var(--gap-peer);
5712 border: 1px solid var(--action);
5713 border-radius: var(--radius-control);
5714 white-space: nowrap;
5715 }
5716
5717 .fork-card--founder {
5718 border-color: var(--action);
5719 box-shadow: 0 0 0 1px var(--highlight-faint);
5720 }
5721
5722 .founder-tier-grid {
5723 display: grid;
5724 grid-template-columns: 1fr 1fr;
5725 gap: var(--gap-peer) var(--gap-section);
5726 margin-bottom: var(--gap-section);
5727 padding-bottom: var(--gap-section);
5728 border-bottom: 1px dashed var(--border);
5729 }
5730
5731 .founder-tier {
5732 display: flex;
5733 flex-direction: column;
5734 gap: var(--gap-bound);
5735 }
5736
5737 .founder-tier-name {
5738 font-family: var(--font-mono);
5739 font-size: var(--text-fine);
5740 font-weight: 600;
5741 text-transform: uppercase;
5742 letter-spacing: 0.05em;
5743 opacity: 0.7;
5744 }
5745
5746 .founder-tier-price {
5747 font-family: var(--font-display);
5748 font-size: var(--text-body);
5749 line-height: 1.1;
5750 }
5751
5752 .founder-tier-price del {
5753 opacity: 0.4;
5754 margin-right: var(--gap-bound);
5755 font-weight: 400;
5756 }
5757
5758 .founder-tier-price strong {
5759 color: var(--action);
5760 font-weight: 700;
5761 }
5762
5763 .founder-tier-unit {
5764 font-family: var(--font-mono);
5765 font-size: var(--text-fine);
5766 opacity: 0.6;
5767 margin-left: var(--gap-bound);
5768 }
5769
5770 .founder-slots {
5771 font-family: var(--font-mono);
5772 font-size: var(--text-fine);
5773 margin: 0 0 var(--gap-section);
5774 line-height: 1.3;
5775 }
5776
5777 .founder-slots-number {
5778 font-family: var(--font-display);
5779 font-size: var(--text-head);
5780 color: var(--action);
5781 font-weight: 700;
5782 margin-right: var(--gap-bound);
5783 }
5784
5785 .founder-slots-cap {
5786 opacity: 0.55;
5787 margin-left: var(--gap-bound);
5788 }
5789
5790 .founder-slots--cap {
5791 opacity: 0.7;
5792 font-size: var(--text-fine);
5793 }
5794
5795 .landing-stats-line {
5796 font-family: var(--font-mono);
5797 font-size: var(--text-note);
5798 opacity: 0.6;
5799 margin-top: var(--gap-page);
5800 }
5801
5802 .landing-stats-line a {
5803 opacity: 0.8;
5804 transition: opacity 0.2s ease;
5805 }
5806
5807 .landing-stats-line a:hover {
5808 opacity: 1;
5809 }
5810
5811 .landing-principles {
5812 margin-top: var(--gap-page);
5813 width: 100%;
5814 }
5815
5816 .landing-use-cases {
5817 margin-top: var(--gap-page);
5818 width: 100%;
5819 }
5820
5821 .use-case-line {
5822 font-family: var(--font-mono);
5823 font-size: var(--text-note);
5824 opacity: 0.7;
5825 letter-spacing: 0.02em;
5826 }
5827
5828 .landing-prose {
5829 font-size: var(--text-body);
5830 line-height: 1.7;
5831 margin-bottom: var(--gap-peer);
5832 }
5833
5834 .notify-form {
5835 display: flex;
5836 gap: var(--gap-peer);
5837 max-width: 400px;
5838 margin: 0 auto;
5839 }
5840
5841 .notify-input {
5842 flex: 1;
5843 padding: var(--gap-peer) var(--gap-section);
5844 border: 1px solid var(--border);
5845 border-radius: var(--radius-control);
5846 font-family: var(--font-sans);
5847 font-size: var(--text-note);
5848 background: var(--surface-page);
5849 color: var(--content);
5850 }
5851
5852 .notify-status {
5853 font-family: var(--font-mono);
5854 font-size: var(--text-fine);
5855 text-align: center;
5856 margin-top: var(--gap-peer);
5857 min-height: 1.2em;
5858 }
5859
5860 .notify-status.success {
5861 color: var(--content);
5862 }
5863
5864 .notify-status.error {
5865 color: var(--danger);
5866 }
5867
5868 .section-label {
5869 font-family: var(--font-mono);
5870 font-size: var(--text-note);
5871 letter-spacing: 0.05em;
5872 text-transform: uppercase;
5873 opacity: 0.6;
5874 margin-bottom: var(--gap-section);
5875 }
5876
5877 .tier-section {
5878 margin: var(--gap-page) 0;
5879 width: 100%;
5880 }
5881 /* Marketing pricing variant of .card--bordered. */
5882 .tier-name {
5883 font-family: var(--font-mono);
5884 font-size: var(--text-note);
5885 font-weight: 600;
5886 margin-bottom: var(--gap-bound);
5887 }
5888
5889 .tier-price {
5890 font-family: var(--font-display);
5891 font-size: var(--text-lead);
5892 margin-bottom: var(--gap-bound);
5893 }
5894
5895 .tier-desc {
5896 font-size: var(--text-note);
5897 opacity: 0.7;
5898 }
5899
5900 .tier-card.planned {
5901 opacity: 0.5;
5902 border-style: dashed;
5903 position: relative;
5904 }
5905 .secondary-links {
5906 display: flex;
5907 gap: var(--gap-pane);
5908 justify-content: center;
5909 margin-top: var(--gap-section);
5910 }
5911
5912 .secondary-links a {
5913 font-size: var(--text-note);
5914 opacity: 0.6;
5915 transition: opacity 0.2s ease;
5916 }
5917
5918 .secondary-links a:hover {
5919 opacity: 1;
5920 }
5921 .section-link {
5922 display: block;
5923 margin-top: var(--gap-section);
5924 font-family: var(--font-mono);
5925 font-size: var(--text-fine);
5926 opacity: 0.55;
5927 transition: opacity 0.2s ease;
5928 }
5929
5930 .section-link:hover {
5931 opacity: 1;
5932 }
5933
5934 .how-list {
5935 list-style: none;
5936 margin: 0;
5937 padding: 0;
5938 text-align: left;
5939 }
5940
5941 .how-list li {
5942 margin-bottom: var(--gap-group);
5943 font-size: var(--text-body);
5944 }
5945
5946 .how-list strong {
5947 font-family: var(--font-mono);
5948 font-size: var(--text-note);
5949 }
5950
5951 .do-rail {
5952 list-style: none;
5953 margin: var(--gap-page) 0 var(--gap-pane);
5954 padding: 0;
5955 display: grid;
5956 /* A do-card stops being readable under about 200px, which is a fact about
5957 the card. Three across is what that yields at full width. */
5958 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
5959 gap: var(--gap-section);
5960 width: 100%;
5961 }
5962
5963 .do-card {
5964 background: transparent;
5965 border: 1px solid var(--border);
5966 padding: var(--gap-section);
5967 text-align: left;
5968 display: flex;
5969 flex-direction: column;
5970 }
5971
5972 .do-card-title {
5973 font-family: var(--font-mono);
5974 font-size: var(--text-note);
5975 font-weight: 600;
5976 text-transform: uppercase;
5977 letter-spacing: 0.05em;
5978 margin: 0 0 var(--gap-peer);
5979 }
5980
5981 .do-card-lede {
5982 font-size: var(--text-note);
5983 line-height: 1.5;
5984 opacity: 0.78;
5985 margin: 0 0 var(--gap-section);
5986 }
5987
5988 .do-card-links {
5989 list-style: none;
5990 margin: auto 0 0;
5991 padding: var(--gap-section) 0 0;
5992 border-top: 1px solid var(--border);
5993 display: flex;
5994 flex-wrap: wrap;
5995 gap: var(--gap-peer) var(--gap-section);
5996 }
5997
5998 .do-card-links a {
5999 font-size: var(--text-note);
6000 border-bottom: 1px solid transparent;
6001 transition: border-color 0.2s ease;
6002 }
6003
6004 .do-card-links a:hover {
6005 border-bottom-color: currentColor;
6006 }
6007
6008 .do-card--wide {
6009 margin-top: var(--gap-page);
6010 width: 100%;
6011 }
6012
6013 .contrast-rail {
6014 list-style: none;
6015 margin: var(--gap-section) 0 0;
6016 padding: 0;
6017 display: grid;
6018 /* Title plus a short link list, so it holds up narrower than a do-card.
6019 Four across at full width. */
6020 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
6021 gap: var(--gap-section);
6022 width: 100%;
6023 }
6024
6025 .contrast-card {
6026 border-top: 1px solid var(--border);
6027 padding: var(--gap-section) var(--gap-bound) 0;
6028 text-align: left;
6029 }
6030
6031 .contrast-card-title {
6032 font-family: var(--font-mono);
6033 font-size: var(--text-fine);
6034 letter-spacing: 0.04em;
6035 text-transform: uppercase;
6036 opacity: 0.5;
6037 text-decoration: line-through;
6038 text-decoration-thickness: 1px;
6039 margin: 0 0 var(--gap-peer);
6040 font-weight: 600;
6041 }
6042
6043 .contrast-card-links {
6044 list-style: none;
6045 margin: 0;
6046 padding: 0;
6047 display: flex;
6048 flex-wrap: wrap;
6049 gap: var(--gap-peer) var(--gap-section);
6050 }
6051
6052 .contrast-card-links a {
6053 font-size: var(--text-note);
6054 border-bottom: 1px solid transparent;
6055 transition: border-color 0.2s ease;
6056 }
6057
6058 .contrast-card-links a:hover {
6059 border-bottom-color: currentColor;
6060 }
6061
6062 /* ===========================================
6063 PRICING CALCULATOR
6064 =========================================== */
6065
6066 /* The calculator itself is a described screen since `1e35bc8a`
6067 (`crate::quasi::pricing`), so it is drawn in makeover's own classes and every
6068 rule this section held for it went with `pages/pricing.html`: the dials, the
6069 tier cards, the list/founder toggle, the verdict, the results table and the
6070 hand-drawn crossover bar. What is left is the one rule another template still
6071 matches. */
6072
6073 .pricing-currency {
6074 font-family: var(--font-display);
6075 font-size: var(--text-display);
6076 opacity: 0.5;
6077 }
6078
6079 /* ===========================================
6080 DOCUMENTATION PAGES
6081 =========================================== */
6082
6083 /* .doc-container takes the shared reading column near the top of this file. */
6084
6085 .doc-breadcrumb {
6086 font-family: var(--font-mono);
6087 font-size: var(--text-note);
6088 margin-bottom: var(--gap-page);
6089 }
6090
6091 .doc-breadcrumb a { color: var(--content); text-decoration: none; }
6092 .doc-breadcrumb a:hover { text-decoration: underline; }
6093
6094 .doc-title {
6095 font-family: var(--font-display);
6096 font-size: var(--text-display);
6097 font-weight: normal;
6098 line-height: 1.2;
6099 margin-bottom: var(--gap-pane);
6100 color: var(--content);
6101 text-align: left;
6102 }
6103
6104 .doc-body {
6105 font-family: var(--font-sans);
6106 font-size: var(--text-lead);
6107 line-height: 1.9;
6108 }
6109
6110 .doc-body p { margin-bottom: var(--gap-pane); }
6111
6112 .doc-body h1 {
6113 font-family: var(--font-display);
6114 font-size: var(--text-title);
6115 font-weight: normal;
6116 margin-top: var(--gap-page);
6117 margin-bottom: var(--gap-section);
6118 color: var(--content);
6119 text-align: left;
6120 }
6121
6122 .doc-body h2 {
6123 font-family: var(--font-mono);
6124 font-size: var(--text-head);
6125 font-weight: normal;
6126 margin-top: var(--gap-page);
6127 margin-bottom: var(--gap-section);
6128 color: var(--content);
6129 }
6130
6131 .doc-body h3 {
6132 font-family: var(--font-mono);
6133 font-size: var(--text-subhead);
6134 font-weight: normal;
6135 margin-top: var(--gap-page);
6136 margin-bottom: var(--gap-section);
6137 color: var(--content);
6138 }
6139
6140 .doc-body blockquote {
6141 border-left: 3px solid var(--action);
6142 padding-left: var(--gap-pane);
6143 margin: var(--gap-page) 0;
6144 font-style: italic;
6145 color: var(--content-muted);
6146 }
6147
6148 .doc-body ul, .doc-body ol { margin-bottom: var(--gap-pane); padding-left: var(--gap-pane); }
6149 .doc-body li { margin-bottom: var(--gap-peer); }
6150
6151 .doc-body a {
6152 color: var(--action);
6153 text-decoration: underline;
6154 text-decoration-color: color-mix(in oklch, var(--action) 40%, transparent);
6155 text-underline-offset: 2px;
6156 }
6157
6158 .doc-body a:hover { text-decoration-color: var(--action); }
6159 .doc-body strong { font-weight: bold; }
6160 .doc-body em { font-style: italic; }
6161
6162 .doc-body code {
6163 font-family: var(--font-mono);
6164 background: var(--surface-sunken);
6165 padding: var(--gap-bound) var(--gap-peer);
6166 font-size: var(--text-body);
6167 }
6168
6169 .doc-body pre {
6170 background: var(--surface-sunken);
6171 padding: var(--gap-section);
6172 overflow-x: auto;
6173 margin-bottom: var(--gap-pane);
6174 }
6175
6176 .doc-body pre code { background: none; padding: 0; }
6177
6178 .doc-body hr { border: none; text-align: center; margin: var(--gap-page) 0; }
6179 .doc-body hr::before { content: "* * *"; color: var(--content-muted); letter-spacing: 1rem; }
6180
6181 .doc-body table { width: 100%; border-collapse: collapse; margin-bottom: var(--gap-pane); }
6182 .doc-body th, .doc-body td { padding: var(--gap-section); text-align: left; border-bottom: 1px solid var(--border); }
6183 .doc-body th { font-family: var(--font-mono); font-weight: 600; }
6184
6185 .doc-backlinks {
6186 margin-top: var(--gap-page);
6187 padding-top: var(--gap-pane);
6188 border-top: 1px solid var(--border);
6189 }
6190
6191 .doc-backlinks-title {
6192 font-family: var(--font-mono);
6193 font-size: var(--text-note);
6194 font-weight: 600;
6195 text-transform: uppercase;
6196 letter-spacing: 0.05em;
6197 color: var(--content-muted);
6198 margin-bottom: var(--gap-section);
6199 }
6200
6201 .doc-backlinks-list {
6202 list-style: none;
6203 margin: 0;
6204 padding: 0;
6205 }
6206
6207 .doc-backlinks-list li { margin-bottom: var(--gap-peer); }
6208 .doc-backlinks-list a { color: var(--content); text-decoration: none; }
6209 .doc-backlinks-list a:hover { text-decoration: underline; }
6210
6211 /* Docs index page */
6212 /* .docs-index takes the shared reading column near the top of this file. */
6213
6214 .docs-index-title {
6215 font-family: var(--font-display);
6216 font-size: var(--text-display);
6217 font-weight: normal;
6218 line-height: 1.2;
6219 margin-bottom: var(--gap-page);
6220 color: var(--content);
6221 }
6222
6223 .docs-section {
6224 margin-bottom: var(--gap-page);
6225 }
6226
6227 .docs-section h2 {
6228 font-family: var(--font-mono);
6229 font-size: var(--text-subhead);
6230 font-weight: normal;
6231 color: var(--content-muted);
6232 margin-bottom: var(--gap-section);
6233 }
6234
6235 .docs-section ul {
6236 list-style: none;
6237 padding: 0;
6238 margin: 0;
6239 }
6240
6241 .docs-section li {
6242 margin-bottom: var(--gap-peer);
6243 }
6244
6245 .docs-section a {
6246 font-family: var(--font-sans);
6247 font-size: var(--text-lead);
6248 color: var(--content);
6249 text-decoration: none;
6250 }
6251
6252 .docs-section a:hover {
6253 color: var(--action);
6254 text-decoration: underline;
6255 }
6256
6257 .docs-section details {
6258 margin-bottom: var(--gap-section);
6259 }
6260
6261 .docs-section summary {
6262 font-family: var(--font-mono);
6263 font-size: var(--text-body);
6264 color: var(--content-muted);
6265 cursor: pointer;
6266 padding: var(--gap-bound) 0;
6267 list-style: disclosure-closed;
6268 }
6269
6270 .docs-section details[open] > summary {
6271 list-style: disclosure-open;
6272 margin-bottom: var(--gap-bound);
6273 }
6274
6275 .docs-section summary:hover {
6276 color: var(--content);
6277 }
6278
6279 .docs-section details ul {
6280 padding-left: var(--gap-section);
6281 }
6282
6283 /* Docs search */
6284 .docs-search-container {
6285 position: relative;
6286 margin-bottom: var(--gap-page);
6287 }
6288 .docs-search-inline {
6289 display: inline-block;
6290 margin-bottom: 0;
6291 margin-left: auto;
6292 float: right;
6293 }
6294 .docs-search-input {
6295 width: 100%;
6296 max-width: 320px;
6297 padding: var(--gap-peer) var(--gap-section);
6298 font-family: var(--font-sans);
6299 font-size: var(--text-body);
6300 background: var(--surface-sunken);
6301 border: 1px solid var(--border);
6302 color: var(--content);
6303 }
6304 .docs-search-input:focus {
6305 outline: 2px solid var(--action);
6306 border-color: var(--action);
6307 }
6308 .docs-search-input::placeholder { opacity: 0.5; }
6309 .docs-search-results {
6310 position: absolute;
6311 top: 100%;
6312 left: 0;
6313 right: 0;
6314 max-width: 320px;
6315 background: var(--surface-page);
6316 border: 1px solid var(--border);
6317 border-top: none;
6318 max-height: 300px;
6319 overflow-y: auto;
6320 z-index: var(--z-dropdown);
6321 box-shadow: var(--elevation-overlay);
6322 }
6323 .docs-search-result {
6324 display: flex;
6325 justify-content: space-between;
6326 align-items: baseline;
6327 padding: var(--gap-peer) var(--gap-section);
6328 text-decoration: none;
6329 color: var(--content);
6330 font-size: var(--text-note);
6331 border-bottom: 1px solid var(--border);
6332 }
6333 .docs-search-result:last-child { border-bottom: none; }
6334 .docs-search-result:hover { background: var(--surface-sunken); }
6335 .docs-search-result-title { font-family: var(--font-sans); }
6336 .docs-search-result-section { font-family: var(--font-mono); font-size: var(--text-fine); opacity: 0.5; }.alert .alert-title {
6337 font-family: var(--font-mono);
6338 font-size: var(--text-note);
6339 font-weight: bold;
6340 text-transform: uppercase;
6341 letter-spacing: 0.04em;
6342 margin-bottom: var(--gap-bound);
6343 }
6344 .alert p:last-child { margin-bottom: 0; }
6345 .alert-note { border-left-color: var(--focus-ring); }
6346 .alert-note .alert-title { color: var(--focus-ring); }
6347 .alert-tip { border-left-color: var(--success); }
6348 .alert-tip .alert-title { color: var(--success); }
6349 .alert-important { border-left-color: var(--action); }
6350 .alert-important .alert-title { color: var(--action); }
6351 .alert-warning { border-left-color: var(--warning); }
6352 .alert-warning .alert-title { color: var(--warning); }
6353 .alert-caution { border-left-color: var(--danger); }
6354 .alert-caution .alert-title { color: var(--danger); }
6355
6356 /* ===========================================
6357 USE CASES PAGE
6358 =========================================== */
6359
6360 /* The use-cases page moved to the description layer; what the template's own
6361 classes said is drawn by the design system now. What is left is the measure,
6362 which a described document carries on <main> rather than on a container div,
6363 the same shape as `.team-page main` and `.feed-page main`.
6364
6365 The grid went with it deliberately: nine equal cards are nine sibling groups
6366 in the description, and how they sit is the design system's answer for every
6367 consumer rather than a `grid-template-columns` here. */
6368 .use-cases-page main {
6369 max-width: 900px;
6370 margin: 0 auto;
6371 }
6372
6373 /* ===========================================
6374 RESPONSIVE, 768px (tablet)
6375 =========================================== */
6376
6377 /* ==========================================================================
6378 Git Source Browser
6379 ========================================================================== */
6380
6381 .git-repo-header { margin-bottom: var(--gap-pane); }
6382 .git-repo-name { font-size: var(--text-head); margin: 0 0 var(--gap-bound); }
6383 .git-repo-name a { color: var(--content); text-decoration: none; }
6384 .git-repo-name a:hover { opacity: 0.6; }
6385 .git-repo-name .sep { opacity: 0.3; margin: 0 var(--gap-bound); }
6386 .git-repo-desc { opacity: 0.6; margin: 0 0 var(--gap-section); font-size: var(--text-note); }
6387 .git-clone-url {
6388 font-family: var(--font-mono);
6389 font-size: var(--text-fine);
6390 padding: var(--gap-peer) var(--gap-group);
6391 background: var(--surface-overlay);
6392 border: 1px solid var(--border);
6393 border-radius: var(--radius-control);
6394 display: inline-block;
6395 user-select: all;
6396 margin-bottom: var(--gap-section);
6397 }
6398 .git-linked-project { margin-top: var(--gap-peer); font-size: var(--text-note); }
6399 .git-linked-project a { color: var(--content); }
6400
6401 /* Ref bar (branch/tag selector + nav links) */
6402 .git-ref-bar {
6403 display: flex;
6404 align-items: center;
6405 gap: var(--gap-section);
6406 margin-bottom: var(--gap-section);
6407 font-size: var(--text-note);
6408 }
6409 .git-ref-select {
6410 font-family: var(--font-mono);
6411 font-size: var(--text-fine);
6412 padding: var(--gap-bound) var(--gap-peer);
6413 background: var(--surface-overlay);
6414 border: 1px solid var(--border);
6415 border-radius: var(--radius-control);
6416 }
6417 .git-nav-links { display: flex; gap: var(--gap-section); }
6418 .git-nav-links a { text-decoration: none; color: var(--content); opacity: 0.6; }
6419 .git-nav-links a:hover,
6420 .git-nav-links a.is-selected { opacity: 1; }
6421
6422 /* Breadcrumb navigation */
6423 .git-breadcrumb {
6424 font-family: var(--font-mono);
6425 font-size: var(--text-note);
6426 margin-bottom: var(--gap-section);
6427 }
6428 .git-breadcrumb a { color: var(--content); text-decoration: none; }
6429 .git-breadcrumb a:hover { text-decoration: underline; }
6430 .git-breadcrumb .sep { opacity: 0.3; margin: 0 var(--gap-bound); }
6431
6432 /* File tree table */
6433 .git-tree { width: 100%; border-collapse: collapse; font-size: var(--text-note); }
6434 .git-tree th {
6435 text-align: left;
6436 padding: var(--gap-peer) var(--gap-group);
6437 border-bottom: 2px solid var(--border);
6438 font-family: var(--font-mono);
6439 font-size: var(--text-fine);
6440 opacity: 0.5;
6441 text-transform: uppercase;
6442 }
6443 .git-tree td { padding: var(--gap-peer) var(--gap-group); border-bottom: 1px solid var(--border); }
6444 .git-tree tr:last-child td { border-bottom: none; }
6445 .git-tree .icon { width: 1.5rem; text-align: center; opacity: 0.4; font-family: var(--font-mono); }
6446 .tree-icon-dir { font-weight: 700; color: var(--content); }
6447 .tree-icon-file { font-size: var(--text-note); }
6448 .git-tree .name a { text-decoration: none; color: var(--content); }
6449 .git-tree .name a:hover { text-decoration: underline; }
6450 .git-tree .size {
6451 text-align: right;
6452 opacity: 0.5;
6453 font-family: var(--font-mono);
6454 font-size: var(--text-fine);
6455 }
6456
6457 /* README rendering */
6458 .git-readme { margin-top: var(--gap-page); padding-top: var(--gap-pane); border-top: 1px solid var(--border); }
6459 .git-readme h2 { font-size: var(--text-body); opacity: 0.5; margin-bottom: var(--gap-section); }
6460 .git-readme-body { line-height: 1.6; }
6461 .git-readme-body h1,
6462 .git-readme-body h2,
6463 .git-readme-body h3 { margin-top: var(--gap-pane); margin-bottom: var(--gap-peer); }
6464 .git-readme-body pre {
6465 background: var(--surface-overlay);
6466 padding: var(--gap-section);
6467 border-radius: var(--radius-control);
6468 overflow-x: auto;
6469 font-family: var(--font-mono);
6470 font-size: var(--text-fine);
6471 }
6472 .git-readme-body code {
6473 font-family: var(--font-mono);
6474 font-size: var(--text-note);
6475 background: var(--surface-overlay);
6476 padding: var(--gap-bound);
6477 border-radius: var(--radius-fine);
6478 }
6479 .git-readme-body pre code { background: none; padding: 0; }
6480 .git-readme-body table { border-collapse: collapse; margin: var(--gap-section) 0; }
6481 .git-readme-body th,
6482 .git-readme-body td { border: 1px solid var(--border); padding: var(--gap-peer) var(--gap-group); }
6483
6484 /* File viewer */
6485 .git-file-header {
6486 display: flex;
6487 justify-content: space-between;
6488 align-items: center;
6489 padding: var(--gap-peer) var(--gap-group);
6490 background: var(--surface-overlay);
6491 border: 1px solid var(--border);
6492 border-bottom: none;
6493 border-radius: var(--radius-control) var(--radius-control) 0 0;
6494 font-size: var(--text-fine);
6495 }
6496 .git-file-meta {
6497 font-family: var(--font-mono);
6498 opacity: 0.6;
6499 }
6500 .git-file-actions a {
6501 font-family: var(--font-mono);
6502 text-decoration: none;
6503 color: var(--content);
6504 opacity: 0.6;
6505 font-size: var(--text-fine);
6506 }
6507 .git-file-actions a:hover { opacity: 1; }
6508 .git-file-content {
6509 border: 1px solid var(--border);
6510 border-radius: 0 0 var(--radius-control) var(--radius-control);
6511 overflow-x: auto;
6512 }
6513 .git-file-content table { border-collapse: collapse; width: 100%; }
6514 .git-file-content .line-number {
6515 width: 1px;
6516 white-space: nowrap;
6517 padding: 0 var(--gap-section);
6518 text-align: right;
6519 user-select: none;
6520 opacity: 0.3;
6521 font-family: var(--font-mono);
6522 font-size: var(--text-fine);
6523 border-right: 1px solid var(--border);
6524 vertical-align: top;
6525 }
6526 .git-file-content .line-code {
6527 padding: 0 var(--gap-section);
6528 white-space: pre;
6529 font-family: var(--font-mono);
6530 font-size: var(--text-fine);
6531 line-height: 1.5;
6532 }
6533 .git-file-content .line-code span {
6534 font-family: var(--font-mono);
6535 }
6536 .git-binary-notice {
6537 padding: var(--gap-page);
6538 text-align: center;
6539 opacity: 0.5;
6540 font-family: var(--font-mono);
6541 font-size: var(--text-note);
6542 border: 1px solid var(--border);
6543 border-radius: 0 0 var(--radius-control) var(--radius-control);
6544 }
6545
6546 /* Syntax highlighting.
6547 The class names are quasi-webview's, from `makeover_layout::Syntax::name`,
6548 and the scope-to-class mapping is `src/git/mod.rs`'s `syntax_of` (quasi
6549 `19d7602d`, 2026-09-02). It matched syntect's own emitted scopes until then,
6550 which meant this file held half a mapping the server held the other half of.
6551 The palette below it is unchanged and still fixed across all 31 themes.
6552
6553 Not scoped under `.git-file-content` any more: a code node is a code node
6554 wherever it is drawn, and a fingerprint in a table or a clone URL in a
6555 sentence wants the same colours as a line of a file. */
6556 .lex-keyword { color: var(--syntax-keyword); }
6557 .lex-string { color: var(--syntax-string); }
6558 .lex-comment { color: var(--syntax-comment); font-style: italic; }
6559 .lex-constant { color: var(--syntax-constant); }
6560 .lex-entity { color: var(--syntax-entity); }
6561 .lex-variable { color: var(--syntax-variable); }
6562 .lex-support { color: var(--syntax-support); }
6563
6564 /* The element the runs sit in. `code` is quasi-webview's, and it is the whole
6565 of what makes an inline literal read as one: a monospace face and the
6566 whitespace kept. A block form (`<pre>`) inherits both from the browser. */
6567 code.code {
6568 font-family: var(--font-mono);
6569 white-space: pre;
6570 }
6571
6572 /* Commit list */
6573 .git-commit-list { list-style: none; }
6574 .git-commit { padding: var(--gap-section) 0; border-bottom: 1px solid var(--border); }
6575 .git-commit:last-child { border-bottom: none; }
6576 .git-commit-message { font-size: var(--text-note); margin: 0 0 var(--gap-bound); }
6577 .git-commit-message .summary { font-weight: 600; }
6578 .git-commit-meta {
6579 font-family: var(--font-mono);
6580 font-size: var(--text-fine);
6581 opacity: 0.5;
6582 display: flex;
6583 gap: var(--gap-section);
6584 flex-wrap: wrap;
6585 }
6586 .git-commit-oid { font-family: var(--font-mono); font-size: var(--text-fine); }
6587 .git-commit-oid a { color: var(--content); text-decoration: none; }
6588 .git-commit-oid a:hover { text-decoration: underline; }
6589 .git-pagination {
6590 display: flex;
6591 justify-content: center;
6592 gap: var(--gap-section);
6593 margin-top: var(--gap-pane);
6594 padding-top: var(--gap-section);
6595 font-size: var(--text-note);
6596 }
6597 .git-pagination a { color: var(--content); text-decoration: none; }
6598 .git-pagination a:hover { text-decoration: underline; }
6599
6600 /* Release items on repo page */
6601 .git-release { margin-bottom: var(--gap-pane); padding: var(--gap-section); background: var(--surface-overlay); }
6602 .git-release-title { margin: 0 0 var(--gap-bound); font-size: var(--text-lead); }
6603 .git-release-title a { color: var(--content); text-decoration: none; }
6604 .git-release-meta { font-size: var(--text-note); opacity: 0.6; margin-bottom: var(--gap-peer); }
6605 .git-release-desc { font-size: var(--text-note); opacity: 0.8; margin-bottom: var(--gap-section); }
6606 .git-release-versions { font-size: var(--text-fine); }
6607 .git-release-version {
6608 display: flex;
6609 gap: var(--gap-section);
6610 align-items: center;
6611 padding: var(--gap-bound) 0;
6612 border-top: 1px solid var(--border);
6613 }
6614 .git-release-version .version-number { font-family: var(--font-mono); }
6615 .git-release-version .version-badge {
6616 font-size: var(--text-fine);
6617 padding: var(--gap-bound) var(--gap-peer);
6618 background: var(--content);
6619 color: var(--surface-page);
6620 border-radius: var(--radius-fine);
6621 }
6622 .git-release-version .version-meta { opacity: 0.5; }
6623
6624 /* Git commit detail + diff */
6625 .git-commit-detail { margin-bottom: var(--gap-pane); }
6626 .git-commit-full-message {
6627 font-family: var(--font-mono);
6628 font-size: var(--text-note);
6629 white-space: pre-wrap;
6630 line-height: 1.5;
6631 padding: var(--gap-section);
6632 background: var(--surface-overlay);
6633 border: 1px solid var(--border);
6634 margin-bottom: var(--gap-section);
6635 }
6636 .git-commit-detail-meta { font-size: var(--text-note); opacity: 0.7; line-height: 1.8; }
6637 .git-commit-detail-meta .git-commit-oid { font-family: var(--font-mono); font-size: var(--text-fine); word-break: break-all; }
6638 .git-commit-parents a { font-family: var(--font-mono); color: var(--content); text-decoration: none; }
6639 .git-commit-parents a:hover { text-decoration: underline; }
6640 /* Commit trailers, lifted out of the message body and shown as what they are */
6641 .git-commit-trailers {
6642 display: grid;
6643 grid-template-columns: max-content 1fr;
6644 gap: var(--gap-bound) var(--gap-section);
6645 font-size: var(--text-note);
6646 margin-bottom: var(--gap-section);
6647 }
6648 .git-commit-trailers dt { opacity: 0.6; }
6649 .git-commit-trailers dd { margin: 0; font-family: var(--font-mono); font-size: var(--text-fine); }
6650 /* Tags, with the annotation an annotated tag carries */
6651 .git-tag-list { list-style: none; }
6652 .git-tag { padding: var(--gap-section) 0; border-bottom: 1px solid var(--border); }
6653 .git-tag:last-child { border-bottom: none; }
6654 .git-tag-name { font-size: var(--text-note); font-weight: 600; margin: 0 0 var(--gap-bound); }
6655 .git-tag-name a { color: var(--content); text-decoration: none; }
6656 .git-tag-name a:hover { text-decoration: underline; }
6657 .git-tag-lightweight { font-weight: 400; font-size: var(--text-fine); opacity: 0.5; }
6658 .git-tag-message {
6659 font-family: var(--font-mono);
6660 font-size: var(--text-note);
6661 white-space: pre-wrap;
6662 line-height: 1.5;
6663 padding: var(--gap-peer) var(--gap-section);
6664 border-left: 2px solid var(--border);
6665 margin-bottom: var(--gap-bound);
6666 }
6667 .git-tag-meta {
6668 font-family: var(--font-mono);
6669 font-size: var(--text-fine);
6670 opacity: 0.5;
6671 display: flex;
6672 gap: var(--gap-section);
6673 flex-wrap: wrap;
6674 }
6675 .git-tag-meta a { color: var(--content); text-decoration: none; }
6676 .git-tag-meta a:hover { text-decoration: underline; }
6677 /* Signature state. Neutral by default: only a bad signature gets a colour. */
6678 .git-signature { font-size: var(--text-fine); opacity: 0.7; }
6679 .git-signature a { color: var(--content); }
6680 .git-signature.is-verified { opacity: 1; }
6681 .git-signature.is-bad { color: var(--danger); opacity: 1; }
6682 /* refs/replace/*: object substitutions, listed but not applied while browsing */
6683 .git-replace-intro { padding-bottom: var(--gap-section); font-size: var(--text-note); }
6684 .git-replace-intro p { margin: 0; }
6685 .git-replace-notice {
6686 font-size: var(--text-note);
6687 border-left: 2px solid var(--border);
6688 padding: var(--gap-peer) var(--gap-section);
6689 margin: 0 0 var(--gap-section);
6690 }
6691 .git-replace-table { width: 100%; border-collapse: collapse; font-size: var(--text-note); }
6692 .git-replace-table th {
6693 text-align: left;
6694 font-weight: 400;
6695 opacity: 0.5;
6696 font-size: var(--text-fine);
6697 padding: var(--gap-bound) var(--gap-section) var(--gap-bound) 0;
6698 border-bottom: 1px solid var(--border);
6699 }
6700 .git-replace-table td { padding: var(--gap-peer) var(--gap-section) var(--gap-peer) 0; border-bottom: 1px solid var(--border); }
6701 .git-replace-table a { color: var(--content); text-decoration: none; }
6702 .git-replace-table a:hover { text-decoration: underline; }
6703 /* Git notes (refs/notes/*) shown against the object they annotate */
6704 .git-notes { margin-bottom: var(--gap-pane); }
6705 .git-note {
6706 border: 1px solid var(--border);
6707 border-left: 2px solid var(--content);
6708 margin-bottom: var(--gap-section);
6709 }
6710 .git-note-header {
6711 display: flex;
6712 align-items: baseline;
6713 justify-content: space-between;
6714 gap: var(--gap-group);
6715 padding: var(--gap-peer) var(--gap-section);
6716 border-bottom: 1px solid var(--border);
6717 font-size: var(--text-fine);
6718 }
6719 .git-note-namespace { font-family: var(--font-mono); }
6720 /* Sits beside the namespace rather than being spread across the header: the
6721 auto margin absorbs the free space `space-between` would otherwise put
6722 between the two, leaving the attribution on the right where it was. */
6723 .git-note-owned { margin-right: auto; opacity: 0.6; }
6724 .git-note-attribution { opacity: 0.6; }
6725 .git-note-namespaces {
6726 display: flex;
6727 flex-wrap: wrap;
6728 gap: var(--gap-group);
6729 padding: var(--gap-section) 0;
6730 border-bottom: 1px solid var(--border);
6731 margin-bottom: var(--gap-section);
6732 font-size: var(--text-note);
6733 }
6734 .git-note-namespaces a { color: var(--content); text-decoration: none; opacity: 0.6; }
6735 .git-note-namespaces a:hover { opacity: 1; }
6736 .git-note-namespaces a.is-selected { opacity: 1; text-decoration: underline; }
6737 .git-note-target-kind { opacity: 0.6; }
6738 .git-notes-fetch { padding-bottom: var(--gap-section); font-size: var(--text-note); }
6739 .git-notes-fetch p { margin: 0 0 var(--gap-peer); }
6740 .git-note-feed-cap { font-size: var(--text-fine); opacity: 0.6; padding-top: var(--gap-section); }
6741 .git-note-search {
6742 display: flex;
6743 flex-wrap: wrap;
6744 align-items: center;
6745 gap: var(--gap-group);
6746 padding-bottom: var(--gap-section);
6747 font-size: var(--text-note);
6748 }
6749 .git-note-search input[type="search"] { flex: 1 1 16rem; }
6750 .git-note-search label { display: flex; align-items: center; gap: var(--gap-bound); opacity: 0.8; }
6751 .git-note-excerpt {
6752 margin: var(--gap-bound) 0 0;
6753 font-size: var(--text-note);
6754 opacity: 0.7;
6755 }
6756 .git-note-paging {
6757 display: flex;
6758 gap: var(--gap-group);
6759 align-items: center;
6760 padding-top: var(--gap-section);
6761 font-size: var(--text-note);
6762 }
6763 .git-note-paging span { font-size: var(--text-fine); opacity: 0.6; }
6764 .git-note-feed-link { padding-top: var(--gap-peer); font-size: var(--text-fine); }
6765 .git-note-badge {
6766 font-size: var(--text-fine);
6767 padding: var(--gap-bound) var(--gap-peer);
6768 border: 1px solid var(--border);
6769 border-radius: var(--radius-fine);
6770 color: var(--content);
6771 text-decoration: none;
6772 opacity: 0.7;
6773 }
6774 .git-note-badge:hover { opacity: 1; }
6775 .git-note-body { padding: var(--gap-section); font-size: var(--text-note); }
6776 .git-note-body > :first-child { margin-top: 0; }
6777 .git-note-body > :last-child { margin-bottom: 0; }
6778 /* Writing a note. Only rendered for a reader who can push. */
6779 .git-notes-edit {
6780 border-top: 1px solid var(--border);
6781 padding-top: var(--gap-section);
6782 margin-bottom: var(--gap-pane);
6783 }
6784 .git-notes-edit textarea { width: 100%; font-family: var(--font-mono); font-size: var(--text-note); }
6785 .git-note-form { margin-bottom: var(--gap-section); }
6786 .git-note-delete { margin-bottom: var(--gap-pane); }
6787 .git-note-merged {
6788 border-left: 2px solid var(--content);
6789 padding: var(--gap-peer) var(--gap-section);
6790 margin-bottom: var(--gap-section);
6791 font-size: var(--text-note);
6792 }
6793 /* A reader's own annotation, which lives in their repository rather than this one. */
6794 .git-annotation {
6795 border-top: 1px solid var(--border);
6796 padding-top: var(--gap-section);
6797 margin-bottom: var(--gap-pane);
6798 }
6799 .git-annotation textarea { width: 100%; font-family: var(--font-mono); font-size: var(--text-note); }
6800 .git-annotations {
6801 border-top: 1px solid var(--border);
6802 padding-top: var(--gap-section);
6803 margin-bottom: var(--gap-pane);
6804 }
6805 .git-annotations-heading { font-size: var(--text-note); margin: 0 0 var(--gap-peer); }
6806 .git-annotations-privacy,
6807 .git-annotations-empty,
6808 .git-annotations-count { font-size: var(--text-note); color: var(--content-muted); }
6809 .git-annotations-list { list-style: none; margin: 0; padding: 0; }
6810 .git-annotation-entry {
6811 border: 1px solid var(--border);
6812 padding: var(--gap-section);
6813 margin-bottom: var(--gap-section);
6814 }
6815 .git-annotation-header {
6816 display: flex;
6817 gap: var(--gap-peer);
6818 flex-wrap: wrap;
6819 align-items: baseline;
6820 font-size: var(--text-note);
6821 margin-bottom: var(--gap-peer);
6822 }
6823 .git-annotation-target,
6824 .git-annotation-namespace,
6825 .git-annotation-when { font-family: var(--font-mono); color: var(--content-muted); }
6826 .git-annotation-orphan { font-size: var(--text-note); color: var(--content-muted); }
6827 .git-diff-stats {
6828 font-size: var(--text-note);
6829 padding: var(--gap-section) 0;
6830 margin-bottom: var(--gap-section);
6831 border-bottom: 1px solid var(--border);
6832 }
6833 .git-diff-stats .additions { color: var(--diff-add); }
6834 .git-diff-stats .deletions { color: var(--diff-del); }
6835 .git-diff-file { margin-bottom: var(--gap-pane); border: 1px solid var(--border); }
6836 .git-diff-file-header {
6837 display: flex;
6838 align-items: center;
6839 gap: var(--gap-peer);
6840 padding: var(--gap-peer) var(--gap-section);
6841 background: var(--surface-overlay);
6842 border-bottom: 1px solid var(--border);
6843 font-size: var(--text-note);
6844 flex-wrap: wrap;
6845 }
6846 .git-diff-file-header a { color: var(--content); text-decoration: none; }
6847 .git-diff-file-header a:hover { text-decoration: underline; }
6848 .git-diff-file-stats { margin-left: auto; font-size: var(--text-fine); }
6849 .git-diff-file-stats .additions { color: var(--diff-add); }
6850 .git-diff-file-stats .deletions { color: var(--diff-del); margin-left: var(--gap-bound); }
6851 .diff-status-added, .diff-status-deleted, .diff-status-modified, .diff-status-renamed {
6852 display: inline-block;
6853 width: 1.2rem;
6854 text-align: center;
6855 font-size: var(--text-fine);
6856 font-weight: 700;
6857 font-family: var(--font-mono);
6858 border-radius: var(--radius-fine);
6859 line-height: 1.4;
6860 }
6861 .diff-status-added { background: color-mix(in oklch, var(--success) 14%, var(--surface-page)); color: var(--success); }
6862 .diff-status-deleted { background: color-mix(in oklch, var(--danger) 14%, var(--surface-page)); color: var(--danger); }
6863 .diff-status-modified { background: color-mix(in oklch, var(--warning) 14%, var(--surface-page)); color: var(--warning); }
6864 .diff-status-renamed { background: color-mix(in oklch, var(--info) 14%, var(--surface-page)); color: var(--info); }
6865 .git-diff-table {
6866 width: 100%;
6867 border-collapse: collapse;
6868 font-family: var(--font-mono);
6869 font-size: var(--text-fine);
6870 table-layout: fixed;
6871 }
6872 .git-diff-hunk-header td {
6873 padding: var(--gap-bound) var(--gap-peer);
6874 background: var(--highlight-faint);
6875 color: var(--content);
6876 opacity: 0.6;
6877 font-size: var(--text-fine);
6878 }
6879 .git-diff-lineno {
6880 width: 3.5rem;
6881 padding: 0 var(--gap-peer);
6882 text-align: right;
6883 color: var(--content);
6884 opacity: 0.35;
6885 user-select: none;
6886 vertical-align: top;
6887 }
6888 .git-diff-origin {
6889 width: 1rem;
6890 text-align: center;
6891 user-select: none;
6892 vertical-align: top;
6893 }
6894 .git-diff-content {
6895 padding: 0 var(--gap-peer);
6896 white-space: pre-wrap;
6897 word-break: break-all;
6898 }
6899 .git-diff-line-add { background: var(--diff-add-bg); }
6900 .git-diff-line-add .git-diff-origin { color: var(--diff-add); }
6901 .git-diff-line-del { background: var(--diff-del-bg); }
6902 .git-diff-line-del .git-diff-origin { color: var(--diff-del); }
6903 .git-diff-line-ctx { background: transparent; }
6904 .git-diff-truncated { padding: var(--gap-peer) var(--gap-section); font-size: var(--text-fine); opacity: 0.5; font-style: italic; }
6905
6906 /* Git file view line linking */
6907 .git-file-content .line-number a {
6908 color: inherit;
6909 text-decoration: none;
6910 display: block;
6911 }
6912 .git-file-content .line-number a:hover { opacity: 0.7; }
6913 .git-file-content tr:target td,
6914 .git-file-content tr.highlighted td { background: var(--highlight-faint); }
6915
6916 /* Git blame view */
6917 .git-blame-content { overflow-x: auto; border: 1px solid var(--border); }
6918 .git-blame-content table { border-collapse: collapse; width: 100%; font-size: var(--text-fine); }
6919 .git-blame-info {
6920 padding: 0 var(--gap-peer);
6921 font-family: var(--font-mono);
6922 white-space: nowrap;
6923 width: 4.5rem;
6924 }
6925 .git-blame-info a { color: var(--content); text-decoration: none; }
6926 .git-blame-info a:hover { text-decoration: underline; }
6927 .git-blame-author {
6928 padding: 0 var(--gap-bound);
6929 font-size: var(--text-fine);
6930 opacity: 0.5;
6931 white-space: nowrap;
6932 max-width: 8rem;
6933 overflow: hidden;
6934 text-overflow: ellipsis;
6935 }
6936 .git-blame-date {
6937 padding: 0 var(--gap-bound);
6938 font-size: var(--text-fine);
6939 opacity: 0.35;
6940 white-space: nowrap;
6941 width: 5.5rem;
6942 }
6943 .git-blame-content .line-number {
6944 padding: 0 var(--gap-peer);
6945 text-align: right;
6946 opacity: 0.35;
6947 user-select: none;
6948 width: 3rem;
6949 font-family: var(--font-mono);
6950 }
6951 .git-blame-content .line-code {
6952 padding: 0 var(--gap-peer);
6953 white-space: pre;
6954 font-family: var(--font-mono);
6955 }
6956 .git-blame-boundary td { border-top: 1px solid var(--border); }
6957
6958 /* The two repository listings, /git and /git/{owner}, moved to the description
6959 layer. Both were a <ul> of two-line entries with a visibility chip; both are
6960 a `Node::Table` now, so which column carries the identity and which can be
6961 dropped on a narrow viewport is stated rather than left to wrapping, and the
6962 chip is a `Tag` on a cell. */
6963
6964 .git-file-log-label {
6965 font-family: var(--font-mono);
6966 font-size: var(--text-fine);
6967 opacity: 0.5;
6968 margin-right: var(--gap-bound);
6969 }
6970
6971 /* Site-wide footer */
6972 .site-footer {
6973 margin-top: var(--gap-page);
6974 padding: var(--gap-page) 0;
6975 text-align: center;
6976 font-family: var(--font-mono);
6977 font-size: var(--text-fine);
6978 opacity: 0.5;
6979 }
6980 .site-footer-links { margin-bottom: var(--gap-peer); }
6981 .site-footer-links a { margin: 0 var(--gap-section); color: inherit; text-decoration: none; }
6982 .site-footer-links a:hover { text-decoration: underline; }
6983
6984 @media (max-width: 839px) {
6985 /* Git browser mobile */
6986 .git-ref-bar { flex-wrap: wrap; gap: var(--gap-peer); }
6987 .git-repo-name { font-size: var(--text-subhead); }
6988 .git-clone-url { font-size: var(--text-fine); word-break: break-all; }
6989 .git-tree .icon { display: none; }
6990 .git-tree td, .git-tree th { padding: var(--gap-bound) var(--gap-peer); }
6991 .git-file-header { flex-direction: column; align-items: flex-start; gap: var(--gap-bound); }
6992 .git-file-content .line-number { padding: 0 var(--gap-peer); }
6993 .git-file-content .line-code { padding: 0 var(--gap-peer); font-size: var(--text-fine); }
6994 .git-commit-meta { flex-direction: column; gap: var(--gap-bound); }
6995 .git-release-version { flex-wrap: wrap; gap: var(--gap-peer); }
6996 .git-diff-file-header { font-size: var(--text-fine); }
6997 .git-diff-table { font-size: var(--text-fine); }
6998 .git-diff-lineno { width: 2.5rem; font-size: var(--text-fine); }
6999 .git-blame-author { display: none; }
7000 .git-blame-date { display: none; }
7001
7002 .container {
7003 padding: var(--gap-section);
7004 }
7005
7006 .padded-page {
7007 padding: var(--gap-section);
7008 }
7009
7010 .form-row {
7011 grid-template-columns: 1fr;
7012 }
7013
7014 .tab {
7015 padding: var(--gap-group) var(--gap-section);
7016 font-size: var(--text-note);
7017 }
7018
7019 .tab-content {
7020 padding: var(--gap-section);
7021 }
7022
7023 .data-table th,
7024 .data-table td {
7025 padding: var(--gap-peer) var(--gap-group);
7026 font-size: var(--text-note);
7027 }
7028
7029 .modal {
7030 max-width: 95%;
7031 }
7032
7033 .stats-grid {
7034 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
7035 }
7036
7037 .content-section {
7038 padding: var(--gap-section);
7039 }
7040
7041 .action-buttons {
7042 flex-direction: column;
7043 }
7044
7045 .action-buttons button {
7046 width: 100%;
7047 }
7048
7049 .analytics-grid {
7050 grid-template-columns: 1fr;
7051 }
7052
7053 /* The disclosure. `~` reaches siblings and only forward, which is the whole
7054 reason the header had to become one element: the checkbox, the search box
7055 and the nav are inside `<header class="chrome-band">` and nothing else. */
7056 .chrome-disclose {
7057 display: block;
7058 }
7059
7060 .chrome-search {
7061 display: none;
7062 flex: none;
7063 margin: 0;
7064 width: 100%;
7065 }
7066
7067 .chrome-disclose-state:checked ~ .chrome-search {
7068 display: block;
7069 position: absolute;
7070 top: 100%;
7071 left: 0;
7072 right: 0;
7073 padding: var(--gap-section) var(--gap-pane);
7074 background: var(--surface-page);
7075 border-bottom: 1px solid var(--border);
7076 z-index: var(--z-header);
7077 }
7078
7079 .chrome-band .chrome-nav {
7080 display: none;
7081 position: absolute;
7082 top: 100%;
7083 left: 0;
7084 right: 0;
7085 background: var(--surface-page);
7086 border-bottom: 1px solid var(--border);
7087 padding: var(--gap-section) var(--gap-pane);
7088 z-index: var(--z-nav);
7089 }
7090
7091 .chrome-band {
7092 position: relative;
7093 }
7094
7095 .chrome-disclose-state:checked ~ .chrome-nav {
7096 display: block;
7097 }
7098
7099 .chrome-band .chrome-nav ul {
7100 flex-direction: column;
7101 gap: var(--gap-section);
7102 }
7103
7104 /* Animate the bars to an X when open */
7105 .chrome-disclose-state:checked ~ .chrome-disclose span:nth-child(1) {
7106 transform: rotate(45deg) translate(5px, 5px);
7107 }
7108
7109 .chrome-disclose-state:checked ~ .chrome-disclose span:nth-child(2) {
7110 opacity: 0;
7111 }
7112
7113 .chrome-disclose-state:checked ~ .chrome-disclose span:nth-child(3) {
7114 transform: rotate(-45deg) translate(5px, -5px);
7115 }
7116 }
7117
7118 /* Doc pages, mobile */
7119 @media (max-width: 599px) {
7120 .doc-title { font-size: var(--text-title); }
7121 .doc-body { font-size: var(--text-body); }
7122 .docs-index-title { font-size: var(--text-title); }
7123 }
7124
7125 /* ===========================================
7126 RESPONSIVE, 480px (phone)
7127 =========================================== */
7128
7129 @media (max-width: 599px) {
7130 .container {
7131 padding: var(--gap-section);
7132 }
7133
7134 .padded-page {
7135 padding: var(--gap-section);
7136 }
7137
7138 h1 {
7139 font-size: var(--text-title);
7140 }
7141
7142 .centered-page h1 {
7143 font-size: var(--text-hero);
7144 }
7145
7146 #login-container,
7147 .login-container,
7148 .form-container {
7149 max-width: 100%;
7150 padding: var(--gap-section);
7151 }
7152
7153 .big-button {
7154 width: 100%;
7155 }
7156
7157 .stats-grid {
7158 grid-template-columns: repeat(2, 1fr);
7159 }
7160
7161 .tab {
7162 padding: var(--gap-peer) var(--gap-section);
7163 font-size: var(--text-note);
7164 }
7165
7166 .tab-content {
7167 padding: var(--gap-section);
7168 }
7169
7170 .content-section {
7171 padding: var(--gap-section);
7172 }
7173
7174 .section-header {
7175 flex-direction: column;
7176 align-items: flex-start;
7177 gap: var(--gap-section);
7178 }
7179
7180 /* Landing page (merged from 500px) */
7181 .landing-fork,
7182 .tier-selector {
7183 grid-template-columns: 1fr;
7184 }
7185
7186 .pricing-input {
7187 font-size: var(--text-title);
7188 }
7189
7190 .pricing-currency {
7191 font-size: var(--text-title);
7192 }
7193
7194 .calc-verdict {
7195 font-size: var(--text-body);
7196 }
7197
7198 .calc-dials {
7199 grid-template-columns: 1fr;
7200 }
7201
7202 .secondary-links {
7203 flex-direction: column;
7204 align-items: center;
7205 gap: var(--gap-section);
7206 }
7207
7208 .toast-container {
7209 left: 0.75rem;
7210 right: 0.75rem;
7211 bottom: 0.75rem;
7212 }
7213
7214 .toast {
7215 max-width: 100%;
7216 }
7217 }
7218
7219 /* ===========================================
7220 DISCOVER PAGE
7221 =========================================== */
7222
7223 .discover-layout { display: grid; grid-template-columns: 200px 1fr; gap: var(--gap-pane); }
7224 .discover-layout.no-sidebar { grid-template-columns: 1fr; }
7225 .discover-layout.no-sidebar .discover-sidebar { display: none; }
7226 .discover-sidebar { font-size: var(--text-note); }
7227 .filter-section { margin-bottom: var(--gap-section); }
7228 .filter-title { font-family: var(--font-display); font-weight: bold; font-size: var(--text-note); margin-bottom: var(--gap-peer); color: var(--content); }
7229 .filter-list { list-style: none; }
7230 /* The row carries selection (`.is-selected` recipe) and hover; the filter itself
7231 is a real <button> child so Enter/Space work without scripting. */
7232 .filter-item { display: flex; align-items: center; gap: var(--gap-bound); transition: opacity 0.1s ease; }
7233 .filter-item:hover { opacity: 0.6; }
7234 .filter-item.is-selected { font-weight: bold; }
7235 .filter-item .count { opacity: 0.4; font-size: var(--text-fine); }
7236 .tag-follow-btn { background: none; border: 1px solid var(--border); cursor: pointer; font-family: var(--font-mono); font-size: var(--text-fine); opacity: 0.55; padding: var(--gap-bound) var(--gap-peer); color: var(--content); }
7237 .tag-follow-btn:hover { opacity: 1; }
7238 .tag-follow-btn.is-selected { opacity: 0.85; border-color: var(--focus-ring); background: var(--highlight-faint); }
7239 .filter-checkbox { display: flex; align-items: center; gap: var(--gap-peer); padding: var(--gap-bound) 0; cursor: pointer; }
7240 .filter-checkbox input { width: auto; margin: 0; }
7241 .price-inputs { display: flex; gap: var(--gap-bound); align-items: center; margin-bottom: var(--gap-peer); }
7242 .price-inputs input { width: 60px; padding: var(--gap-bound); font-size: var(--text-fine); background: var(--surface-sunken); border: none; }
7243 .price-inputs span { opacity: 0.4; }
7244 .discover-main { min-width: 0; }
7245
7246 /* ---- Discover sidebar: tag spine ----------------------------------------
7247 Tags are the sidebar's structure, not one facet among several, so the spine
7248 reads as the primary surface and .filter-refine below it is subordinate.
7249 Everything here composes existing primitives: .filter-item carries the row
7250 and its .is-selected recipe, the described field draws its own list, and
7251 .breadcrumb carries the crumb idiom. */
7252 .tag-spine { padding-bottom: var(--gap-section); border-bottom: 1px solid var(--border); }
7253
7254 /* Selected tags. Also the active-filter summary, so they use the canonical
7255 selected tint rather than a new colour. */
7256 .tag-chips { list-style: none; display: flex; flex-wrap: wrap; gap: var(--gap-bound); margin-bottom: var(--gap-section); }
7257 .tag-chip { display: flex; align-items: center; gap: var(--gap-bound); padding: var(--gap-bound) var(--gap-peer); font-size: var(--text-fine); background: var(--highlight-faint); border: 1px solid var(--focus-ring); }
7258 .tag-chip-label { overflow-wrap: anywhere; }
7259 .tag-chip-remove { text-decoration: none; color: var(--content-secondary); font-size: var(--text-body); line-height: 1; padding: 0 var(--gap-bound); }
7260 .tag-chip-remove:hover { color: var(--content); }
7261 .tag-chip-clear { background: none; border: 1px dashed var(--border-strong); }
7262 .tag-chip-clear a { text-decoration: none; color: var(--content-secondary); font-size: var(--text-fine); }
7263 .tag-chip-clear a:hover { color: var(--content); }
7264
7265 /* Typeahead. The box, the list and everything that opens and closes it are
7266 the described field's, so all this carries is the positioning context the
7267 list is drawn against. */
7268 .tag-combobox { position: relative; margin-bottom: var(--gap-section); }
7269
7270 /* Drill-down. A rung either selects (assignable leaf) or navigates (category);
7271 rows with both put the chevron after the label. */
7272 .tag-crumbs { list-style: none; display: flex; flex-wrap: wrap; font-family: var(--font-mono); font-size: var(--text-fine); color: var(--content-muted); margin-bottom: var(--gap-peer); }
7273 .tag-crumbs li + li::before { content: "/"; opacity: 0.4; margin: 0 var(--gap-bound); }
7274 .tag-crumbs a { color: var(--content); text-decoration: none; }
7275 .tag-crumbs a:hover { text-decoration: underline; }
7276 .tag-drill-select { flex: 1; display: flex; align-items: center; gap: var(--gap-peer); cursor: pointer; padding: var(--gap-bound) 0; }
7277 .tag-drill-select input { width: auto; margin: 0; }
7278 .tag-drill-select span:first-of-type { flex: 1; }
7279 .tag-drill-into { display: flex; align-items: center; gap: var(--gap-peer); flex: 1; text-decoration: none; color: inherit; padding: var(--gap-bound) 0; }
7280 .tag-drill-into span:first-child { flex: 1; }
7281 .tag-drill-chevron { opacity: 0.4; }
7282
7283 /* ---- Discover sidebar: refine block -------------------------------------
7284 Subordinate to the spine: same controls, quieter. */
7285 .filter-refine { margin-top: var(--gap-section); }
7286 .filter-fieldset { border: none; padding: 0; margin: 0; min-width: 0; }
7287 .filter-check { flex: 1; display: flex; align-items: center; gap: var(--gap-peer); cursor: pointer; padding: var(--gap-bound) 0; }
7288 .filter-check input { width: auto; margin: 0; }
7289 .filter-check span:first-of-type { flex: 1; }
7290
7291 /* A zero-count facet stays visible but inert, so the shape of the catalog
7292 stays legible as filters narrow. No existing rule covers a disabled input
7293 inside a label, so this is the checkbox counterpart of the button rule.
7294 `.is-empty` covers the drill rungs, which are links and carry no input at
7295 all; they were rendering identically to rungs with items. */
7296 .filter-item:has(input:disabled), .filter-item.is-empty { opacity: 0.5; }
7297 .filter-item:has(input:disabled) .filter-check { cursor: not-allowed; }
7298
7299 /* Price buckets are links carrying a range, not form state. */
7300 .price-buckets { margin-top: var(--gap-peer); }
7301 .price-bucket { flex: 1; display: flex; align-items: center; justify-content: space-between; gap: var(--gap-peer); text-decoration: none; color: inherit; padding: var(--gap-bound) 0; }
7302
7303 .mode-toggle { display: flex; gap: var(--gap-peer); margin-bottom: var(--gap-section); }
7304 .toggle-btn { background: var(--surface-sunken); border: none; padding: var(--gap-peer) var(--gap-section); font-size: var(--text-note); font-family: inherit; cursor: pointer; transition: background 0.1s ease, opacity 0.1s ease; }
7305 .toggle-btn:hover { opacity: 0.7; }
7306 .toggle-btn.is-selected { background: var(--primary-dark); color: var(--primary-light); }
7307 .table-controls { background: var(--surface-sunken); display: flex; align-items: center; gap: var(--gap-section); padding: var(--gap-peer) var(--gap-section); }
7308 .table-meta { font-size: var(--text-fine); opacity: 0.6; white-space: nowrap; flex-shrink: 0; }
7309 .sort-select { background: var(--surface-page); border: none; padding: var(--gap-bound) var(--gap-peer); font-size: var(--text-fine); font-family: inherit; cursor: pointer; width: auto; flex-shrink: 0; }
7310 .table-header { display: grid; grid-template-columns: 50px 1fr 100px 70px 70px; gap: var(--gap-peer); padding: var(--gap-peer) var(--gap-section); background: var(--surface-overlay); font-size: var(--text-fine); opacity: 0.7; text-transform: uppercase; letter-spacing: 0.03em; }
7311 .table-header.projects-header { grid-template-columns: 1fr 100px 80px 70px; }
7312 /* Column headers describe the list layout only; hidden when the grid view is active. */
7313 .table-header.is-hidden { display: none; }
7314 .col-right { text-align: right; }
7315 .results-table { border: 1px solid var(--border); border-top: none; }
7316 .table-row { /* respec-ok: a five-column grid template. The generated `.table-row` sets a display
7317 but has no vocabulary for column tracks, so the tracks and the display that carries
7318 them are stated together here. */ display: grid; grid-template-columns: 50px 1fr 100px 70px 70px; gap: var(--gap-peer); padding: var(--gap-peer) var(--gap-section); align-items: center; text-decoration: none; color: var(--content); font-size: var(--text-note); border-bottom: 1px solid var(--border); transition: background 0.1s ease; }
7319 .table-row-item { grid-template-columns: 1fr 30px; padding: 0; gap: 0; }
7320 .table-row-item .table-row-link { display: grid; grid-template-columns: 50px 1fr 100px 70px 70px; gap: var(--gap-peer); padding: var(--gap-peer) var(--gap-section); align-items: center; text-decoration: none; color: inherit; flex: 1; }
7321 .table-row-item .row-save { display: flex; align-items: center; justify-content: center; }
7322 .table-row.project-row { grid-template-columns: 1fr 100px 80px 70px; }
7323 .table-row:last-child { border-bottom: none; }
7324 .table-row:hover { background: var(--surface-sunken); text-decoration: none; }
7325 .row-type { font-size: var(--text-fine); background: var(--surface-sunken); padding: var(--gap-bound) var(--gap-peer); text-align: center; opacity: 0.7; }
7326 .row-info { min-width: 0; display: flex; flex-direction: column; gap: var(--gap-bound); }
7327 .row-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
7328 .row-creator { font-size: var(--text-fine); opacity: 0.5; }.row-category { font-size: var(--text-fine); opacity: 0.6; }
7329 .row-price { text-align: right; }
7330 .row-items { text-align: right; opacity: 0.6; font-size: var(--text-fine); }
7331 .row-date { text-align: right; opacity: 0.5; font-size: var(--text-fine); }.pagination { display: flex; gap: var(--gap-bound); }
7332 .pagination button { background: var(--surface-page); border: none; padding: var(--gap-bound) var(--gap-group); font-size: var(--text-fine); cursor: pointer; }
7333 .pagination button:disabled { opacity: 0.3; cursor: not-allowed; }
7334 .pagination button.is-selected { background: var(--primary-dark); color: var(--primary-light); }
7335
7336 /* Issue tabs (open / closed) inside the git browser */
7337 .issue-tabs { display: flex; gap: var(--gap-peer); margin-bottom: var(--gap-section); }
7338 .issue-tab {
7339 padding: var(--gap-peer) var(--gap-section);
7340 border: 1px solid var(--border);
7341 background: var(--surface-overlay);
7342 font-family: var(--font-mono);
7343 font-size: var(--text-note);
7344 color: var(--content);
7345 text-decoration: none;
7346 opacity: 0.7;
7347 }
7348 .issue-tab:hover { opacity: 1; }
7349 .issue-tab.is-selected {
7350 opacity: 1;
7351 background: var(--highlight-faint);
7352 border-color: var(--focus-ring);
7353 }
7354 .page-info { opacity: 0.6; }
7355 .padded-page .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--gap-section); padding-bottom: var(--gap-section); border-bottom: 1px solid var(--border); }
7356 .padded-page .page-header h1 { font-size: var(--text-head); text-align: center; position: absolute; left: 50%; transform: translateX(-50%); }
7357 .padded-page .page-header nav { margin: 0; }
7358 .padded-page .page-header .nav-links { gap: var(--gap-pane); }
7359 .discover-filter-toggle {
7360 display: none;
7361 font-family: var(--font-mono);
7362 font-size: var(--text-note);
7363 padding: var(--gap-peer) var(--gap-section);
7364 background: var(--surface-overlay);
7365 border: 1px solid var(--border);
7366 cursor: pointer;
7367 margin-bottom: var(--gap-section);
7368 }
7369
7370 .discover-filter-toggle.active { background: var(--surface-sunken); border-color: var(--border-strong); }
7371
7372 .discover-filter-toggle .filter-count {
7373 background: var(--action);
7374 color: var(--primary-light);
7375 font-size: var(--text-fine);
7376 padding: var(--gap-bound) var(--gap-peer);
7377 border-radius: var(--radius-control);
7378 margin-left: var(--gap-bound);
7379 }
7380
7381 @media (max-width: 839px) {
7382 .discover-filter-toggle { display: inline-block; }
7383 .discover-layout { grid-template-columns: 1fr; }
7384 .discover-sidebar { display: none; }
7385 /* Open state lives on the layout, not the sidebar: the sidebar is replaced
7386 by an out-of-band swap on every filter change and would lose the class. */
7387 .discover-layout.filters-open .discover-sidebar { display: block; margin-bottom: var(--gap-section); }
7388 .table-header, .table-row { grid-template-columns: 40px 1fr 60px; }
7389 .table-row-item { grid-template-columns: 1fr 30px; }
7390 .table-row-item .table-row-link { grid-template-columns: 40px 1fr 60px; }
7391 .table-header.projects-header, .table-row.project-row { grid-template-columns: 1fr 70px; }
7392 .row-date, .row-items, .row-category { display: none; }
7393 .table-header span:nth-child(3), .table-header span:nth-child(4), .table-header span:nth-child(5) { display: none; }
7394 .table-header.projects-header span:nth-child(3), .table-header.projects-header span:nth-child(4) { display: none; }
7395 }
7396
7397 @media (max-width: 599px) {
7398 .table-header, .table-row { grid-template-columns: 1fr 60px; }
7399 .table-row-item { grid-template-columns: 1fr 30px; }
7400 .table-row-item .table-row-link { grid-template-columns: 1fr 60px; }
7401 .row-type { display: none; }
7402 .table-header span:first-child { display: none; }
7403 .table-header.projects-header, .table-row.project-row { grid-template-columns: 1fr; }
7404 .table-header.projects-header span:nth-child(2) { display: none; }
7405 .row-price { font-size: var(--text-fine); }
7406 .table-controls { flex-wrap: wrap; }
7407 .mode-toggle { flex-wrap: wrap; }
7408 .table-footer { flex-direction: column; gap: var(--gap-peer); align-items: flex-start; }
7409 }
7410
7411 /* ── Feature Cards & Suggestion Input (shared by wizard + settings) ── */
7412
7413 .type-grid {
7414 display: grid;
7415 grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
7416 gap: var(--gap-section);
7417 margin-bottom: var(--gap-section);
7418 }
7419
7420 /* Canonical selectable-card primitive. One recipe; per-context size modifiers.
7421 Used by wizard type pickers, pricing-model pickers, content-choice pickers,
7422 monetization-mode pickers, anywhere a radio-like choice is rendered as a card. */
7423 .card--selectable {
7424 cursor: pointer;
7425 display: block;
7426 }
7427 .card--selectable input[type="radio"],
7428 .card--selectable input[type="checkbox"] {
7429 position: absolute;
7430 opacity: 0;
7431 pointer-events: none;
7432 }
7433
7434 .card--selectable-inner {
7435 display: flex;
7436 flex-direction: column;
7437 align-items: center;
7438 justify-content: center;
7439 padding: var(--gap-section);
7440 border: 2px solid var(--border);
7441 border-radius: var(--radius-square);
7442 background: var(--surface-page);
7443 text-align: center;
7444 transition: border-color 0.15s, background 0.15s;
7445 min-height: 60px;
7446 }
7447 /* Variant: column-aligned content (text-heavy pricing cards). */
7448 .card--selectable.is-text-heavy .card--selectable-inner {
7449 align-items: stretch;
7450 text-align: left;
7451 padding: var(--gap-section);
7452 }
7453
7454 .card--selectable input:checked + .card--selectable-inner,
7455 .card--selectable.is-selected .card--selectable-inner {
7456 border-color: var(--action);
7457 background: var(--highlight-faint);
7458 }
7459
7460 .card--selectable:hover .card--selectable-inner {
7461 background: var(--surface-sunken);
7462 }
7463
7464 /* Focus. The card is a <label> wrapping a visually hidden radio, so the focus
7465 lands on the input and the ring has to be drawn on the inner surface. The
7466 <a> form of the card takes focus itself. */
7467 .card--selectable:focus-visible,
7468 .card--selectable input:focus-visible + .card--selectable-inner {
7469 outline: 2px solid var(--focus-ring);
7470 outline-offset: 2px;
7471 }
7472
7473 .card--selectable-label {
7474 font-weight: bold;
7475 font-size: var(--text-note);
7476 }
7477
7478 .card--selectable-desc {
7479 font-size: var(--text-fine);
7480 color: var(--content-muted);
7481 margin-top: var(--gap-bound);
7482 }
7483
7484 /* Disabled selectable card, visually dimmed, ignores pointer events. */
7485 .card--selectable.is-disabled {
7486 opacity: 0.7;
7487 pointer-events: none;
7488 }
7489
7490 .suggestion-input {
7491 position: relative;
7492 }
7493
7494 .suggestion-dropdown {
7495 display: none;
7496 position: absolute;
7497 top: 100%;
7498 left: 0;
7499 right: 0;
7500 background: var(--surface-page);
7501 border: 1px solid var(--border);
7502 border-top: none;
7503 border-radius: 0 0 var(--radius-square) var(--radius-square);
7504 max-height: 200px;
7505 overflow-y: auto;
7506 z-index: var(--z-dropdown);
7507 box-shadow: var(--elevation-overlay);
7508 }
7509
7510 .suggestion-dropdown.open {
7511 display: block;
7512 }
7513
7514 .suggestion-item {
7515 padding: var(--gap-peer) var(--gap-section);
7516 cursor: pointer;
7517 font-size: var(--text-note);
7518 }
7519
7520 .suggestion-item:hover {
7521 background: var(--surface-overlay);
7522 }
7523
7524 .suggestion-create {
7525 font-style: italic;
7526 opacity: 0.8;
7527 border-top: 1px solid var(--border);
7528 }
7529
7530 /* The tip offer is described (`src/quasi/tip.rs`), so its button, its
7531 disclosure, its two fields and its submit are the design system's. What was
7532 here restated them, and a restated rule fails the drift check. */
7533
7534 /* ===========================================
7535 COLLECTION PICKER (shared dropdown)
7536 =========================================== */
7537
7538 .collection-picker-anchor { position: relative; }
7539 .collection-picker {
7540 position: absolute; left: 0; right: 0; top: 100%; z-index: var(--z-picker);
7541 background: var(--surface-page); border: 1px solid var(--border);
7542 box-shadow: var(--elevation-overlay); min-width: 220px;
7543 }
7544 .collection-picker-list { max-height: 200px; overflow-y: auto; padding: var(--gap-bound) 0; }
7545 .collection-picker-item {
7546 display: flex; align-items: center; gap: var(--gap-peer);
7547 padding: var(--gap-peer) var(--gap-section); cursor: pointer; font-size: var(--text-note);
7548 }
7549 .collection-picker-item:hover { background: var(--surface-sunken); }
7550 .collection-picker-loading,
7551 .collection-picker-create { border-top: 1px solid var(--border); padding: var(--gap-peer) var(--gap-section); }
7552 .collection-picker-create form { display: flex; gap: var(--gap-peer); }
7553 .collection-picker-create input { flex: 1; padding: var(--gap-bound) var(--gap-peer); font-size: var(--text-note); min-width: 0; }
7554 .collection-picker-create button { font-size: var(--text-note); white-space: nowrap; }
7555
7556 /* Save button on discover cards */
7557 .row-save, .grid-card-save {
7558 background: none; border: none; cursor: pointer;
7559 font-size: var(--text-note); padding: var(--gap-bound) var(--gap-peer); opacity: 0.4;
7560 color: var(--content); transition: opacity 0.15s;
7561 }
7562 .row-save:hover, .grid-card-save:hover { opacity: 1; }
7563 .grid-card-save {
7564 position: absolute; top: 0.4rem; right: 0.4rem; z-index: var(--z-raised);
7565 background: var(--surface-page); border-radius: var(--radius-control); padding: var(--gap-bound) var(--gap-peer);
7566 box-shadow: var(--elevation-overlay); opacity: 0;
7567 }
7568 .grid-card:hover .grid-card-save { opacity: 0.7; }
7569 .grid-card-save:hover { opacity: 1; }
7570
7571 /* Item page save button saved state */
7572 button.saved { border-color: var(--action); color: var(--action); }
7573
7574 /* ===========================================
7575 SEARCH BOX
7576 ===========================================
7577 The box and its suggestion list are a described Field (N8), which draws its
7578 own control and its own list. What is left here is where the toolbar puts
7579 it: a flex child that takes the rest of the row, and the positioning context
7580 the list is absolutely placed against. */
7581
7582 .search-wrapper { position: relative; flex: 1; min-width: 0; }
7583
7584 /* ===========================================
7585 DOC UI EXAMPLES (embedded live UI in docs)
7586 =========================================== */
7587
7588 .doc-ui {
7589 margin: var(--gap-pane) 0; border: 1px solid var(--border); border-radius: var(--radius-control); overflow: hidden;
7590 }
7591 .doc-ui-frame {
7592 padding: var(--gap-section); background: var(--surface-overlay); pointer-events: none; user-select: none;
7593 }
7594 .doc-ui figcaption {
7595 padding: var(--gap-peer) var(--gap-section); font-size: var(--text-fine); opacity: 0.6;
7596 border-top: 1px solid var(--border); background: var(--surface-overlay);
7597 font-family: var(--font-mono);
7598 }
7599 .doc-ui-missing {
7600 padding: var(--gap-section); opacity: 0.4; font-style: italic;
7601 }
7602
7603 /* Library "New" badge for items with undownloaded versions. Not a status, so
7604 it takes no tone; it is the plain badge with the accent hue and a little
7605 more weight, since "New" is the one badge meant to catch the eye. */
7606 .badge-new {
7607 color: var(--action); font-weight: bold;
7608 margin-left: var(--gap-peer); vertical-align: middle;
7609 }
7610
7611 /* ===========================================
7612 BENEFIT LIST (bulleted feature lists, e.g. Stripe Connect prompt)
7613 =========================================== */
7614 .benefit-list {
7615 list-style: none;
7616 padding: 0;
7617 margin: 0 auto var(--gap-pane);
7618 max-width: 280px;
7619 text-align: left;
7620 display: flex;
7621 flex-direction: column;
7622 gap: var(--gap-peer);
7623 }
7624 .benefit-list li {
7625 position: relative;
7626 padding-left: var(--gap-section);
7627 font-size: var(--text-note);
7628 }
7629 .benefit-list li::before {
7630 content: "";
7631 position: absolute;
7632 left: 0.15rem;
7633 top: 0.55rem;
7634 width: 6px;
7635 height: 6px;
7636 border-radius: var(--radius-round);
7637 background: var(--success);
7638 }
7639
7640 /* ===========================================
7641 CHECKLIST (setup / onboarding step lists)
7642 =========================================== */
7643 .checklist {
7644 margin-bottom: var(--gap-pane);
7645 padding: var(--gap-section) var(--gap-pane);
7646 background: var(--surface-overlay);
7647 border: 1px solid var(--border);
7648 }
7649 .checklist-header {
7650 display: flex;
7651 justify-content: space-between;
7652 align-items: center;
7653 margin-bottom: var(--gap-section);
7654 }
7655 .checklist-title {
7656 margin: 0;
7657 font-family: var(--font-display);
7658 font-weight: bold;
7659 }
7660 .checklist-mark {
7661 width: 1.1rem;
7662 height: 1.1rem;
7663 border-radius: var(--radius-round);
7664 border: 2px solid var(--border);
7665 flex-shrink: 0;
7666 box-sizing: border-box;
7667 }
7668 .checklist-mark--done {
7669 border-color: var(--success);
7670 background: var(--success);
7671 }
7672 .checklist-label {
7673 flex: 1;
7674 font-size: var(--text-note);
7675 }
7676 .checklist-label--done {
7677 opacity: 0.5;
7678 text-decoration: line-through;
7679 }
7680
7681 /* ===========================================
7682 PAYMENTS TAB (replaces inline styles in user_payments.html)
7683 =========================================== */
7684 /* Canonical "stack row", title-on-left + actions-on-right header bar.
7685 One primitive; per-context modifiers handle gap, wrap, alignment, margins.
7686 Aliases below cover existing per-tab class names (kept as JS hooks). */
7687 .stack-row,
7688 .content-header,
7689 .user-media-header,
7690 .transactions-header,
7691 .analytics-range-bar,
7692 .cart-multi-bar {
7693 display: flex;
7694 justify-content: space-between;
7695 align-items: center;
7696 flex-wrap: wrap;
7697 gap: var(--gap-section);
7698 }
7699 .stack-row--tight { gap: var(--gap-section); flex-wrap: nowrap; }
7700 .stack-row--top { align-items: flex-start; }
7701 .stack-row--bordered,
7702 .transactions-header {
7703 margin-bottom: var(--gap-section);
7704 padding-bottom: var(--gap-section);
7705 border-bottom: 1px solid var(--border);
7706 }
7707 .content-header,
7708 .user-media-header,
7709 .analytics-range-bar { margin-bottom: var(--gap-section); }
7710 .cart-multi-bar {
7711 margin-bottom: var(--gap-pane);
7712 padding: var(--gap-section);
7713 background: var(--surface-sunken);
7714 border: 1px solid var(--border);
7715 }
7716
7717 /* Canonical list row (flex, gap, padding, bottom border). Aliases below cover
7718 the per-tab variants. They exist as JS hooks but share the same look. */
7719 .list-row,
7720 .psection-row,
7721 .section-mgmt-row,
7722 .bundle-picker-row,
7723 .checklist-row {
7724 display: flex;
7725 align-items: center;
7726 gap: var(--gap-section);
7727 padding: var(--gap-group) 0;
7728 border-bottom: 1px solid var(--border);
7729 }
7730 .list-row:last-child,
7731 .psection-row:last-child,
7732 .section-mgmt-row:last-child,
7733 .bundle-picker-row:last-child,
7734 .checklist-row:last-child { border-bottom: none; }
7735 .warn-glyph {
7736 font-size: var(--text-subhead);
7737 color: var(--warning);
7738 }
7739
7740 .card-h {
7741 margin: 0 0 var(--gap-section) 0;
7742 font-size: var(--text-body);
7743 }
7744
7745 .amount-big {
7746 font-size: var(--text-subhead);
7747 font-weight: bold;
7748 }
7749
7750 .account-details {
7751 margin-top: var(--gap-section);
7752 padding-top: var(--gap-section);
7753 border-top: 1px solid var(--border);
7754 }
7755 .account-details summary { cursor: pointer; }
7756 .account-details-id {
7757 font-size: var(--text-fine);
7758 opacity: 0.5;
7759 font-family: var(--font-mono);
7760 margin-top: var(--gap-bound);
7761 }
7762
7763 .stripe-actions {
7764 display: flex;
7765 gap: var(--gap-section);
7766 flex-wrap: wrap;
7767 }
7768 .stripe-actions-disconnect { margin-left: auto; }
7769
7770 .fee-breakdown {
7771 display: grid;
7772 grid-template-columns: 1fr 1fr;
7773 gap: var(--gap-peer);
7774 font-size: var(--text-note);
7775 }
7776 .fee-row {
7777 display: flex;
7778 justify-content: space-between;
7779 padding: var(--gap-peer) 0;
7780 border-bottom: 1px solid var(--border);
7781 }
7782
7783 .checkbox-row {
7784 display: flex;
7785 align-items: flex-start;
7786 gap: var(--gap-peer);
7787 cursor: pointer;
7788 font-size: var(--text-note);
7789 }
7790 .check-inline {
7791 width: auto;
7792 margin-top: var(--gap-bound);
7793 }
7794
7795 .stripe-connect-prompt { text-align: center; padding: var(--gap-pane); }
7796 .stripe-logo { opacity: 0.8; }
7797 .stripe-connect-btn {
7798 background: var(--stripe);
7799 padding: var(--gap-section) var(--gap-page);
7800 }
7801
7802 .export-row {
7803 display: flex;
7804 justify-content: flex-end;
7805 margin-bottom: var(--gap-section);
7806 }
7807
7808 .splits-grid {
7809 display: grid;
7810 grid-template-columns: 1fr 1fr;
7811 gap: var(--gap-section);
7812 margin-bottom: var(--gap-section);
7813 }
7814
7815 .tip-message {
7816 max-width: 300px;
7817 overflow: hidden;
7818 text-overflow: ellipsis;
7819 }
7820
7821 .transactions-title { font-size: var(--text-subhead); }
7822
7823 /* ===========================================
7824 CREATOR TAB (replaces inline styles in user_creator.html)
7825 =========================================== */
7826 .creator-tab-section {
7827 padding: var(--gap-page);
7828 }
7829 .creator-tab-section--centered { text-align: center; }
7830 .creator-tab-section--no-top { padding-top: 0; }
7831
7832 .creator-h2 { font-size: var(--text-subhead); margin-bottom: var(--gap-section); }
7833 .creator-h2--sm { font-size: var(--text-subhead); }
7834
7835 .creator-section-label {
7836 margin-left: var(--gap-page);
7837 margin-right: var(--gap-page);
7838 }
7839 .creator-form-section { padding: 0 var(--gap-page); }
7840
7841 .creator-plan-box {
7842 background: var(--surface-overlay);
7843 padding: var(--gap-section) var(--gap-pane);
7844 }
7845 .creator-plan-head {
7846 display: flex;
7847 align-items: center;
7848 gap: var(--gap-section);
7849 margin-bottom: var(--gap-peer);
7850 flex-wrap: wrap;
7851 }
7852 .creator-plan-name {
7853 font-family: var(--font-display);
7854 font-weight: bold;
7855 font-size: var(--text-lead);
7856 }
7857
7858 /* Founder pricing, two states, and neither is a status tone: locked-for-life
7859 is emphasis and pending-lock-in is an aside. They were a --content fill and
7860 a --surface-sunken fill back when every badge was filled; as coloured text
7861 the same distinction is full-strength ink against muted. */
7862 .badge--founder-locked {
7863 color: var(--content);
7864 font-weight: bold;
7865 }
7866 .badge--founder-pending {
7867 color: var(--content-muted);
7868 }
7869
7870 .founder-callout {
7871 background: var(--surface-overlay);
7872 border-left: 3px solid var(--content);
7873 padding: var(--gap-section);
7874 }
7875
7876 .creator-tier-grid {
7877 display: grid;
7878 grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
7879 gap: var(--gap-section);
7880 }
7881 .creator-tier-card {
7882 background: var(--surface-overlay);
7883 padding: var(--gap-section);
7884 text-align: center;
7885 }
7886 .creator-tier-name {
7887 font-family: var(--font-display);
7888 font-weight: bold;
7889 font-size: var(--text-body);
7890 margin-bottom: var(--gap-bound);
7891 }
7892 .creator-tier-link {
7893 color: inherit;
7894 text-decoration: none;
7895 border-bottom: 1px solid var(--border);
7896 }
7897 .creator-tier-sub {
7898 font-size: var(--text-fine);
7899 opacity: 0.7;
7900 }
7901 .creator-tier-storage { font-size: var(--text-fine); }
7902 .creator-tier-buttons {
7903 display: flex;
7904 flex-direction: column;
7905 gap: var(--gap-peer);
7906 }
7907 .creator-tier-btn {
7908 font-size: var(--text-note);
7909 width: 100%;
7910 }
7911
7912 .strike-dim {
7913 opacity: 0.5;
7914 text-decoration: line-through;
7915 }
7916
7917 .storage-box {
7918 background: var(--surface-overlay);
7919 padding: var(--gap-section) var(--gap-pane);
7920 }
7921 .storage-row {
7922 display: flex;
7923 justify-content: space-between;
7924 margin-bottom: var(--gap-peer);
7925 font-size: var(--text-note);
7926 }
7927 .storage-breakdown {
7928 display: grid;
7929 grid-template-columns: 1fr 1fr;
7930 gap: var(--gap-bound) var(--gap-pane);
7931 margin-top: var(--gap-section);
7932 font-size: var(--text-note);
7933 }
7934
7935 .broadcast-head {
7936 display: flex;
7937 justify-content: space-between;
7938 align-items: flex-start;
7939 }
7940 .broadcast-desc { flex: 1; }
7941 .broadcast-export { margin-left: var(--gap-section); }
7942
7943 .creator-divider {
7944 margin: 0 var(--gap-page);
7945 border-color: var(--border);
7946 }
7947
7948 .creator-intro { line-height: 1.6; }
7949 .creator-pitch-box {
7950 background: var(--surface-overlay);
7951 padding: var(--gap-pane);
7952 }
7953
7954 .mono-sm {
7955 font-family: var(--font-mono);
7956 font-size: var(--text-note);
7957 }
7958
7959 /* ===========================================
7960 PROJECT CONTENT TAB (replaces inline styles in project_content.html)
7961 =========================================== */
7962
7963 .empty-state--lg { padding: var(--gap-page) var(--gap-section); }
7964
7965 .content-filters {
7966 display: flex;
7967 gap: var(--gap-section);
7968 margin-bottom: var(--gap-section);
7969 align-items: center;
7970 flex-wrap: wrap;
7971 }
7972 .content-filter-search {
7973 flex: 1;
7974 min-width: 180px;
7975 padding: var(--gap-peer) var(--gap-group);
7976 font-size: var(--text-note);
7977 }
7978 .content-filter-select {
7979 padding: var(--gap-peer);
7980 font-size: var(--text-note);
7981 }
7982
7983 .bulk-action-bar {
7984 padding: var(--gap-section);
7985 margin-bottom: var(--gap-section);
7986 background: var(--surface-raised);
7987 border: 1px solid var(--border);
7988 border-radius: var(--radius-square);
7989 display: flex;
7990 align-items: center;
7991 gap: var(--gap-peer);
7992 flex-wrap: wrap;
7993 opacity: 0.4;
7994 }
7995 .bulk-action-bar--active { opacity: 1; }
7996 .bulk-count { font-weight: bold; min-width: 5rem; }
7997 .bulk-deselect { margin-left: auto; }
7998 .danger-text { color: var(--danger); }
7999
8000 .bulk-form {
8001 margin-bottom: var(--gap-section);
8002 padding: var(--gap-section);
8003 background: var(--surface-sunken);
8004 border: 1px solid var(--border);
8005 border-radius: var(--radius-square);
8006 }
8007 .bulk-form-row {
8008 display: flex;
8009 gap: var(--gap-section);
8010 align-items: end;
8011 }
8012 .bulk-form-label {
8013 font-size: var(--text-note);
8014 display: block;
8015 margin-bottom: var(--gap-bound);
8016 }
8017
8018 /* Sortable table column header
8019
8020 The cursor and the caret are NOT here. `.table-heading[data-sortable]` and
8021 `.table-heading[aria-sort="..."]::after` are generated into static/layout.css
8022 by makeover-webview out of `Column::sortable` and `Column::sorted`. This file
8023 carried two hand-written copies of the same fact -- `.sortable-th` here and
8024 `.data-table th.sortable` up at the data-table block -- and a third channel in
8025 tab-project-content.js, which wrote the glyph as text and toggled
8026 `.sort-arrow--active`. Three vocabularies, one state, and the generated pair
8027 matched nothing because no heading carried `.table-heading`.
8028
8029 The focus ring stays: it is this app's focus treatment, not a restatement of
8030 the caret, and makeover emits no focus rule for a heading. */
8031 .table-heading[data-sortable] {
8032 user-select: none;
8033 transition: background 0.2s ease;
8034 }
8035
8036 .table-heading[data-sortable]:hover {
8037 background: var(--border);
8038 }
8039
8040 .table-heading[data-sortable]:focus-visible {
8041 outline: 2px solid var(--focus-ring);
8042 outline-offset: 2px;
8043 }
8044
8045 /* Data-table column-width utilities (numeric width hints,
8046 kept as classes to avoid inline `style="width: NN%"`) */
8047 .col-3 { width: 3%; }
8048 .col-5 { width: 5%; }
8049 .col-8 { width: 8%; }
8050 .col-10 { width: 10%; }
8051 .col-12 { width: 12%; }
8052 .col-15 { width: 15%; }
8053 .col-20 { width: 20%; }
8054 .col-32 { width: 32%; }
8055 .col-45 { width: 45%; }
8056
8057 /* Pixel-width utilities for inputs and narrow fixed-width controls.
8058 Use on form inputs that can't be sized by their parent column. */
8059 .w-80 { width: 80px; }
8060 .w-100 { width: 100px; }
8061 .w-120 { width: 120px; }
8062 .w-180 { width: 180px; }
8063 .w-220 { width: 220px; }
8064 .w-260 { width: 260px; }
8065 .w-full { width: 100%; }
8066 .maxw-320 { max-width: 320px; }
8067
8068 /* Canonical input shape modifiers. One base form-input look (defined globally
8069 at line ~365 input[type], textarea, select) plus these size + feature variants.
8070 Apply alongside the per-tab class which acts as a JS hook. */
8071 .input--xs {
8072 padding: var(--gap-bound) var(--gap-peer);
8073 font-size: var(--text-note);
8074 }
8075 .input--sm {
8076 padding: var(--gap-bound) var(--gap-peer);
8077 font-size: var(--text-note);
8078 }
8079 .input--mono { font-family: var(--font-mono); }
8080 .input--upper { text-transform: uppercase; }
8081 .input--numeric { text-align: right; }
8082
8083 .order-arrow-btn {
8084 padding: var(--gap-bound);
8085 font-size: var(--text-fine);
8086 line-height: 1;
8087 }
8088
8089 .bundle-toggle {
8090 background: none;
8091 border: none;
8092 cursor: pointer;
8093 padding: 0 var(--gap-bound) 0 0;
8094 font-size: var(--text-fine);
8095 color: var(--content-muted);
8096 }
8097 .bundle-child-cell { padding-left: var(--gap-page); }
8098 .bundle-child-title { opacity: 0.85; }
8099 .bundle-child-arrow {
8100 color: var(--content-muted);
8101 margin-right: var(--gap-bound);
8102 }
8103
8104 .item-actions {
8105 display: flex;
8106 gap: var(--gap-peer);
8107 }
8108
8109 .badge--inline-tiny {
8110 font-size: var(--text-fine);
8111 margin-left: var(--gap-bound);
8112 opacity: 0.7;
8113 }
8114
8115 .deleted-items-details { margin-top: var(--gap-page); }
8116 .deleted-items-summary {
8117 cursor: pointer;
8118 font-size: var(--text-body);
8119 opacity: 0.7;
8120 }
8121
8122 .section-divider {
8123 margin: var(--gap-page) 0;
8124 border: none;
8125 border-top: 1px solid var(--border);
8126 }
8127
8128 .inline-rename-input {
8129 font-weight: bold;
8130 width: 100%;
8131 padding: var(--gap-bound) var(--gap-peer);
8132 font-size: inherit;
8133 }
8134
8135 /* ===========================================
8136 ITEM DETAILS TAB (replaces inline styles in item_details.html)
8137 =========================================== */
8138
8139 /* Compact button utility (small icon-side actions) */
8140 .btn-compact { padding: var(--gap-peer) var(--gap-section); }
8141
8142 .ml-2 { margin-left: var(--gap-peer); }
8143 .cursor-pointer { cursor: pointer; }
8144 .inline { display: inline; }
8145
8146 .tag-search-wrap { position: relative; }
8147 .tag-suggestions {
8148 position: absolute;
8149 z-index: var(--z-dropdown);
8150 background: var(--surface-sunken);
8151 border: 1px solid var(--border);
8152 width: 100%;
8153 max-height: 200px;
8154 overflow-y: auto;
8155 }
8156
8157 .advanced-details { margin-top: var(--gap-pane); }
8158 .advanced-summary {
8159 cursor: pointer;
8160 font-size: var(--text-body);
8161 opacity: 0.8;
8162 }
8163
8164 /* Bundle table inside item-details */
8165 .bundle-table {
8166 width: 100%;
8167 border-collapse: collapse;
8168 }
8169 .bundle-table-head {
8170 text-align: left;
8171 font-size: var(--text-fine);
8172 opacity: 0.7;
8173 text-transform: uppercase;
8174 letter-spacing: 0.03em;
8175 }
8176 .bundle-row { border-bottom: 1px solid var(--border); }
8177 .bundle-cell { padding: var(--gap-peer); }
8178 .bundle-cell--first { padding-left: 0; }
8179 .bundle-cell--actions { width: 60px; }
8180 .bundle-cell--desc { font-size: var(--text-note); opacity: 0.8; }
8181 .bundle-cell--file { font-size: var(--text-note); }
8182
8183 .bundle-remove-btn {
8184 padding: var(--gap-bound) var(--gap-peer);
8185 font-size: var(--text-fine);
8186 }
8187
8188 .bundle-add-row {
8189 margin-top: var(--gap-section);
8190 display: flex;
8191 gap: var(--gap-section);
8192 align-items: center;
8193 }
8194
8195 .bundle-existing-details { margin-top: var(--gap-section); }
8196 .bundle-existing-summary {
8197 cursor: pointer;
8198 font-size: var(--text-note);
8199 opacity: 0.7;
8200 }
8201 .bundle-existing-row {
8202 margin-top: var(--gap-peer);
8203 display: flex;
8204 gap: var(--gap-peer);
8205 align-items: center;
8206 }
8207 .bundle-existing-select {
8208 flex: 1;
8209 padding: var(--gap-peer);
8210 }
8211
8212 .sections-intro {
8213 font-size: var(--text-note);
8214 opacity: 0.7;
8215 margin: var(--gap-section) 0 var(--gap-section);
8216 }
8217
8218 .section-mgmt-title {
8219 flex: 1;
8220 font-weight: bold;
8221 }
8222 .section-edit-btn,
8223 .section-del-btn {
8224 padding: var(--gap-bound) var(--gap-group);
8225 font-size: var(--text-fine);
8226 }
8227
8228 .section-add-details { margin-top: var(--gap-section); }
8229 .section-add-summary {
8230 cursor: pointer;
8231 font-size: var(--text-body);
8232 }
8233
8234 .section-edit-modal {
8235 margin-top: var(--gap-section);
8236 padding: var(--gap-section);
8237 background: var(--surface-sunken);
8238 }
8239 .section-edit-actions {
8240 display: flex;
8241 gap: var(--gap-peer);
8242 }
8243
8244 .publish-note {
8245 margin: var(--gap-section) 0;
8246 opacity: 0.7;
8247 text-align: left;
8248 }
8249
8250 /* ===========================================
8251 CONTENT WIZARD STEP (replaces inline styles in wizards/steps/item/content.html)
8252 =========================================== */
8253
8254 /* (Wizard upload-status + slim progress moved into canonical .upload-status*
8255 / .progress-bar-container--slim primitives near top of file.) */
8256
8257 /* Bundle items picker, server-rendered rows and JS-appended rows share these. */
8258 .bundle-picker-row-title { flex: 1; }
8259 .bundle-picker-row-type { font-size: var(--text-fine); opacity: 0.6; }
8260 .bundle-picker-row-unlisted {
8261 font-size: var(--text-fine);
8262 display: flex;
8263 align-items: center;
8264 gap: var(--gap-bound);
8265 }
8266
8267 /* Batch upload queue table. */
8268 .batch-queue-table {
8269 width: 100%;
8270 border-collapse: collapse;
8271 }
8272 .batch-queue-table thead tr {
8273 border-bottom: 2px solid var(--border);
8274 text-align: left;
8275 }
8276 .batch-queue-table th,
8277 .batch-queue-table td {
8278 padding: var(--gap-peer);
8279 font-size: var(--text-note);
8280 }
8281 .batch-queue-table th:first-child,
8282 .batch-queue-table td:first-child { padding-left: 0; }
8283 .batch-queue-table th:last-child,
8284 .batch-queue-table td:last-child { padding-right: 0; text-align: right; }
8285 .batch-queue-table tbody tr { border-bottom: 1px solid var(--border); }
8286
8287 .batch-queue-name {
8288 max-width: 150px;
8289 overflow: hidden;
8290 text-overflow: ellipsis;
8291 white-space: nowrap;
8292 }
8293
8294 /* ===========================================
8295 ACCOUNT TAB (replaces inline styles in partials/tabs/user_account.html)
8296 =========================================== */
8297
8298 /* Canonical solid-tint callout (one of these in the codebase).
8299 Distinct from .alert (left-border) and .banner (full-bleed page notice).
8300 Variants:
8301 - .callout--danger (light bg, danger border, moderation, errors)
8302 - .callout--warning (light bg, warning border, DNS, email-not-verified)
8303 - .callout--solid-warning (full saturation warning bg, dark text, partial checkout) */
8304 .callout {
8305 padding: var(--gap-section);
8306 margin-bottom: var(--gap-section);
8307 font-size: var(--text-note);
8308 border: 1px solid;
8309 }
8310 .callout--danger {
8311 background: color-mix(in oklch, var(--danger) 10%, var(--surface-page));
8312 border-color: var(--danger);
8313 }
8314 .callout--warning {
8315 background: color-mix(in oklch, var(--warning) 14%, var(--surface-page));
8316 border-color: var(--warning);
8317 }
8318 .callout--solid-warning {
8319 background: var(--warning);
8320 color: var(--primary-dark);
8321 border-color: transparent;
8322 padding: var(--gap-section);
8323 }
8324 .callout-title { text-transform: capitalize; }
8325 .callout-meta {
8326 opacity: 0.6;
8327 margin-left: var(--gap-peer);
8328 }
8329 .callout-body { margin: var(--gap-peer) 0 0 0; }
8330 .callout-hint {
8331 font-size: var(--text-note);
8332 margin-top: var(--gap-peer);
8333 }
8334
8335 .account-mod-history { margin-top: var(--gap-section); }
8336 .account-mod-history summary {
8337 cursor: pointer;
8338 font-size: var(--text-note);
8339 opacity: 0.7;
8340 }
8341 .account-mod-history-list { margin-top: var(--gap-peer); }
8342 .account-mod-history-item {
8343 padding: var(--gap-peer) 0;
8344 border-bottom: 1px solid var(--border);
8345 font-size: var(--text-note);
8346 }
8347 .account-mod-history-reason {
8348 margin: var(--gap-bound) 0 0 0;
8349 opacity: 0.8;
8350 }
8351
8352 .account-tip-card-title {
8353 font-weight: bold;
8354 margin-bottom: var(--gap-peer);
8355 }
8356 .account-tip-list {
8357 display: flex;
8358 flex-direction: column;
8359 gap: var(--gap-bound);
8360 opacity: 0.8;
8361 }
8362
8363 /* Resend-verification button needs left spacing from preceding strong. */
8364 .account-resend-btn { margin-left: var(--gap-peer); }
8365
8366 /* Cluster of small inline forms (manage-billing / cancel / resume). */
8367 .inline-form { display: inline; }
8368
8369 /* Notification preferences, bare fieldset + monospace legend. */
8370 .pref-fieldset {
8371 border: none;
8372 padding: 0;
8373 margin: 0 0 var(--gap-section) 0;
8374 }
8375 .pref-fieldset--last { margin-bottom: var(--gap-peer); }
8376 .pref-legend {
8377 font-family: var(--font-mono);
8378 font-size: var(--text-note);
8379 font-weight: bold;
8380 margin-bottom: var(--gap-peer);
8381 }
8382
8383 /* The cannot-opt-out list, rendered from email::OperationalKind. Reads as
8384 reference material rather than as more settings: no controls, and the
8385 justification sits under its label rather than beside it. */
8386 .operational-mail {
8387 margin: var(--gap-peer) 0 0 0;
8388 }
8389 .operational-mail dt {
8390 font-weight: bold;
8391 font-size: var(--text-note);
8392 margin-top: var(--gap-peer);
8393 }
8394 .operational-mail dd {
8395 margin: 0;
8396 opacity: 0.8;
8397 font-size: var(--text-note);
8398 }
8399
8400 /* ===========================================
8401 PROJECT CODE TAB (replaces inline styles in partials/tabs/project_code.html)
8402 =========================================== */
8403
8404 .proj-code-docs-link { margin-left: var(--gap-section); }
8405
8406 .proj-code-linked-list { margin-bottom: var(--gap-pane); }
8407 .proj-code-linked-list > label { margin-bottom: var(--gap-peer); display: block; }
8408
8409 .proj-code-repo {
8410 margin-bottom: var(--gap-section);
8411 padding: var(--gap-section);
8412 background: var(--surface-overlay);
8413 }
8414 .proj-code-repo-row {
8415 display: flex;
8416 align-items: center;
8417 gap: var(--gap-section);
8418 }
8419 .proj-code-repo-link { flex: 1; }
8420 .proj-code-repo .danger { padding: var(--gap-bound) var(--gap-section); font-size: var(--text-note); }
8421
8422 .proj-code-collab-block {
8423 margin-top: var(--gap-peer);
8424 padding-top: var(--gap-peer);
8425 border-top: 1px solid var(--border);
8426 }
8427 .proj-code-collab-label { font-size: var(--text-note); margin-bottom: var(--gap-bound); display: block; }
8428 .proj-code-collab-row {
8429 display: flex;
8430 align-items: center;
8431 gap: var(--gap-peer);
8432 margin-bottom: var(--gap-bound);
8433 font-size: var(--text-note);
8434 }
8435 .proj-code-collab-name { flex: 1; }
8436 .proj-code-collab-readonly { opacity: 0.5; }
8437
8438 .proj-code-add-collab-row {
8439 display: flex;
8440 gap: var(--gap-peer);
8441 align-items: center;
8442 }
8443 .proj-code-add-collab-row .collab-username-input {
8444 flex: 1;
8445 font-size: var(--text-note);
8446 padding: var(--gap-bound) var(--gap-peer);
8447 }
8448 .proj-code-add-collab-row .collab-add-btn { white-space: nowrap; }
8449
8450 .proj-code-link-row {
8451 display: flex;
8452 gap: var(--gap-peer);
8453 align-items: center;
8454 }
8455 .proj-code-link-row #link-repo-select { flex: 1; }
8456 .proj-code-link-row #link-repo-btn { white-space: nowrap; }
8457
8458 .proj-code-create-section {
8459 margin-top: var(--gap-pane);
8460 padding-top: var(--gap-section);
8461 border-top: 1px solid var(--border);
8462 }
8463 .proj-code-create-section #create-repo-name { flex: 1; }
8464 .proj-code-create-section #create-repo-btn { white-space: nowrap; }
8465 .proj-code-create-status { margin-top: var(--gap-bound); }
8466 .proj-code-create-status.is-error { color: var(--danger); }
8467
8468 .proj-code-disabled {
8469 padding: var(--gap-page);
8470 text-align: center;
8471 opacity: 0.7;
8472 }
8473
8474 /* ===========================================
8475 ADMIN REPORT ENTRIES (replaces inline styles in partials/admin_report_entries.html)
8476 =========================================== */
8477
8478 .admin-report-table {
8479 width: 100%;
8480 border-collapse: collapse;
8481 font-size: var(--text-note);
8482 }
8483 .admin-report-table thead tr {
8484 border-bottom: 2px solid var(--border);
8485 text-align: left;
8486 }
8487 .admin-report-table tbody tr {
8488 border-bottom: 1px solid var(--border);
8489 }
8490 .admin-report-table th,
8491 .admin-report-table td {
8492 padding: var(--gap-peer);
8493 }
8494 .admin-report-table td.admin-report-reason {
8495 max-width: 200px;
8496 overflow: hidden;
8497 text-overflow: ellipsis;
8498 white-space: nowrap;
8499 }
8500 .admin-report-table td.admin-report-date {
8501 white-space: nowrap;
8502 }
8503
8504 .admin-report-target-type {
8505 opacity: 0.5;
8506 font-size: var(--text-fine);
8507 }
8508
8509 .admin-report-status-open {
8510 color: var(--action);
8511 font-weight: 600;
8512 }
8513 .admin-report-status-closed { opacity: 0.6; }
8514
8515 .admin-report-resolve-form {
8516 display: inline-flex;
8517 gap: var(--gap-bound);
8518 align-items: center;
8519 }
8520 .admin-report-resolve-form input[type="text"] {
8521 width: 120px;
8522 padding: var(--gap-bound) var(--gap-peer);
8523 font-size: var(--text-fine);
8524 }
8525 .admin-report-dismiss-form { display: inline; }
8526
8527 .admin-report-admin-notes {
8528 font-size: var(--text-fine);
8529 opacity: 0.6;
8530 }
8531
8532 /* ===========================================
8533 PROJECT PROMOTIONS TAB (replaces inline styles in partials/tabs/project_promotions.html)
8534 =========================================== */
8535
8536 .proj-promo-form {
8537 margin-bottom: var(--gap-page);
8538 }
8539
8540 .proj-promo-grid {
8541 display: grid;
8542 grid-template-columns: 1fr 1fr;
8543 gap: var(--gap-section) var(--gap-pane);
8544 max-width: 500px;
8545 }
8546
8547 .proj-promo-grid--bordered {
8548 margin-top: var(--gap-section);
8549 padding-top: var(--gap-section);
8550 border-top: 1px solid var(--border);
8551 }
8552
8553 .proj-promo-trial-fields {
8554 display: none;
8555 max-width: 500px;
8556 margin-top: var(--gap-section);
8557 }
8558
8559 .proj-promo-trial-fields.is-visible {
8560 display: block;
8561 }
8562
8563 .proj-promo-grid.is-hidden,
8564 .proj-promo-trial-fields.is-hidden {
8565 display: none;
8566 }
8567
8568 .proj-promo-form .form-group {
8569 margin-bottom: 0;
8570 }
8571
8572 .proj-promo-form .form-group label {
8573 font-size: var(--text-note);
8574 }
8575
8576 .proj-promo-trial-group {
8577 max-width: 240px;
8578 }
8579
8580 .proj-promo-scope {
8581 grid-column: 1 / -1;
8582 }
8583
8584 .proj-promo-actions {
8585 margin-top: var(--gap-section);
8586 }
8587
8588 .proj-promo-hint {
8589 margin-top: var(--gap-peer);
8590 }
8591
8592 /* ===========================================
8593 PROJECT SYNCKIT TAB (replaces inline styles in partials/tabs/project_synckit.html)
8594 =========================================== */
8595
8596 .proj-synckit-intro { margin-bottom: var(--gap-section); }
8597 .proj-synckit-intro-secondary { margin-bottom: var(--gap-pane); }
8598 .proj-synckit-create { margin-bottom: var(--gap-pane); }
8599 .proj-synckit-create-body {
8600 padding: var(--gap-section); background: var(--surface-overlay); border: 1px solid var(--border);
8601 }
8602 .proj-synckit-create-form {
8603 display: flex; flex-direction: row; gap: var(--gap-section); align-items: flex-end;
8604 }
8605 .proj-synckit-label {
8606 display: block; font-size: var(--text-note); margin-bottom: var(--gap-bound);
8607 }
8608 .proj-synckit-code { font-size: var(--text-fine); }
8609 .proj-synckit-code--wrap { font-size: var(--text-fine); word-break: break-all; }
8610 .proj-synckit-code--selectable { font-size: var(--text-note); word-break: break-all; user-select: all; }
8611 .proj-synckit-btn-tight {
8612 margin-left: var(--gap-peer); padding: var(--gap-bound) var(--gap-peer); font-size: var(--text-fine);
8613 }
8614 .proj-synckit-btn-row {
8615 padding: var(--gap-bound) var(--gap-peer); font-size: var(--text-fine);
8616 }
8617 .proj-synckit-btn-row + .proj-synckit-btn-row { margin-left: var(--gap-bound); }
8618 .proj-synckit-hint-inline {
8619 font-size: var(--text-fine); margin-left: var(--gap-bound);
8620 }
8621 .proj-synckit-hint-warn {
8622 font-size: var(--text-fine); color: var(--danger);
8623 }
8624 .proj-synckit-status-active { color: var(--success); }
8625 .proj-synckit-status-inactive { color: var(--danger); }
8626 .proj-synckit-new-key {
8627 padding: var(--gap-section); background: var(--surface-overlay);
8628 border: 1px solid var(--border); margin-top: var(--gap-peer);
8629 }
8630 .proj-synckit-new-key-heading {
8631 margin: 0 0 var(--gap-peer) 0; font-weight: 500; color: var(--danger);
8632 }
8633 .proj-synckit-create-status--error { color: var(--danger); margin-top: var(--gap-peer); }
8634 .proj-synckit-create-status--reset { margin-top: var(--gap-peer); }
8635
8636 /* ===========================================
8637 ITEM PAGE (replaces inline styles in pages/item.html)
8638 =========================================== */
8639
8640 .item-page .breadcrumb {
8641 display: flex;
8642 justify-content: space-between;
8643 align-items: center;
8644 }
8645
8646 .item-page .breadcrumb-edit-link {
8647 font-size: var(--text-note);
8648 color: var(--content);
8649 }
8650
8651 .item-page .video-player {
8652 width: 100%;
8653 max-height: 600px;
8654 background: var(--primary-dark);
8655 }
8656
8657 .item-page .item-cover-img {
8658 width: 100%;
8659 aspect-ratio: 1 / 1;
8660 object-fit: cover;
8661 display: block;
8662 }
8663
8664
8665 .item-page .bundle-notice-inline {
8666 background: var(--surface-sunken);
8667 padding: var(--gap-section);
8668 }
8669
8670 .item-page .bundle-notice-title {
8671 font-weight: bold;
8672 margin-bottom: var(--gap-peer);
8673 }
8674
8675 .item-page .bundle-notice-desc {
8676 opacity: 0.8;
8677 font-size: var(--text-note);
8678 }
8679
8680 .item-page .bundle-notice-entry {
8681 margin-top: var(--gap-section);
8682 }
8683
8684 .item-page .bundle-notice-entry a {
8685 font-weight: bold;
8686 }
8687
8688 .item-page .bundle-notice-price {
8689 opacity: 0.7;
8690 }
8691
8692 .item-page .bundle-notice-empty {
8693 margin-top: var(--gap-peer);
8694 opacity: 0.7;
8695 font-size: var(--text-note);
8696 }
8697
8698 .item-page .promo-details {
8699 margin-bottom: var(--gap-section);
8700 font-size: var(--text-note);
8701 }
8702
8703 .item-page .promo-summary {
8704 cursor: pointer;
8705 opacity: 0.7;
8706 }
8707
8708 .item-page .promo-form {
8709 display: flex;
8710 gap: var(--gap-peer);
8711 align-items: center;
8712 margin-top: var(--gap-peer);
8713 }
8714
8715 .item-page .promo-input {
8716 flex: 1;
8717 padding: var(--gap-peer) var(--gap-group);
8718 font-size: var(--text-note);
8719 }
8720
8721 .item-page .action-row {
8722 margin-top: var(--gap-section);
8723 }
8724
8725 .item-page .action-row-tight {
8726 margin-top: var(--gap-peer);
8727 }
8728
8729 .item-page .full-width-btn {
8730 width: 100%;
8731 font-size: var(--text-note);
8732 }
8733
8734 .item-page .license-preset-name {
8735 margin-bottom: var(--gap-section);
8736 }
8737
8738 .item-page .license-summary {
8739 cursor: pointer;
8740 opacity: 0.7;
8741 font-size: var(--text-note);
8742 }
8743
8744 .item-page .license-text {
8745 white-space: pre-wrap;
8746 font-family: var(--font-sans);
8747 font-size: var(--text-note);
8748 margin-top: var(--gap-section);
8749 padding: var(--gap-section);
8750 background: var(--surface-sunken);
8751 }
8752
8753 .item-page .license-download {
8754 margin-top: var(--gap-section);
8755 }
8756
8757 .item-page .license-download a {
8758 font-size: var(--text-note);
8759 color: var(--content);
8760 }
8761
8762 .item-page .bundle-list-entry {
8763 margin-top: var(--gap-peer);
8764 }
8765
8766 .item-page .footer-links {
8767 margin-top: var(--gap-peer);
8768 }
8769
8770 /* ===========================================
8771 JOIN COMPLETE STEP (replaces inline styles in wizards/steps/join/complete.html)
8772 =========================================== */
8773 .join-complete-intro { margin-bottom: var(--gap-pane); }
8774 .join-complete-choices {
8775 display: grid; grid-template-columns: 1fr 1fr;
8776 gap: var(--gap-section); max-width: 500px;
8777 }
8778 .join-complete-choice { text-decoration: none; color: inherit; display: block; }
8779 .join-complete-card {
8780 background: var(--surface-sunken); padding: var(--gap-section); text-align: center;
8781 border: 2px solid transparent; transition: border-color 0.15s;
8782 }
8783 .join-complete-choice:hover .join-complete-card,
8784 .join-complete-card--active { border-color: var(--content); }
8785 .join-complete-card-title {
8786 font-family: var(--font-display); font-weight: bold;
8787 font-size: var(--text-lead); margin-bottom: var(--gap-peer);
8788 }
8789 .join-complete-card-desc {
8790 font-size: var(--text-note); opacity: 0.7; margin: 0;
8791 }
8792 .join-complete-footnote {
8793 font-size: var(--text-note); opacity: 0.5;
8794 margin-top: var(--gap-section); text-align: center;
8795 }
8796
8797 /* ===========================================
8798 PROMO CODES LIST (replaces inline styles in partials/promo_codes_list.html)
8799 =========================================== */
8800 .promo-codes-row--expired { opacity: 0.5; }
8801 .promo-codes-row--pending { opacity: 0.7; }
8802 .promo-codes-schedule { font-size: var(--text-note); }
8803 .promo-codes-schedule-pending { color: var(--warning); }
8804 .promo-codes-schedule-expired { color: var(--danger); }
8805 .promo-codes-schedule-none { opacity: 0.5; }
8806 .promo-codes-actions { white-space: nowrap; }
8807 .promo-codes-edit-btn { font-size: var(--text-fine); }
8808 .promo-codes-edit-row { background: var(--surface-sunken); }
8809 .promo-codes-edit-cell { padding: var(--gap-section); }
8810 .promo-codes-edit-form {
8811 display: flex; gap: var(--gap-section); align-items: end; flex-wrap: wrap;
8812 }
8813 .promo-codes-edit-label {
8814 font-size: var(--text-fine); display: block; margin-bottom: var(--gap-bound);
8815 }
8816 .promo-codes-edit-save { font-size: var(--text-note); }
8817 .promo-codes-bulk-row { margin-top: var(--gap-section); }
8818 .promo-codes-bulk-btn { font-size: var(--text-note); opacity: 0.7; }
8819
8820 /* ===========================================
8821 USER MEDIA TAB (replaces inline styles in partials/tabs/user_media.html)
8822 =========================================== */
8823
8824 .user-media-intro {
8825 margin-bottom: var(--gap-section);
8826 }
8827
8828 .user-media-upload {
8829 border: 2px dashed var(--border);
8830 padding: var(--gap-pane);
8831 margin-bottom: var(--gap-pane);
8832 text-align: center;
8833 background: var(--surface-overlay);
8834 }
8835
8836 .user-media-upload--dragover {
8837 border-color: var(--action);
8838 }
8839
8840 .user-media-upload-row {
8841 margin-bottom: var(--gap-section);
8842 }
8843
8844 .user-media-upload-label {
8845 display: inline-block;
8846 font-size: var(--text-note);
8847 margin-right: var(--gap-section);
8848 }
8849
8850 .user-media-upload-folder-input {
8851 padding: var(--gap-bound);
8852 font-size: var(--text-note);
8853 width: 140px;
8854 margin-left: var(--gap-bound);
8855 }
8856
8857 .user-media-upload-progress {
8858 margin-top: var(--gap-section);
8859 }
8860
8861 .user-media-upload-status {
8862 font-size: var(--text-note);
8863 margin-bottom: var(--gap-bound);
8864 }
8865
8866 .user-media-upload-status--ok {
8867 color: var(--success);
8868 }
8869
8870 .user-media-upload-status--err {
8871 color: var(--danger);
8872 }
8873
8874 .user-media-folders {
8875 margin-bottom: var(--gap-section);
8876 display: flex;
8877 gap: var(--gap-peer);
8878 flex-wrap: wrap;
8879 align-items: center;
8880 }
8881
8882 .user-media-folders-label {
8883 font-size: var(--text-note);
8884 opacity: 0.7;
8885 }
8886
8887 .user-media-folders-btn {
8888 font-size: var(--text-fine);
8889 }
8890
8891 .user-media-grid {
8892 display: grid;
8893 grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
8894 gap: var(--gap-section);
8895 }
8896
8897 .user-media-card {
8898 background: var(--surface-overlay);
8899 border: 1px solid var(--border);
8900 padding: var(--gap-section);
8901 position: relative;
8902 }
8903
8904 .user-media-card-thumb {
8905 width: 100%;
8906 height: 120px;
8907 overflow: hidden;
8908 margin-bottom: var(--gap-peer);
8909 display: flex;
8910 align-items: center;
8911 justify-content: center;
8912 background: var(--surface-sunken);
8913 }
8914
8915 .user-media-card-thumb--video {
8916 font-size: var(--text-title);
8917 opacity: 0.4;
8918 }
8919
8920 .user-media-card-img {
8921 max-width: 100%;
8922 max-height: 120px;
8923 object-fit: contain;
8924 }
8925
8926 .user-media-card-name {
8927 font-size: var(--text-fine);
8928 word-break: break-all;
8929 margin-bottom: var(--gap-bound);
8930 }
8931
8932 .user-media-card-meta {
8933 font-size: var(--text-fine);
8934 opacity: 0.6;
8935 margin-bottom: var(--gap-peer);
8936 }
8937
8938 .user-media-card-actions {
8939 display: flex;
8940 gap: var(--gap-bound);
8941 }
8942
8943 .user-media-card-btn {
8944 font-size: var(--text-fine);
8945 padding: var(--gap-bound) var(--gap-peer);
8946 }
8947
8948 /* ===========================================
8949 ITEM VERSION UPLOAD (replaces inline styles in partials/item_version_upload.html)
8950 =========================================== */
8951
8952 .item-version-upload-grid {
8953 display: grid;
8954 grid-template-columns: 1fr 1fr;
8955 gap: var(--gap-section);
8956 }
8957
8958 /* Where one slot's own work has got to, in the three words `Progress` emits.
8959 Here rather than in makeover because the hook is a data attribute
8960 `quasi-webview` writes rather than a class makeover names, and this is its
8961 only consumer. A second host wanting it is the reason to move it. */
8962 .field-repeat-slot[data-repeat-progress="working"] {
8963 opacity: 0.75;
8964 }
8965
8966 .field-repeat-slot[data-repeat-progress="done"] {
8967 opacity: 0.55;
8968 }
8969
8970 .field-repeat-slot[data-repeat-progress="failed"] {
8971 border-left: 2px solid var(--danger);
8972 padding-left: var(--gap-peer);
8973 }
8974
8975 .item-version-upload-speed {
8976 font-size: var(--text-fine);
8977 opacity: 0.6;
8978 margin-top: var(--gap-bound);
8979 }
8980
8981 /* ===========================================
8982 INSERTION LIST (replaces inline styles in partials/insertion_list.html)
8983 =========================================== */
8984
8985 .insertion-list-heading {
8986 font-family: var(--font-mono);
8987 font-size: var(--text-body);
8988 margin-bottom: var(--gap-section);
8989 }
8990 .insertion-list-toolbar {
8991 margin-bottom: var(--gap-section);
8992 }
8993 .insertion-list-upload {
8994 font-family: var(--font-mono);
8995 }
8996 .insertion-list-table {
8997 width: 100%;
8998 font-family: var(--font-sans);
8999 }
9000 .insertion-list-table th {
9001 text-align: left;
9002 font-family: var(--font-mono);
9003 font-size: var(--text-note);
9004 }
9005 .insertion-list-table th.insertion-list-th-actions {
9006 text-align: right;
9007 }
9008 .insertion-list-table .insertion-list-actions {
9009 text-align: right;
9010 }
9011 .insertion-list-table .insertion-list-action-btn {
9012 font-family: var(--font-mono);
9013 }
9014
9015 /* ===========================================
9016 PROJECT MEMBERS TAB (replaces inline styles in partials/tabs/project_members.html)
9017 =========================================== */
9018
9019 .proj-members-summary {
9020 background: var(--surface-sunken);
9021 padding: var(--gap-section);
9022 margin-bottom: var(--gap-section);
9023 }
9024
9025 .proj-members-summary-row {
9026 display: flex;
9027 justify-content: space-between;
9028 align-items: center;
9029 }
9030
9031 .proj-members-stat {
9032 font-size: var(--text-head);
9033 font-weight: bold;
9034 }
9035
9036 .proj-members-add {
9037 margin-bottom: var(--gap-page);
9038 }
9039
9040 .proj-members-add-grid {
9041 display: grid;
9042 grid-template-columns: 1fr auto auto;
9043 gap: var(--gap-section);
9044 align-items: end;
9045 }
9046
9047 .proj-members-field-label {
9048 font-size: var(--text-note);
9049 display: block;
9050 margin-bottom: var(--gap-bound);
9051 }
9052
9053 .proj-members-add-result {
9054 margin-top: var(--gap-peer);
9055 }
9056
9057 .proj-members-name-link {
9058 color: inherit;
9059 }
9060
9061 .proj-members-split-cell {
9062 font-weight: bold;
9063 }
9064
9065 /* ===========================================
9066 PLACEMENT LIST (replaces inline styles in partials/placement_list.html)
9067 =========================================== */
9068
9069 .placement-list-heading {
9070 font-family: var(--font-mono);
9071 font-size: var(--text-body);
9072 margin-bottom: var(--gap-section);
9073 }
9074 .placement-list-table {
9075 width: 100%;
9076 font-family: var(--font-sans);
9077 margin-bottom: var(--gap-section);
9078 }
9079 .placement-list-table th {
9080 text-align: left;
9081 font-family: var(--font-mono);
9082 font-size: var(--text-note);
9083 }
9084 .placement-list-table th.placement-list-th-actions {
9085 text-align: right;
9086 }
9087 .placement-list-table .placement-list-actions {
9088 text-align: right;
9089 }
9090 .placement-list-table .placement-list-remove {
9091 font-family: var(--font-mono);
9092 }
9093 .placement-list-form {
9094 display: flex;
9095 gap: var(--gap-peer);
9096 align-items: flex-end;
9097 flex-wrap: wrap;
9098 }
9099 .placement-list-label {
9100 display: block;
9101 font-family: var(--font-mono);
9102 font-size: var(--text-fine);
9103 margin-bottom: var(--gap-bound);
9104 }
9105 .placement-list-add {
9106 font-family: var(--font-mono);
9107 }
9108
9109 /* ===========================================
9110 USER SYNCKIT TAB (replaces inline styles in partials/tabs/user_synckit.html)
9111 =========================================== */
9112
9113 .user-synckit-create { margin-bottom: var(--gap-pane); }
9114 .user-synckit-create-body {
9115 padding: var(--gap-section);
9116 background: var(--surface-overlay);
9117 border: 1px solid var(--border);
9118 }
9119 .user-synckit-create-form {
9120 display: flex;
9121 flex-direction: row;
9122 gap: var(--gap-section);
9123 align-items: flex-end;
9124 }
9125 .user-synckit-create-form label {
9126 display: block;
9127 font-size: var(--text-note);
9128 margin-bottom: var(--gap-bound);
9129 }
9130 .user-synckit-create-form input[type="text"],
9131 .user-synckit-create-form select {
9132 padding: var(--gap-peer);
9133 font-size: var(--text-note);
9134 }
9135 .user-synckit-create-form input[type="text"] { width: 220px; }
9136 .user-synckit-create-form select { width: 180px; }
9137
9138 .user-synckit-code { font-size: var(--text-fine); }
9139 .user-synckit-hint {
9140 font-size: var(--text-fine);
9141 margin-left: var(--gap-bound);
9142 }
9143
9144 .user-synckit-inline-btn {
9145 margin-left: var(--gap-peer);
9146 padding: var(--gap-bound) var(--gap-peer);
9147 font-size: var(--text-fine);
9148 }
9149 .user-synckit-row-btn {
9150 padding: var(--gap-bound) var(--gap-peer);
9151 font-size: var(--text-fine);
9152 }
9153 .user-synckit-row-btn + .user-synckit-row-btn {
9154 margin-left: var(--gap-bound);
9155 }
9156
9157 .user-synckit-status-active { color: var(--success); }
9158 .user-synckit-status-inactive { color: var(--danger); }
9159
9160 .user-synckit-edit-btn {
9161 padding: var(--gap-bound) var(--gap-peer);
9162 font-size: var(--text-fine);
9163 }
9164
9165 .user-synckit-create-error {
9166 color: var(--danger);
9167 margin-top: var(--gap-peer);
9168 }
9169
9170 /* ===========================================
9171 TOTP (replaces inline styles in partials/totp_setup.html and totp_status.html)
9172 =========================================== */
9173
9174 /* Section wrapper between TOTP subsections (QR + backup codes + confirm,
9175 or regenerate + disable). */
9176 .totp-section { margin-bottom: var(--gap-pane); }
9177
9178 /* Confirm-setup section is separated from the backup codes by a top border. */
9179 .totp-confirm {
9180 border-top: 1px solid var(--border);
9181 padding-top: var(--gap-section);
9182 }
9183
9184 /* QR code container, centered above the manual-entry details. */
9185 .totp-qr {
9186 text-align: center;
9187 margin-bottom: var(--gap-section);
9188 }
9189 .totp-qr img {
9190 max-width: 200px;
9191 image-rendering: pixelated;
9192 }
9193
9194 /* Collapsible "Manual entry key" group. */
9195 .totp-manual { margin-bottom: var(--gap-section); }
9196 .totp-manual summary {
9197 cursor: pointer;
9198 opacity: 0.7;
9199 font-size: var(--text-note);
9200 }
9201 .totp-manual-secret {
9202 display: block;
9203 margin-top: var(--gap-peer);
9204 word-break: break-all;
9205 font-size: var(--text-note);
9206 padding: var(--gap-peer);
9207 background: var(--surface-sunken);
9208 }
9209
9210 /* Backup codes grid (rendered list of one-time codes). */
9211 .backup-codes-grid {
9212 display: grid;
9213 grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
9214 gap: var(--gap-peer);
9215 }
9216 .backup-codes-grid code {
9217 padding: var(--gap-peer);
9218 background: var(--surface-sunken);
9219 text-align: center;
9220 font-size: var(--text-note);
9221 }
9222
9223 /* Status row for the "Enabled" badge above the management forms. */
9224 .totp-status-row {
9225 display: flex;
9226 align-items: center;
9227 gap: var(--gap-peer);
9228 margin-bottom: var(--gap-section);
9229 }
9230
9231 /* Container where regenerated backup codes are swapped in via HTMX. */
9232 .totp-regen-output { margin-top: var(--gap-section); }
9233
9234 /* ===========================================
9235 DASHBOARD-USER SHELL (replaces inline styles in dashboards/dashboard-user.html)
9236 =========================================== */
9237
9238 .sandbox-banner {
9239 background: var(--action);
9240 color: var(--primary-light);
9241 padding: var(--gap-section) var(--gap-pane);
9242 text-align: center;
9243 font-family: var(--font-mono);
9244 font-size: var(--text-note);
9245 }
9246
9247 .sandbox-banner-link {
9248 color: var(--primary-light);
9249 text-decoration: underline;
9250 margin-left: var(--gap-peer);
9251 }
9252 .account-status-actions {
9253 display: flex;
9254 gap: var(--gap-section);
9255 flex-wrap: wrap;
9256 margin-top: var(--gap-section);
9257 }
9258
9259 .suspension-wrap {
9260 margin-bottom: var(--gap-section);
9261 }
9262
9263 .password-warning-banner {
9264 margin-bottom: var(--gap-section);
9265 padding: var(--gap-section);
9266 background: color-mix(in oklch, var(--warning) 14%, var(--surface-page));
9267 border: 1px solid var(--warning);
9268 font-size: var(--text-body);
9269 }
9270
9271 .dash-user-header-links {
9272 margin-bottom: var(--gap-peer);
9273 display: flex;
9274 gap: var(--gap-pane);
9275 }
9276
9277 .dash-user-header-link {
9278 font-size: var(--text-note);
9279 opacity: 0.7;
9280 }
9281
9282 .checklist-recovery {
9283 padding: var(--gap-section) 0;
9284 text-align: right;
9285 }
9286
9287 .checklist-recovery-link {
9288 font-size: var(--text-note);
9289 opacity: 0.7;
9290 }
9291
9292
9293 /* ===========================================
9294 SUSPENSION BANNER (replaces inline styles in partials/suspension_banner.html)
9295 =========================================== */
9296
9297 .suspension-banner {
9298 background: color-mix(in oklch, var(--danger) 10%, var(--surface-page));
9299 border: 1px solid var(--danger);
9300 padding: var(--gap-pane);
9301 margin-bottom: var(--gap-pane);
9302 }
9303
9304 .suspension-banner-title {
9305 margin: 0 0 var(--gap-peer) 0;
9306 color: var(--danger);
9307 }
9308
9309 .suspension-banner-reason {
9310 font-size: var(--text-note);
9311 margin: 0 0 var(--gap-section) 0;
9312 }
9313
9314 .suspension-banner-denial {
9315 background: var(--surface-overlay);
9316 padding: var(--gap-section);
9317 margin-bottom: var(--gap-section);
9318 font-size: var(--text-note);
9319 }
9320
9321 .suspension-banner-denial-response {
9322 margin: var(--gap-bound) 0 0 0;
9323 }
9324
9325 .suspension-banner-denial-hint {
9326 margin: var(--gap-peer) 0 0 0;
9327 opacity: 0.7;
9328 }
9329
9330 .suspension-banner-pending {
9331 font-size: var(--text-note);
9332 opacity: 0.7;
9333 margin: 0;
9334 }
9335
9336 .suspension-banner-appeal {
9337 margin-top: var(--gap-peer);
9338 }
9339
9340 .suspension-banner-appeal-summary {
9341 cursor: pointer;
9342 font-size: var(--text-note);
9343 }
9344
9345 .suspension-banner-appeal-form {
9346 margin-top: var(--gap-section);
9347 }
9348
9349 .suspension-banner-appeal-text {
9350 width: 100%;
9351 margin-bottom: var(--gap-peer);
9352 font-size: var(--text-note);
9353 }
9354
9355 .suspension-banner-appeal-submit {
9356 font-size: var(--text-note);
9357 }
9358
9359 .suspension-banner-export {
9360 margin-top: var(--gap-section);
9361 }
9362
9363 .suspension-banner-export-link {
9364 font-size: var(--text-note);
9365 }
9366
9367 /* ===========================================
9368 REPORT MODAL (replaces inline styles in partials/report_modal.html)
9369 =========================================== */
9370
9371 .report-modal-content {
9372 background: var(--surface-page);
9373 padding: var(--gap-page);
9374 width: 90%;
9375 max-width: 480px;
9376 max-height: 90vh;
9377 overflow-y: auto;
9378 }
9379
9380 .report-modal-title {
9381 font-size: var(--text-subhead);
9382 margin: 0;
9383 }
9384
9385 .report-modal-fieldset {
9386 border: none;
9387 padding: 0;
9388 margin: 0 0 var(--gap-section) 0;
9389 }
9390
9391 .report-modal-legend {
9392 font-size: var(--text-note);
9393 font-weight: 600;
9394 margin-bottom: var(--gap-peer);
9395 }
9396
9397 .report-modal-option {
9398 display: block;
9399 margin-bottom: var(--gap-peer);
9400 cursor: pointer;
9401 }
9402
9403 .report-modal-details {
9404 margin-bottom: var(--gap-section);
9405 }
9406
9407 .report-modal-details-label {
9408 display: block;
9409 font-size: var(--text-note);
9410 margin-bottom: var(--gap-bound);
9411 }
9412
9413 .report-modal-textarea {
9414 width: 100%;
9415 font-size: var(--text-note);
9416 }
9417
9418 .report-modal-actions {
9419 display: flex;
9420 gap: var(--gap-peer);
9421 justify-content: flex-end;
9422 }
9423
9424 /* ===========================================
9425 LIBRARY COLLECTIONS TAB (partials/tabs/library_collections.html)
9426 =========================================== */
9427
9428 .badge.is-faded { opacity: 0.5; }
9429 .lib-coll-new { margin-top: var(--gap-pane); }
9430 .lib-coll-new-summary { cursor: pointer; font-weight: bold; }
9431 .lib-coll-new-form { margin-top: var(--gap-section); }
9432 .lib-coll-wishlist-heading {
9433 font-size: var(--text-lead);
9434 margin-top: var(--gap-page);
9435 margin-bottom: var(--gap-section);
9436 opacity: 0.8;
9437 }
9438 .lib-coll-wishlist-wrap {
9439 overflow-x: auto;
9440 -webkit-overflow-scrolling: touch;
9441 }
9442 .lib-coll-wishlist-table { min-width: 500px; }
9443 .lib-coll-row-actions { white-space: nowrap; }
9444 .lib-coll-row-actions .lib-coll-cart-btn { margin-right: var(--gap-bound); }
9445
9446 /* ===========================================
9447 USER ANALYTICS TAB (partials/tabs/user_analytics.html)
9448 =========================================== */
9449
9450 .analytics-export-link { margin-left: var(--gap-section); }
9451 .analytics-range-heading {
9452 font-size: var(--text-lead);
9453 margin: 0;
9454 }
9455 .analytics-section--comparison { margin-bottom: var(--gap-page); }
9456 .analytics-section--comparison h2 { margin-bottom: var(--gap-section); }
9457 .analytics-table-wrap {
9458 overflow-x: auto;
9459 -webkit-overflow-scrolling: touch;
9460 }
9461 .analytics-table { min-width: 500px; }
9462 .analytics-revenue-cell {
9463 display: flex;
9464 align-items: center;
9465 gap: var(--gap-peer);
9466 }
9467 .analytics-revenue-bar {
9468 width: var(--fill, 0%);
9469 height: 10px;
9470 min-width: 2px;
9471 max-width: 80px;
9472 background: var(--action);
9473 }
9474
9475 /* ===========================================
9476 ITEM EMBED TAB (templates/partials/tabs/item_embed.html)
9477 =========================================== */
9478 .embed-intro {
9479 opacity: 0.7;
9480 font-size: var(--text-note);
9481 margin-bottom: var(--gap-pane);
9482 }
9483 .embed-option {
9484 margin-bottom: var(--gap-pane);
9485 padding: var(--gap-section);
9486 border: 1px solid var(--border);
9487 border-radius: var(--radius-panel);
9488 }
9489 .embed-option h4 {
9490 margin-bottom: var(--gap-bound);
9491 font-size: var(--text-body);
9492 }
9493 .embed-option-desc {
9494 font-size: var(--text-note);
9495 opacity: 0.7;
9496 margin-bottom: var(--gap-peer);
9497 }
9498 .embed-preview {
9499 margin: var(--gap-section) 0;
9500 overflow: hidden;
9501 }
9502 .embed-iframe {
9503 border: none;
9504 border-radius: var(--radius-control);
9505 }
9506 .embed-layout-toggle {
9507 font-size: var(--text-note);
9508 margin-bottom: var(--gap-peer);
9509 display: flex;
9510 gap: var(--gap-section);
9511 }
9512 .embed-layout-toggle label {
9513 cursor: pointer;
9514 display: flex;
9515 align-items: center;
9516 gap: var(--gap-bound);
9517 }
9518 .embed-code-row {
9519 display: flex;
9520 align-items: center;
9521 gap: var(--gap-peer);
9522 }
9523 .embed-snippet {
9524 flex: 1;
9525 padding: var(--gap-peer) var(--gap-section);
9526 background: var(--surface-overlay);
9527 border-radius: var(--radius-control);
9528 font-size: var(--text-fine);
9529 overflow-x: auto;
9530 white-space: nowrap;
9531 font-family: var(--font-mono);
9532 border: 1px solid var(--border);
9533 }
9534 .copy-btn {
9535 padding: 6px 12px;
9536 font-size: var(--text-fine);
9537 background: var(--action);
9538 color: var(--primary-light);
9539 border: none;
9540 border-radius: var(--radius-control);
9541 cursor: pointer;
9542 white-space: nowrap;
9543 }
9544 .copy-btn:hover {
9545 opacity: 0.9;
9546 }
9547
9548 /* ===========================================
9549 ADMIN NAV (templates/partials/admin_nav.html)
9550 =========================================== */
9551 .admin-nav {
9552 display: flex;
9553 gap: var(--gap-peer);
9554 margin-bottom: var(--gap-pane);
9555 flex-wrap: wrap;
9556 font-family: var(--font-mono);
9557 font-size: var(--text-note);
9558 }
9559 .admin-nav a {
9560 padding: var(--gap-bound) var(--gap-section);
9561 text-decoration: none;
9562 }
9563
9564 /* Admin nav items and filter-bar toggles are .btn-primary / .btn-secondary,
9565 toggled by the templates to mark the current page or active filter. Colour
9566 and depth come from the button recipe; only the tighter admin metrics and
9567 the active weight are stated here. */
9568 .admin-nav a.btn-primary,
9569 .filter-bar button.btn-primary {
9570 font-weight: 700;
9571 }
9572 .admin-nav a.btn-secondary:hover,
9573 .filter-bar button.btn-secondary:hover {
9574 color: var(--content);
9575 }
9576
9577 /* ===========================================
9578 VIDEO PLAYER PAGE (templates/pages/video_player.html)
9579 =========================================== */
9580 .video-cover-img {
9581 width: 100%;
9582 display: block;
9583 background: var(--primary-dark);
9584 }
9585 .video-cover-placeholder {
9586 width: 100%;
9587 aspect-ratio: 16 / 9;
9588 background: var(--primary-dark);
9589 display: flex;
9590 align-items: center;
9591 justify-content: center;
9592 color: var(--content-muted);
9593 }
9594 .video-store-cta {
9595 margin: var(--gap-page) 0;
9596 padding: var(--gap-pane);
9597 background: var(--surface-sunken);
9598 text-align: center;
9599 }
9600 .video-cta-lead {
9601 font-size: var(--text-lead);
9602 margin-bottom: var(--gap-section);
9603 }
9604 .video-cta-price {
9605 font-size: var(--text-head);
9606 margin-bottom: var(--gap-section);
9607 }
9608 .video-cta-fineprint {
9609 font-size: var(--text-note);
9610 opacity: 0.7;
9611 margin-top: var(--gap-section);
9612 }
9613 .video-library-status {
9614 font-family: var(--font-mono);
9615 color: var(--content-muted);
9616 }
9617
9618 /* ===========================================
9619 LIBRARY LOCKED PAGE (templates/pages/library_locked.html)
9620 =========================================== */
9621 .library-locked-byline {
9622 opacity: 0.7;
9623 }
9624 .library-locked-cover {
9625 max-width: 320px;
9626 margin: var(--gap-section) 0;
9627 /* Reserve the space before the image loads (audit Run 17 Frontend). */
9628 aspect-ratio: 1 / 1;
9629 object-fit: cover;
9630 }
9631 .library-locked-purchase {
9632 margin-top: var(--gap-page);
9633 }
9634 .library-locked-lead {
9635 opacity: 0.85;
9636 }
9637 .library-locked-actions {
9638 margin-top: var(--gap-pane);
9639 display: flex;
9640 gap: var(--gap-section);
9641 flex-wrap: wrap;
9642 }
9643 .library-locked-bundles {
9644 margin-top: var(--gap-pane);
9645 }
9646 .library-locked-bundles-title {
9647 font-weight: bold;
9648 }
9649 .library-locked-bundle {
9650 margin-top: var(--gap-peer);
9651 }
9652 .library-locked-bundle-price {
9653 opacity: 0.7;
9654 }
9655
9656 /* ===========================================
9657 BLOG (replaces inline styles in project_blog.html
9658 and dashboard-blog-editor.html)
9659 =========================================== */
9660
9661 .blog-empty-state { padding: var(--gap-page) 0; }
9662 .blog-empty-hint {
9663 opacity: 0.7;
9664 font-size: var(--text-note);
9665 margin-top: var(--gap-peer);
9666 }
9667 .blog-post-title-link { font-weight: bold; }
9668 .blog-post-actions {
9669 display: flex;
9670 gap: var(--gap-peer);
9671 }
9672
9673 /* Blog editor dashboard (templates/dashboards/dashboard-blog-editor.html). */
9674 .blog-editor h1 {
9675 font-size: var(--text-title);
9676 margin-bottom: var(--gap-section);
9677 text-align: left;
9678 }
9679 .blog-editor .editor-form { max-width: 800px; }
9680 .blog-editor .editor-form .form-group { margin-bottom: var(--gap-section); }
9681 .blog-editor .editor-form textarea {
9682 font-family: var(--font-mono);
9683 font-size: var(--text-note);
9684 min-height: 400px;
9685 resize: vertical;
9686 }
9687 .blog-editor-slug-spinner { font-size: var(--text-note); }
9688 .blog-editor-actions {
9689 display: flex;
9690 gap: var(--gap-section);
9691 align-items: center;
9692 }
9693 .blog-editor-status { margin-top: var(--gap-peer); }
9694
9695 /* ===========================================
9696 AUDIO PLAYER (replaces inline styles in pages/audio_player.html)
9697 =========================================== */
9698
9699 .audio-store-cta {
9700 margin: var(--gap-page) 0;
9701 padding: var(--gap-pane);
9702 background: var(--surface-sunken);
9703 text-align: center;
9704 }
9705 .audio-store-cta .audio-access-msg {
9706 font-size: var(--text-lead);
9707 margin-bottom: var(--gap-section);
9708 }
9709 .audio-store-cta .audio-price {
9710 font-size: var(--text-head);
9711 margin-bottom: var(--gap-section);
9712 }
9713 .audio-store-cta .audio-fee-note {
9714 font-size: var(--text-note);
9715 opacity: 0.7;
9716 margin-top: var(--gap-section);
9717 }
9718 .audio-library-status {
9719 font-family: var(--font-mono);
9720 color: var(--content-muted);
9721 }
9722
9723 /* ===========================================
9724 SECTIONS WIZARD STEP (templates/wizards/steps/item/sections.html)
9725 Parallels .psection-row used by item_details + project-sections.js,
9726 but isolated so wizard markup can evolve independently.
9727 =========================================== */
9728
9729 .section-row {
9730 display: flex;
9731 align-items: center;
9732 gap: var(--gap-section);
9733 padding: var(--gap-group) 0;
9734 border-bottom: 1px solid var(--border);
9735 }
9736 .section-row-title { flex: 1; font-weight: bold; }
9737 .section-row-length { font-size: var(--text-fine); opacity: 0.6; }
9738 .section-row .section-delete-btn {
9739 padding: var(--gap-bound) var(--gap-group);
9740 font-size: var(--text-fine);
9741 }
9742
9743 .section-add-body { margin-top: var(--gap-section); }
9744
9745 .wizard-actions { margin-top: var(--gap-pane); }
9746
9747 /* ===========================================
9748 PROJECT SUBSCRIPTIONS TAB (templates/partials/tabs/project_subscriptions.html)
9749 =========================================== */
9750
9751 .subs-connect-info {
9752 margin-bottom: var(--gap-pane);
9753 padding: var(--gap-section);
9754 }
9755 .subs-new-tier { margin-bottom: var(--gap-page); }
9756
9757 .tier-row-desc {
9758 font-size: var(--text-note);
9759 opacity: 0.7;
9760 margin-top: var(--gap-bound);
9761 }
9762 .tier-action-btn {
9763 font-size: var(--text-note);
9764 padding: var(--gap-bound) var(--gap-group);
9765 }
9766
9767 .subs-members-bar {
9768 margin-top: var(--gap-page);
9769 display: flex;
9770 justify-content: space-between;
9771 align-items: center;
9772 }
9773
9774 /* ===========================================
9775 LIBRARY CONSUMPTION PAGES
9776 ===========================================
9777 Shared styles for the library-side consumption surfaces:
9778 text_reader.html, library_text.html (.article-page),
9779 library_downloads.html (.library-page),
9780 library_audio.html / library_video.html (.media-container + helpers).
9781 The .article-page and .library-page sections higher up in this file
9782 are activated by body classes set in the templates. */
9783
9784 /* Faded excerpt text on the public store-page text reader. */
9785 .article-page .article-body--excerpt { opacity: 0.85; }
9786
9787 /* Store-page call-to-action block (text_reader.html). */
9788 .article-page .store-cta {
9789 margin: var(--gap-page) 0;
9790 padding: var(--gap-pane);
9791 background: var(--surface-sunken);
9792 text-align: center;
9793 }
9794 .article-page .store-cta-lead {
9795 font-size: var(--text-lead);
9796 margin-bottom: var(--gap-section);
9797 }
9798 .article-page .store-cta-price {
9799 font-size: var(--text-head);
9800 margin-bottom: var(--gap-section);
9801 }
9802 .article-page .store-cta-note {
9803 font-size: var(--text-note);
9804 opacity: 0.7;
9805 margin-top: var(--gap-section);
9806 }
9807 .article-page .store-cta .library-status {
9808 font-family: var(--font-mono);
9809 color: var(--content-muted);
9810 }
9811
9812 /* Library back-link breadcrumb shared by the media library pages. */
9813 .media-container .library-back {
9814 font-size: var(--text-note);
9815 opacity: 0.7;
9816 margin: var(--gap-section) 0;
9817 }
9818 .media-container .library-back a { color: var(--content); }
9819
9820 /* Source-files download list under audio/video library pages.
9821 The .library-page version exists higher up; this block scopes the
9822 same look under .media-container for audio/video. */
9823 .media-container .library-downloads {
9824 background: var(--surface-overlay);
9825 padding: var(--gap-pane);
9826 margin-top: var(--gap-page);
9827 }
9828 .media-container .library-downloads h3 {
9829 font-size: var(--text-lead);
9830 margin-bottom: var(--gap-section);
9831 }
9832 .media-container .download-row {
9833 display: flex;
9834 align-items: center;
9835 gap: var(--gap-section);
9836 padding: var(--gap-peer) 0;
9837 border-bottom: 1px solid var(--border);
9838 }
9839 .media-container .download-row:last-child { border-bottom: none; }
9840 .media-container .download-version {
9841 min-width: 4em;
9842 font-family: var(--font-mono);
9843 font-size: var(--text-note);
9844 }
9845 .media-container .download-label { flex: 1; }
9846 .media-container .download-label--empty { flex: 1; opacity: 0.5; }
9847 .media-container .download-size {
9848 font-size: var(--text-note);
9849 opacity: 0.6;
9850 min-width: 5em;
9851 text-align: right;
9852 }
9853
9854 /* Compact download button used in both library_downloads.html and
9855 media library pages. */
9856 .btn-download-compact {
9857 padding: var(--gap-peer) var(--gap-section);
9858 font-size: var(--text-note);
9859 }
9860
9861 /* Faded "no label" placeholder on library_downloads.html download rows. */
9862 .library-page .download-label--empty { opacity: 0.5; }
9863
9864 /* Library-downloads description <details> summary + body. */
9865 .library-page .lib-summary {
9866 cursor: pointer;
9867 font-family: var(--font-display);
9868 font-size: var(--text-subhead);
9869 }
9870 .library-page .lib-summary-body { margin-top: var(--gap-section); }
9871
9872 /* License section on library_downloads.html. */
9873 .library-page .lib-license-name { margin-bottom: var(--gap-section); }
9874 .library-page .lib-license-download {
9875 font-size: var(--text-note);
9876 color: var(--content);
9877 }
9878
9879 /* Footer below the item-footer (was inline on library_downloads.html). */
9880 .library-page .item-footer-sub { margin-top: var(--gap-peer); }
9881
9882 /* library_text.html footer: a terse in-page footer for the library's own
9883 text view, unrelated to base.html's site-footer. */
9884 .article-page .lib-text-footer {
9885 text-align: center;
9886 padding: var(--gap-page);
9887 font-size: var(--text-note);
9888 opacity: 0.6;
9889 border-top: 1px solid var(--border);
9890 }
9891 .article-page .lib-text-footer a { color: var(--content); }
9892
9893 /* Media player play/pause icon initial state. The JS in media-player.js
9894 toggles via element.style.display directly, so we must NOT use
9895 !important here, element.style would still beat a normal-priority
9896 declaration, but !important in the stylesheet would defeat that. */
9897 .media-icon-hidden { display: none; }
9898
9899 /* Video element fills its .video-display parent. .video-display already
9900 provides a black background. */
9901 .video-display video { width: 100%; }
9902
9903 /* Cover image inside .cover-art on library_audio.html, the parent
9904 .cover-art already sets aspect ratio and centering; the inline
9905 width/aspect-ratio/object-fit was redundant with .cover-art img. */
9906
9907 /* ===========================================
9908 USER DASHBOARD TABS (support / sessions / buyer contacts)
9909 =========================================== */
9910
9911 /* Sessions tab (templates/partials/tabs/user_sessions.html). */
9912 .sessions-tab .sessions-table { width: 100%; }
9913 .sessions-tab .device-cell {
9914 max-width: 300px;
9915 overflow: hidden;
9916 text-overflow: ellipsis;
9917 white-space: nowrap;
9918 }
9919
9920 /* Buyer contacts tab (templates/partials/tabs/buyer_contacts.html). */
9921 .buyer-contacts .contacts-table { min-width: 500px; }
9922
9923 /* ===========================================
9924 LIBRARY TABS
9925 Shared classes for the library-dashboard tab partials:
9926 library_wishlists, library_contacts, library_purchases,
9927 library_subscriptions. Migrated from inline styles.
9928 =========================================== */
9929
9930 /* Tables in library tabs need a minimum width so columns stay readable
9931 while the outer container scrolls horizontally on narrow viewports. */
9932
9933 /* Empty-state hint paragraph (smaller, slightly faded). */
9934 .library-tab-empty-hint {
9935 font-size: var(--text-note);
9936 opacity: 0.7;
9937 margin-bottom: var(--gap-section);
9938 }
9939
9940 /* Compact row-action button (wishlist Add to Cart / Remove, etc.). */
9941
9942 .library-filter-wrap { margin-bottom: var(--gap-section); }
9943
9944 /* Purchases table "Open" action link + context menu trigger group. */
9945 .library-row-actions {
9946 display: flex;
9947 align-items: center;
9948 gap: var(--gap-peer);
9949 }
9950 .library-row-open {
9951 font-size: var(--text-fine);
9952 padding: var(--gap-bound) var(--gap-section);
9953 text-decoration: none;
9954 white-space: nowrap;
9955 }
9956
9957 /* Section sub-heading inside purchases tab (e.g. "Subscriptions"). */
9958 .library-tab-subheading {
9959 font-size: var(--text-lead);
9960 margin-top: var(--gap-page);
9961 margin-bottom: var(--gap-section);
9962 opacity: 0.8;
9963 }
9964
9965 /* Inline-block button styled as a link target on empty-state CTAs. */
9966 .library-tab-cta {
9967 display: inline-block;
9968 text-decoration: none;
9969 }
9970
9971 /* Context menu (dropdown) on each purchase row. */
9972 .context-menu-wrapper { position: relative; }
9973 .context-menu-btn {
9974 background: none;
9975 border: none;
9976 cursor: pointer;
9977 padding: var(--gap-peer);
9978 font-size: var(--text-subhead);
9979 line-height: 1;
9980 color: var(--content-muted);
9981 border-radius: var(--radius-control);
9982 }
9983 .context-menu-btn:hover {
9984 background: var(--surface-sunken);
9985 color: var(--content);
9986 }
9987 .context-menu {
9988 display: none;
9989 position: absolute;
9990 right: 0;
9991 top: 100%;
9992 background: var(--surface-overlay);
9993 border: 1px solid var(--border);
9994 box-shadow: var(--elevation-overlay);
9995 min-width: 160px;
9996 z-index: var(--z-context);
9997 }
9998 .context-menu.open { display: block; }
9999 .context-menu-item {
10000 display: block;
10001 width: 100%;
10002 padding: var(--gap-section);
10003 text-align: left;
10004 background: none;
10005 border: none;
10006 font-family: var(--font-sans);
10007 font-size: var(--text-note);
10008 color: var(--content);
10009 cursor: pointer;
10010 text-decoration: none;
10011 }
10012 .context-menu-item:hover { background: var(--surface-sunken); }
10013 .context-menu-item.danger { color: var(--danger); }
10014
10015 /* Inline license-key code chip shown beneath a purchase date. */
10016 .license-key-inline { margin-top: var(--gap-bound); }
10017 .key-code-small {
10018 font-size: var(--text-fine);
10019 padding: var(--gap-bound) var(--gap-peer);
10020 background: var(--surface-sunken);
10021 border-radius: var(--radius-control);
10022 cursor: pointer;
10023 user-select: all;
10024 }
10025 .key-code-small:hover { background: var(--border); }
10026
10027 /* Library row title link reads as plain text until hovered. */
10028 .library-title-link:hover { text-decoration: underline; }
10029 /* ===========================================
10030 ADMIN ENTRIES (replaces inline styles in partials/admin_user_entries.html,
10031 partials/admin_waitlist_entries.html,
10032 partials/admin_appeal_entries.html)
10033 =========================================== */
10034
10035 /* (`.badge-active` / `.badge-trusted` removed in wave 2 tier B2. They were a
10036 second, ad-hoc spelling of what data-tone="success" already says, and the
10037 admin entries take the tone now.) */
10038
10039 /* "Trust" toggle button sits next to a Trusted badge. */
10040 .admin-entries-trust-btn {
10041 margin-left: var(--gap-bound);
10042 }
10043
10044 /* Waitlist pitch cell, narrow body text. */
10045 .admin-entries-pitch {
10046 font-size: var(--text-note);
10047 max-width: 300px;
10048 }
10049 .admin-entries-pitch-empty {
10050 opacity: 0.5;
10051 font-style: italic;
10052 }
10053 .admin-entries-pitch-inviter {
10054 margin-top: var(--gap-bound);
10055 }
10056
10057 /* Appeal text columns. */
10058 .admin-entries-reason {
10059 font-size: var(--text-note);
10060 max-width: 200px;
10061 }
10062 .admin-entries-appeal {
10063 font-size: var(--text-note);
10064 max-width: 300px;
10065 }
10066
10067 /* Decide form textarea + stacked action buttons. */
10068 .admin-entries-decide-textarea {
10069 width: 200px;
10070 }
10071 .admin-entries-decide-actions {
10072 display: flex;
10073 flex-direction: column;
10074 gap: var(--gap-bound);
10075 }
10076
10077 /* Pagination row at the bottom of admin entry tables. */
10078 .admin-entries-pagination {
10079 display: flex;
10080 justify-content: center;
10081 align-items: center;
10082 gap: var(--gap-section);
10083 padding: var(--gap-section) 0;
10084 }
10085
10086 /* ===========================================
10087 ITEM TABS (item_settings, item_sales, item_overview partials)
10088 =========================================== */
10089
10090 /* Schedule form is hidden until "Schedule" button is clicked. JS toggles
10091 the `.hidden` utility class. */
10092 .item-settings-schedule-form {
10093 margin-top: var(--gap-section);
10094 }
10095
10096 .item-overview-embed {
10097 margin-top: var(--gap-pane);
10098 }
10099 .item-overview-embed > summary {
10100 cursor: pointer;
10101 font-weight: bold;
10102 font-size: var(--text-lead);
10103 }
10104
10105 /* ===========================================
10106 ITEM TEXT EDITOR (partials/item_text_editor.html)
10107 =========================================== */
10108
10109 .text-editor-actions {
10110 margin-top: var(--gap-section);
10111 }
10112
10113 /* ===========================================
10114 PROJECT ANALYTICS TAB (partials/tabs/project_analytics.html)
10115 =========================================== */
10116
10117 .tab-docs-extra {
10118 margin-left: var(--gap-section);
10119 }
10120
10121 .analytics-tab-header {
10122 display: flex;
10123 justify-content: space-between;
10124 align-items: center;
10125 margin-bottom: var(--gap-section);
10126 }
10127 .analytics-tab-header h2 {
10128 font-size: var(--text-lead);
10129 margin: 0;
10130 }
10131
10132 .analytics-empty-block {
10133 text-align: center;
10134 padding: var(--gap-page) var(--gap-section);
10135 opacity: 0.6;
10136 }
10137
10138 /* ===========================================
10139 RECEIPT PAGE INLINE FRAGMENTS (pages/receipt.html)
10140 =========================================== */
10141
10142 .receipt-page .receipt-brand {
10143 opacity: 0.6;
10144 font-size: var(--text-note);
10145 }
10146 .receipt-page .receipt-meta {
10147 text-align: right;
10148 }
10149 .receipt-page .receipt-meta-line {
10150 font-family: var(--font-mono);
10151 font-size: var(--text-note);
10152 opacity: 0.6;
10153 }
10154 .receipt-page .receipt-meta-sub {
10155 font-family: var(--font-mono);
10156 font-size: var(--text-fine);
10157 opacity: 0.4;
10158 margin-top: var(--gap-bound);
10159 }
10160 .receipt-page .print-btn {
10161 margin-top: var(--gap-section);
10162 font-size: var(--text-note);
10163 }
10164
10165 /* ===========================================
10166 IMPORT PAGE INLINE FRAGMENTS (dashboards/dashboard-import.html)
10167 =========================================== */
10168
10169 .import-page .import-preview-label,
10170 .import-page .import-mapping-label,
10171 .import-page .import-progress-label {
10172 font-family: var(--font-mono);
10173 font-size: var(--text-note);
10174 margin-bottom: var(--gap-peer);
10175 }
10176 .import-page .import-mapping-label {
10177 margin-bottom: var(--gap-section);
10178 }
10179 .import-page .import-progress-label {
10180 font-size: var(--text-note);
10181 }
10182 .import-page .import-start-btn {
10183 margin-top: var(--gap-section);
10184 }
10185 .import-page .import-history-heading {
10186 font-size: var(--text-subhead);
10187 margin-bottom: var(--gap-section);
10188 }
10189
10190 /* ===========================================
10191 SLUG STATUS PARTIAL (partials/slug_status.html)
10192 =========================================== */
10193
10194 .slug-status {
10195 font-size: var(--text-note);
10196 }
10197 .slug-status.available { color: var(--success); }
10198 .slug-status.taken { color: var(--danger); }
10199
10200 .slug-suggestions {
10201 font-size: var(--text-note);
10202 display: block;
10203 margin-top: var(--gap-bound);
10204 }
10205 .slug-suggestion-link {
10206 margin-right: var(--gap-peer);
10207 }
10208
10209 /* ===========================================
10210 STRIPE DISCLAIMER PAGE (pages/stripe_disclaimer.html)
10211 =========================================== */
10212
10213 .stripe-disclaimer-page .container {
10214 max-width: 700px;
10215 margin: var(--gap-pane) auto;
10216 padding: 0 var(--gap-page);
10217 }
10218 .stripe-disclaimer-page .stripe-disclaimer-title {
10219 text-align: center;
10220 }
10221 .stripe-disclaimer-page .tagline {
10222 font-size: var(--text-note);
10223 opacity: 0.7;
10224 margin-bottom: var(--gap-page);
10225 text-align: center;
10226 }
10227 .stripe-disclaimer-page h2 {
10228 font-size: var(--text-head);
10229 margin-bottom: var(--gap-pane);
10230 }
10231 .stripe-disclaimer-page .stripe-disclaimer-lead {
10232 margin-bottom: var(--gap-pane);
10233 opacity: 0.8;
10234 }
10235 .stripe-disclaimer-page .section { margin-bottom: var(--gap-page); }
10236 .stripe-disclaimer-page .section h3 {
10237 font-size: var(--text-lead);
10238 margin-bottom: var(--gap-section);
10239 font-family: var(--font-mono);
10240 }
10241 .stripe-disclaimer-page .section ul {
10242 margin: 0;
10243 padding-left: var(--gap-section);
10244 }
10245 .stripe-disclaimer-page .section li {
10246 margin-bottom: var(--gap-peer);
10247 line-height: 1.5;
10248 }
10249 .stripe-disclaimer-page .stripe-disclaimer-fee-lead {
10250 margin-bottom: var(--gap-section);
10251 }
10252 .stripe-disclaimer-page .fee-example {
10253 background: var(--surface-page);
10254 padding: var(--gap-section);
10255 margin: var(--gap-section) 0;
10256 font-family: var(--font-mono);
10257 font-size: var(--text-note);
10258 }
10259 .stripe-disclaimer-page .fee-row {
10260 display: flex;
10261 justify-content: space-between;
10262 padding: var(--gap-bound) 0;
10263 }
10264 .stripe-disclaimer-page .fee-row.highlight {
10265 font-weight: bold;
10266 border-top: 1px solid var(--border);
10267 margin-top: var(--gap-peer);
10268 padding-top: var(--gap-peer);
10269 }
10270 .stripe-disclaimer-page .fee-row .zero { color: var(--content); }
10271 .stripe-disclaimer-page .stripe-disclaimer-note {
10272 font-size: var(--text-note);
10273 opacity: 0.8;
10274 }
10275 .stripe-disclaimer-page .acceptance {
10276 background: var(--surface-page);
10277 padding: var(--gap-section);
10278 margin: var(--gap-pane) 0;
10279 }
10280 .stripe-disclaimer-page .checkbox-group {
10281 display: flex;
10282 align-items: flex-start;
10283 gap: var(--gap-section);
10284 }
10285 .stripe-disclaimer-page .checkbox-group input[type="checkbox"] {
10286 margin-top: var(--gap-bound);
10287 flex-shrink: 0;
10288 }
10289 .stripe-disclaimer-page .checkbox-group label {
10290 line-height: 1.5;
10291 cursor: pointer;
10292 }
10293 .stripe-disclaimer-page .button-row {
10294 display: flex;
10295 gap: var(--gap-section);
10296 margin-top: var(--gap-pane);
10297 }
10298 .stripe-disclaimer-page .button-row button { flex: 1; }
10299 .stripe-disclaimer-page button:disabled {
10300 opacity: 0.5;
10301 cursor: not-allowed;
10302 }
10303 .stripe-disclaimer-page .note {
10304 font-size: var(--text-note);
10305 opacity: 0.6;
10306 margin-top: var(--gap-section);
10307 text-align: center;
10308 }
10309
10310 /* ===========================================
10311 LONG-TAIL CLEANUP, shared utilities promoted from the final inline-style sweep.
10312 Each rule replaces ≥2 inline-style instances across templates.
10313 =========================================== */
10314
10315 /* Tab-spinner loading indicator (used by 3+ dashboards). */
10316
10317 /* Inline-block secondary action link styled as a button. */
10318 .btn-link {
10319 display: inline-block;
10320 padding: var(--gap-peer) var(--gap-section);
10321 text-decoration: none;
10322 }
10323 .btn-link--sm { padding: var(--gap-peer) var(--gap-section); }
10324
10325 /* Username-availability spinner (wizards + standalone account form). */
10326 .username-spinner { font-size: var(--text-note); }
10327
10328 /* Centered foot-link beneath wizard cards. */
10329 .foot-link--centered { margin-top: var(--gap-section); text-align: center; }
10330
10331 /* Health-page status dots. */
10332 .health-indicator-dot { width: 8px; height: 8px; margin-right: var(--gap-peer); }
10333 .health-error-text { color: var(--danger); }
10334
10335 /* Inline error fragments (login_error, error_fragment) toggle this on. */
10336 .error-message.is-active { display: block; }
10337
10338 /* Cart count badge in nav. */
10339 .cart-badge-count { font-size: var(--text-fine); }
10340
10341 /* Empty-row inside a data table. */
10342 .table-empty-row { text-align: center; opacity: 0.6; }
10343
10344 /* Discover grid: card wrapper + body anchor. */
10345 .discover-grid-card { position: relative; }
10346 .discover-card-link {
10347 text-decoration: none;
10348 color: inherit;
10349 display: flex;
10350 flex-direction: column;
10351 flex: 1;
10352 }
10353 .discover-filter-checkbox {
10354 display: flex;
10355 align-items: center;
10356 gap: var(--gap-peer);
10357 cursor: pointer;
10358 font-size: var(--text-note);
10359 }
10360
10361 /* Tag suggestion list. */
10362 .tag-suggest-label {
10363 font-size: var(--text-fine);
10364 opacity: 0.7;
10365 margin-bottom: var(--gap-peer);
10366 overflow-wrap: anywhere;
10367 }
10368 .tag-suggest-pill {
10369 background: var(--surface-sunken);
10370 border: 1px solid var(--border);
10371 padding: var(--gap-bound) var(--gap-peer);
10372 font-size: var(--text-fine);
10373 cursor: pointer;
10374 margin: var(--gap-bound);
10375 }
10376
10377 /* Dashboard-export header layout + Export-all CTA. */
10378 .dashboard-export-header {
10379 display: flex;
10380 justify-content: space-between;
10381 align-items: flex-start;
10382 }
10383 .export-all-btn {
10384 white-space: nowrap;
10385 margin-top: var(--gap-peer);
10386 }
10387
10388 /* Tiny inline action button (admin-metrics, dashboard-item share). */
10389 .dashboard-item-share-btn {
10390 font-size: var(--text-fine);
10391 padding: var(--gap-bound) var(--gap-peer);
10392 opacity: 0.6;
10393 }
10394
10395 /* Monospaced page sub-headers. */
10396 .section-mono-meta {
10397 font-family: var(--font-mono);
10398 font-size: var(--text-note);
10399 opacity: 0.6;
10400 margin-bottom: var(--gap-pane);
10401 }
10402 /* Email-result single-card page. */
10403 .email-result-wrap {
10404 max-width: 600px;
10405 margin: 50px auto;
10406 padding: 20px;
10407 }
10408
10409 /* Admin lottery form (admin-waitlist). */
10410 .lottery-form {
10411 display: flex;
10412 gap: var(--gap-section);
10413 align-items: flex-end;
10414 }
10415
10416 /* Login screen: "or" divider + passkey button + error slot. */
10417 .login-divider {
10418 text-align: center;
10419 margin: var(--gap-pane) 0 var(--gap-section);
10420 opacity: 0.5;
10421 font-size: var(--text-note);
10422 }
10423 /* The passkey offer is out only where the browser can do WebAuthn.
10424 `page-login-2.js` asks and states the answer; the region's own markup is the
10425 renderer's, so there is no class for a script to take off. */
10426 #passkey-login { display: none; }
10427 #passkey-login[data-supported] { display: block; }
10428
10429 .login-passkey-btn { width: 100%; }
10430 .login-passkey-error { margin-top: var(--gap-peer); }
10431
10432 /* Upload-progress sibling text (item_audio_upload). */
10433 .upload-speed-text {
10434 font-size: var(--text-fine);
10435 opacity: 0.6;
10436 margin-top: var(--gap-bound);
10437 }
10438
10439 /* Faded "Following" follow-button state. */
10440 .follow-btn.is-selected { opacity: 0.7; }
10441
10442 /* Compact alert / paragraph margin variants used 2+ times. */
10443
10444 /* Paywall trust line. */
10445 .paywall-trust {
10446 font-size: var(--text-note);
10447 opacity: 0.7;
10448 margin-top: var(--gap-section);
10449 line-height: 1.4;
10450 }
10451
10452 /* Confirm-delete: button background already covered by .danger class.
10453 The wizard step uses `.primary` and inlines the danger background; we just
10454 change the class instead. No CSS rule needed here. */
10455
10456 /* Paywall-style centered narrow form-group used inside project_paywall. */
10457 .form-group--centered-narrow {
10458 max-width: 240px;
10459 margin: var(--gap-section) auto;
10460 }
10461
10462 /* Inline-form margin-left for adjacent pills (collection.html private badge). */
10463 .badge--inline-l { margin-left: var(--gap-peer); }
10464
10465 /* ===== Team page ===== */
10466
10467 /* The whole block below this line went with `templates/pages/team.html` when
10468 `/team` moved to the description layer. What is left is the measure, which a
10469 described document carries on <main> rather than on a container div: the
10470 screen has no `.container`, exactly as `.feed-page main` found. The intro
10471 paragraph, the section spacing, the card's parts and the bio disclosure are
10472 all drawn by the design system now, off `Node::text`, `Node::section`,
10473 `Node::list` and a selective region. */
10474 .team-page main { max-width: 56rem; margin: 0 auto; }
10475
10476 /* ── SyncKit billing panel ──
10477 Per-app accordion shown inside the SyncKit dashboard tab (both user and
10478 project levels). The .synckit-billing root carries pricing constants as
10479 data-attrs that static/synckit-billing.js reads to mirror monthly_price_cents.
10480 */
10481 .synckit-billing {
10482 background: var(--surface-raised);
10483 border: 1px solid var(--border);
10484 border-radius: var(--radius-control);
10485 padding: var(--gap-peer) var(--gap-section);
10486 min-width: 220px;
10487 }
10488 .synckit-billing-summary {
10489 cursor: pointer;
10490 display: flex;
10491 align-items: center;
10492 gap: var(--gap-section);
10493 font-size: var(--text-note);
10494 }
10495 .synckit-billing-toggle { margin-left: auto; color: var(--content-muted); }
10496 .synckit-billing-status {
10497 display: inline-block;
10498 padding: 2px var(--gap-peer);
10499 border-radius: var(--radius-fine);
10500 font-size: var(--text-fine);
10501 font-weight: 600;
10502 }
10503 .synckit-billing-status--draft { background: var(--surface-sunken); }
10504 .synckit-billing-status--active { background: color-mix(in oklch, var(--success) 12%, var(--surface-page)); color: var(--success); }
10505 .synckit-billing-status--past-due { background: color-mix(in oklch, var(--warning) 14%, var(--surface-page)); color: var(--warning); }
10506 .synckit-billing-status--canceled { background: color-mix(in oklch, var(--danger) 10%, var(--surface-page)); color: var(--danger); }
10507 .synckit-billing-price { font-weight: 600; }
10508 .synckit-billing-body {
10509 padding-top: var(--gap-section);
10510 display: flex;
10511 flex-direction: column;
10512 gap: var(--gap-section);
10513 }
10514 .synckit-billing-section { display: flex; flex-direction: column; gap: var(--gap-peer); }
10515 .synckit-billing-subheading { font-size: var(--text-note); margin: 0; font-weight: 600; }
10516 .synckit-knob-row {
10517 display: flex;
10518 align-items: center;
10519 gap: var(--gap-peer);
10520 flex-wrap: wrap;
10521 }
10522 .synckit-knob-row label { min-width: 110px; }
10523 .synckit-knob-row input[type="range"] { flex: 1 1 160px; min-width: 120px; }
10524 .synckit-radio { display: inline-flex; align-items: center; gap: var(--gap-bound); min-width: 0; }
10525 .synckit-billing-summary-line {
10526 display: flex;
10527 align-items: baseline;
10528 gap: var(--gap-peer);
10529 padding-top: var(--gap-peer);
10530 border-top: 1px solid var(--border);
10531 }
10532 .synckit-price-preview { font-size: var(--text-lead); font-weight: 600; }
10533 .synckit-billing-actions { display: flex; gap: var(--gap-peer); }
10534 .synckit-billing-manage { flex-direction: row; gap: var(--gap-peer); }
10535 .synckit-billing-status-msg { font-size: var(--text-note); min-height: 1.25em; }
10536 .synckit-billing-status-msg--info { color: var(--content-muted); }
10537 .synckit-billing-status-msg--error { color: var(--danger); }
10538 .synckit-billing-period { margin: 0; }
10539 .synckit-gauge { display: flex; flex-direction: column; gap: 2px; }
10540 .synckit-gauge-label {
10541 display: flex;
10542 justify-content: space-between;
10543 font-size: var(--text-fine);
10544 }
10545 .synckit-gauge-value { color: var(--content-muted); }
10546
10547 /* ============================================================================
10548 Carousel: the widget tier's first consumer.
10549
10550 The markup is no longer written in a template. It comes from a description --
10551 `crate::quasi::widgets::carousel` says "an ordered set of pictures, called a
10552 carousel" -- and quasi-webview turns that into a region carrying
10553 `data-widget="carousel"` with one `.picture` or `.picture-img` per frame.
10554
10555 So these rules key on the widget name rather than on a hand-written class.
10556 That is what "a renderer that recognises the name draws it its own way"
10557 means on this host: the name is the hook, and the stylesheet plus the island
10558 are the browser's idea of a carousel.
10559
10560 THE FALLBACK RUNS THE OTHER WAY NOW. This used to render frame one and hide
10561 the rest, because the controls that reach them are the scripted part -- so
10562 with JS off, two of three screenshots were unreachable. Now the unenhanced
10563 page is the whole gallery, stacked in order.
10564
10565 THE CHROME IS DESCRIBED TOO, as of makeover-layout 0.23.0. The description
10566 says `Showing::One` -- an ordered set of pictures with one showing -- and
10567 quasi-webview derives prev/position/next from that, once, for every widget
10568 there will ever be. Nothing here or in any renderer matches on the name
10569 "carousel" to get it. The island stopped injecting chrome and now only binds
10570 what is already on the page; the dot strip and the overlaid arrows are gone.
10571
10572 The controls are still not shipped live to a reader with no script: makeover
10573 keeps `.showing` hidden until something sets [data-ready], so the markup no
10574 longer has to be absent to avoid lying.
10575 ========================================================================= */
10576
10577 /* Public gallery carousel sections (item + project pages). */
10578 .item-gallery,
10579 .project-gallery {
10580 margin: var(--gap-pane) 0;
10581 }
10582
10583 /* <mnw-carousel> wraps the described region rather than being it: a custom
10584 element is what the browser upgrades automatically after an htmx swap, and
10585 the description emits a plain div because it does not know this host's tag
10586 prefix. Default display for an unknown element is inline, so say otherwise. */
10587 mnw-carousel {
10588 display: block;
10589 }
10590
10591 /* The default is ONE frame, which is what a visitor with JavaScript sees, so
10592 the page is settled from first paint and nothing rearranges on load. Showing
10593 all three and collapsing them was a 141px -> 58px jump on every load that
10594 everyone with JS paid; `no-js.css`, loaded from a <noscript>, is what opens
10595 the stack out for a visitor without scripting.
10596
10597 This is the one half of the carousel that stays local, and it is host policy
10598 rather than a gap. makeover's generated sheet collapses the stack on
10599 [data-ready], which is the honest default for a renderer that cannot know
10600 whether this page has script: it ships every child and takes them away once
10601 something binds them. Trading a load-time jump for that is a decision about
10602 this site's landing page, and <noscript> is a mechanism a generated
10603 stylesheet has no way to reach.
10604
10605 No padding-bottom and no position: relative any more. Both were for chrome
10606 that is gone -- the strip that had to be reserved and the arrows that were
10607 absolutely positioned over the frame. */
10608 [data-widget="carousel"] > .showing-frame:not(.current) {
10609 /* respec-ok: the generated rule is the same claim gated on [data-ready], and
10610 what it cannot express is <noscript>. A renderer that cannot know whether a
10611 page has script has to ship every child and take them away once something
10612 binds them; this page collapses from first paint instead and lets no-js.css
10613 open it back, which is a trade about this landing page rather than about
10614 the vocabulary. */
10615 display: none;
10616 }
10617
10618 [data-widget="carousel"] .picture {
10619 margin: 0;
10620 }
10621
10622 /* Only the corner. `display`, `width`, `height` and the raised fill and bevel
10623 all come from makeover's own `.picture-img` rule, which layout.css puts on
10624 every page through crate::shell::head. Restating any of them here is a
10625 components-layer declaration quietly beating the design system, which is what the
10626 drift check in build.rs exists to catch -- and did, on the first attempt at
10627 this file. */
10628 [data-widget="carousel"] .picture-img {
10629 border-radius: var(--radius-control);
10630 }
10631
10632 /* No colour: makeover's `.picture-caption` already sets it to
10633 --content-muted, and this said the same thing one layer louder. What is
10634 local is the centring and the mono face, which are this widget's look and
10635 not a claim about what a caption is. */
10636 [data-widget="carousel"] .picture-caption {
10637 margin-top: var(--gap-group);
10638 font-family: var(--font-mono);
10639 font-size: var(--text-note);
10640 text-align: center;
10641 }
10642
10643 /* Landing showcase only: close the top edge.
10644
10645 The three landing frames are screenshots of this site, so the parchment they
10646 were shot on is the parchment they sit on, and a 1px border between two
10647 identical grounds is not an edge anyone sees.
10648
10649 THE ELEVATION IS GONE, and that is the point of the port. This carried
10650 `box-shadow: 0 4px 10px var(--elevation), 0 14px 36px var(--elevation)`,
10651 which is the defect `c0b63ea9` names: in-flow depth is a bevel and elevation
10652 is for what sits *over* the page. It survived this long because it was
10653 invisible -- a declaration in a later layer on the same class makeover
10654 sets, so it beat the design system's bevel silently. The drift check in build.rs found it
10655 the moment the frame became a described `.picture-img`.
10656
10657 So the frame is raised by makeover's bevel now, from `Depth::Raised`, and
10658 what is left here is the one thing that was never about depth: a stronger
10659 border colour, because a shadow falling downward does nothing for the top
10660 edge and the top edge is where a parchment screenshot dissolves into a
10661 parchment page. */
10662 .landing-showcase .picture-img {
10663 border-color: var(--border-strong);
10664 }
10665
10666 /* ---------------------------------------------------------------------------
10667 Enhanced. Everything below needs the island, and the island sets
10668 [data-ready] once it has upgraded and bound the controls.
10669 --------------------------------------------------------------------------- */
10670
10671 [data-widget="carousel"][data-ready]:focus-visible {
10672 outline: 2px solid var(--focus-ring);
10673 outline-offset: 3px;
10674 }
10675
10676 /* The control row is makeover's `.showing`, rendered by the description rather
10677 than injected here, and it needs no rules: it is two `.button`s and a muted
10678 readout, all three of which the generated sheet already styles. What used to
10679 sit here was `.carousel-nav`, `.carousel-prev`, `.carousel-next`,
10680 `.carousel-dots` and `.carousel-dot` -- 60 lines for chrome this host drew by
10681 hand because nothing could describe it.
10682
10683 The arrows were absolutely positioned over the frame, which Max called
10684 cluttered and which a terminal cannot honestly do at all; the dots had no
10685 form past a handful of frames, and two of the three galleries here are
10686 creator uploads of arbitrary length. Both halves of that were the same half. */
10687
10688
10689 /* ---------------------------------------------------------------------------
10690 Custom page editor (dashboard/custom_page_editor.html)
10691 Moved out of an inline <style> block per the extract-shared-CSS rule.
10692 --------------------------------------------------------------------------- */
10693 .cp-editor { max-width: 1200px; margin: 0 auto; padding: var(--gap-section); }
10694 .cp-editor-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--gap-section); flex-wrap: wrap; }
10695 .cp-editor-actions { display: flex; gap: var(--gap-section); }
10696 .cp-intro { color: var(--content-muted); margin: var(--gap-bound) 0 var(--gap-section); }
10697 /* Form beside preview. A code textarea below about 380px is not editable, so
10698 that number is the pane's, not the viewport's, and the pair stacks itself. */
10699 .cp-editor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: var(--gap-section); align-items: start; }
10700 .cp-editor-form { display: flex; flex-direction: column; gap: var(--gap-peer); }
10701 .cp-code { width: 100%; min-height: 220px; font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--text-note); line-height: 1.4; resize: vertical; }
10702 .cp-editor-buttons { display: flex; align-items: center; gap: var(--gap-section); margin-top: var(--gap-peer); }
10703 .cp-preview-pane { border: 1px solid var(--border); border-radius: var(--radius-control); overflow: hidden; }
10704 .cp-preview-bar { font-size: var(--text-fine); padding: var(--gap-bound) var(--gap-group); background: var(--surface-sunken); border-bottom: 1px solid var(--border); }
10705 .cp-preview { width: 100%; height: 520px; border: 0; background: var(--primary-light); display: block; }
10706 .cp-blocked { margin-top: var(--gap-section); }
10707 .cp-blocked-title { font-weight: 600; margin-bottom: var(--gap-peer); }
10708 .cp-blocked-empty { color: var(--content-muted); }
10709 .cp-blocked-list { list-style: none; padding: 0; margin: 0; }
10710 .cp-blocked-list li { padding: var(--gap-peer) 0; border-bottom: 1px solid var(--border); font-size: var(--text-note); display: flex; gap: var(--gap-peer); flex-wrap: wrap; align-items: baseline; }
10711 .cp-blocked-kind { font-weight: 600; }
10712 .cp-blocked-loc { color: var(--content-muted); }
10713 .cp-blocked-val { background: var(--surface-sunken); padding: 0 var(--gap-bound); }
10714 .cp-status.cp-ok { color: var(--success); }
10715 .cp-status.cp-err { color: var(--warning); }
10716 .cp-reset { margin-top: var(--gap-pane); }
10717 .cp-locked-banner { margin: var(--gap-peer) 0 var(--gap-section); padding: var(--gap-section); border: 1px solid var(--warning); border-radius: var(--radius-control); background: color-mix(in oklch, var(--warning) 12%, var(--surface-page)); }
10718 /* Layout-shift fix (audit Run 17 Frontend): reserve space for a cover image
10719 whose class didn't already set an aspect-ratio / fixed size. */
10720 .proj-image-preview img { width: 120px; height: 120px; object-fit: cover; }
10721
10722 /* ---- Discover search: how well a result matched -------------------------
10723 Two surfaces, doing two different jobs (db/discover.rs, migration 179).
10724
10725 .results-tier-break is the single heading, and it sits exactly where the
10726 claim it makes turns true: above it every row holds at least one word the
10727 searcher typed, below it none of them do. .row-match-note is the per-row
10728 half, "Matched 3 of 5 words", because coverage varies row to row and one
10729 page can hold a 4-of-5 beside a 1-of-5.
10730
10731 Both are deliberately quiet. They explain a result the reader is already
10732 looking at; competing with the title would make search feel like it is
10733 apologising. Muted colour and small type, no accent, no border of their own
10734 beyond the rule that separates the tiers. */
10735 .results-tier-break {
10736 display: flex;
10737 align-items: baseline;
10738 gap: var(--gap-peer);
10739 flex-wrap: wrap;
10740 padding: var(--gap-section) var(--gap-section) var(--gap-peer);
10741 border-top: 1px solid var(--border);
10742 background: var(--surface-page);
10743 }
10744
10745 /* .results-table sets border-top: none and lets .table-row draw the rules, so
10746 the break supplies its own top border. Mid-list that sits against the
10747 preceding row's border-bottom and reads as one heavier 2px rule, which is
10748 wanted here: it is a section change, not another row. At the top of an
10749 all-fuzzy page there is no preceding row, and the same border supplies the
10750 table's missing top edge. */
10751 .results-tier-label {
10752 font-family: var(--font-display);
10753 font-weight: bold;
10754 font-size: var(--text-fine);
10755 text-transform: uppercase;
10756 letter-spacing: 0.03em;
10757 color: var(--content);
10758 }
10759
10760 .results-tier-hint {
10761 font-size: var(--text-fine);
10762 color: var(--content-muted);
10763 }
10764
10765 /* The grid is `repeat(auto-fill, minmax(280px, 1fr))`, so without this the
10766 heading takes one card's slot and the tiers interleave visually. */
10767 .results-container.results-grid .results-tier-break {
10768 grid-column: 1 / -1;
10769 padding-left: 0;
10770 padding-right: 0;
10771 background: none;
10772 }
10773
10774 /* In list view this rides inside .row-info, under the creator line, not as
10775 another child of .table-row-link: that is a fixed five-column grid and a
10776 sixth child would invent a column and shift every row that carried one. */
10777 .row-match-note {
10778 font-family: var(--font-mono);
10779 font-size: var(--text-fine);
10780 color: var(--content-muted);
10781 white-space: nowrap;
10782 overflow: hidden;
10783 text-overflow: ellipsis;
10784 }
10785
10786 /* Grid cards have room to breathe; give the note its own line under the meta. */
10787 .grid-card .row-match-note {
10788 display: block;
10789 margin-top: var(--gap-bound);
10790 }
10791
10792 /* The narrow list layouts drop columns rather than wrap, and .row-info is the
10793 column that survives. Keeping the note there would crowd the title on a
10794 phone, where the tier heading above still carries the important half. */
10795 @media (max-width: 599px) {
10796 .table-row .row-match-note { /* respec-ok: hiding a cell at a breakpoint. The
10797 generated sheet describes one `.table-row`; which of its children survive a
10798 narrow viewport is this page's call, not the design system's. */ display: none; }
10799 }
10800
10801
10802 /* ============================================================================
10803 LAYER HANDOFFS
10804
10805 Everything below gives a property back to @layer makeover rather than
10806 setting one. This file is in @layer components and the order statement puts
10807 it after `makeover`, so every rule here wins over the design system whatever
10808 the specificity -- including the bare `button`, `a` and `input` rules further
10809 up, which carry no class and so reach every described component the site
10810 renders. That is the whole reason this section exists: without it a described
10811 act takes this file's chrome instead of the one makeover drew for it, and the
10812 difference is invisible in the source of either file.
10813
10814 `revert-layer` is the deferral. It names no value, so the design system can
10815 change what a chip looks like without this file learning about it.
10816
10817 Two rules govern how these are written, and both are load-bearing:
10818
10819 - Hand a property back on the arms makeover actually paints, not on the
10820 plain class. Makeover gives a chip a colour only when it is disabled, so
10821 `.chip { color: revert-layer }` would roll a live chip back past this
10822 file's `button { color }` to the UA default rather than to anything
10823 makeover chose.
10824 - Outrank the rule being deferred past. Both rules are this file's and both
10825 are in the same layer, so that contest is decided by specificity and then
10826 by source order. `.field` does not beat `input[type="text"]`, which is
10827 why the field handoffs are element-qualified, and this section sits at
10828 the end of the file so an equal-specificity handoff still wins.
10829
10830 makeover-build's check_vocabulary reads both halves and fails the build on a
10831 pair that has neither a handoff nor a reviewed exception, so this section
10832 cannot fall behind the design system without somebody being told.
10833 ============================================================================ */
10834
10835 /* The surfaces a bare `button` was taking. `.card`, `.tab` and the two facet
10836 controls are all rendered as buttons by the described screens. */
10837 .card,
10838 .tab,
10839 .facet-take,
10840 .facet-prune {
10841 /* respec-ok: a layer handoff, not a value. */
10842 background: revert-layer;
10843 box-shadow: revert-layer;
10844 }
10845
10846 .card:hover,
10847 .tab:hover,
10848 .facet-take:hover,
10849 .facet-prune:hover {
10850 /* respec-ok: a layer handoff, not a value. */
10851 background: revert-layer;
10852 }
10853
10854 .card:active,
10855 .tab:active,
10856 .facet-take:active,
10857 .facet-prune:active {
10858 /* respec-ok: a layer handoff, not a value. */
10859 background: revert-layer;
10860 box-shadow: revert-layer;
10861 }
10862
10863 /* A facet control's colour is makeover's in every state: muted when disabled,
10864 secondary when the value it names is not standing, inherited otherwise.
10865 There is no arm of it this file has an opinion about. */
10866 .facet-take,
10867 .facet-prune {
10868 /* respec-ok: a layer handoff, not a value. */
10869 color: revert-layer;
10870 }
10871
10872 /* The disabled arm. Makeover paints all four properties here -- muted text, the
10873 not-allowed cursor, and the surface it keeps rather than dropping -- and this
10874 file's `button:disabled` was taking every one of them. */
10875 .button:disabled,
10876 .button[aria-disabled="true"],
10877 .card:disabled,
10878 .card[aria-disabled="true"],
10879 .chip:disabled,
10880 .chip[aria-disabled="true"],
10881 .tab:disabled,
10882 .tab[aria-disabled="true"],
10883 .segment:disabled,
10884 .segment[aria-disabled="true"],
10885 .toggle:disabled,
10886 .toggle[aria-disabled="true"],
10887 .field:disabled,
10888 .field[aria-disabled="true"],
10889 .facet-take:disabled,
10890 .facet-take[aria-disabled="true"],
10891 .facet-prune:disabled,
10892 .facet-prune[aria-disabled="true"] {
10893 /* respec-ok: a layer handoff, not a value. */
10894 background: revert-layer;
10895 box-shadow: revert-layer;
10896 color: revert-layer;
10897 cursor: revert-layer;
10898 }
10899
10900 /* The focus ring. Makeover insets a field's so it does not collide with the
10901 bevel and leaves every other one outside, and this file's `:focus-visible`
10902 rules were flattening the distinction on every described control. */
10903 .button:focus-visible,
10904 .card:focus-visible,
10905 .chip:focus-visible,
10906 .link:focus-visible,
10907 .tab:focus-visible,
10908 .segment:focus-visible,
10909 .toggle:focus-visible,
10910 .facet-take:focus-visible,
10911 .facet-prune:focus-visible {
10912 /* respec-ok: a layer handoff, not a value. */
10913 outline: revert-layer;
10914 outline-offset: revert-layer;
10915 }
10916
10917 /* A described field is an `<input>`, a `<select>` or a `<textarea>`, and the
10918 rules for those three carry an attribute, so the handoff has to as well: a
10919 bare `.field` is the weaker selector and loses inside this layer. The `:focus`
10920 arm is here because this file sets `outline: none` on it, which removes the
10921 ring makeover draws on `:focus-visible` rather than replacing it. */
10922 input.field,
10923 select.field,
10924 textarea.field,
10925 input.field:hover,
10926 select.field:hover,
10927 textarea.field:hover,
10928 input.field:active,
10929 select.field:active,
10930 textarea.field:active {
10931 /* respec-ok: a layer handoff, not a value. */
10932 background: revert-layer;
10933 box-shadow: revert-layer;
10934 }
10935
10936 input.field:focus,
10937 select.field:focus,
10938 textarea.field:focus,
10939 input.field:focus-visible,
10940 select.field:focus-visible,
10941 textarea.field:focus-visible {
10942 /* respec-ok: a layer handoff, not a value. */
10943 outline: revert-layer;
10944 outline-offset: revert-layer;
10945 }
10946
10947 /* A described toggle is an `<input>` too. The hover and active arms are here
10948 for the same reason as the field's: this file dresses `input[type="submit"]`
10949 as a raised button in every state, and a toggle is an input. */
10950 input.toggle,
10951 input.toggle:hover,
10952 input.toggle:active,
10953 input.toggle:focus,
10954 input.toggle:focus-visible {
10955 /* respec-ok: a layer handoff, not a value. */
10956 background: revert-layer;
10957 box-shadow: revert-layer;
10958 outline: revert-layer;
10959 outline-offset: revert-layer;
10960 }
10961
10962 /* A described link on hover. `.link` above hands the resting colour back; this
10963 file's `a:hover` is the more specific of the two and was taking the hover
10964 colour and the underline with it. */
10965 .link:hover {
10966 /* respec-ok: a layer handoff, not a value. */
10967 color: revert-layer;
10968 text-decoration: revert-layer;
10969 }
10970
10971 /* Text makeover mutes: a facet's name, and the line a placeholder shows when
10972 there is nothing to list. This file's bare `p` rule was giving both the body
10973 colour. */
10974 .facet-name,
10975 .placeholder-text {
10976 /* respec-ok: a layer handoff, not a value. */
10977 color: revert-layer;
10978 }
10979 }
10980
10981 /* ===========================================
10982 DESCRIBED MODAL (quasi RegionKind::Modal)
10983 ===========================================
10984 The first described modal on this host is the media picker; these two rules
10985 are what "modal" means here, and every one after it gets them for free. The
10986 description says the region IS a modal and says nothing about where it sits,
10987 which is the division the vocabulary draws: `role="dialog" aria-modal="true"`
10988 comes off the renderer, and a dialog that lays out in flow would make that
10989 attribute a lie.
10990
10991 Two boxes because a scrim and a panel are two boxes: the modal region covers
10992 the viewport and takes the presses the page behind it must not get, and the
10993 region inside it is the panel. */
10994 .region.modal {
10995 position: fixed;
10996 inset: 0;
10997 z-index: var(--z-overlay);
10998 display: flex;
10999 align-items: center;
11000 justify-content: center;
11001 background: var(--overlay);
11002 }
11003
11004 .region.modal > .region {
11005 position: relative;
11006 z-index: var(--z-modal);
11007 width: 90%;
11008 max-width: 700px;
11009 max-height: 80vh;
11010 overflow-y: auto;
11011 padding: var(--gap-section);
11012 background: var(--surface-overlay);
11013 box-shadow: var(--elevation-overlay);
11014 }
11015
11016 /* ===========================================
11017 MEDIA PICKER (crate::quasi::media_picker)
11018 ===========================================
11019 Addressed by the widget names the description carries, not by ids: a document
11020 holds one picker per markdown editor and their ids differ by destination,
11021 while what they are drawn like is the same assembly every time. */
11022 [data-widget="media-picker-grid"] {
11023 display: grid;
11024 grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
11025 gap: var(--gap-peer);
11026 margin-top: var(--gap-section);
11027 }
11028
11029 /* The tile IS the control, since quasi 0.69.0 (`Act::shows`). It was a
11030 `media-tile` region holding a picture and a button as siblings, and only the
11031 button answered a press; a reader aiming at the thumbnail hit nothing. The
11032 widget name went with the region, so the tile is addressed as the button
11033 inside the grid. */
11034 [data-widget="media-picker-grid"] .button {
11035 display: flex;
11036 flex-direction: column;
11037 gap: var(--gap-peer);
11038 min-width: 0;
11039 /* The file name wraps rather than pushing the tile wide. */
11040 overflow-wrap: anywhere;
11041 text-align: left;
11042 font-size: var(--text-note);
11043 }
11044
11045 /* A cap, not a height, and never a bare `img`. The design system already sets
11046 the picture's width, height and background, and an app rule taking any of
11047 those wins over `@layer makeover` silently — which is what `makeover-build`'s
11048 drift check refuses, and it is right to. `max-height` is not one of them, and
11049 it is all this needs: `Fit::Contain` is in the description, so a capped
11050 picture letterboxes inside the cap rather than being cropped or stretched. */
11051 [data-widget="media-picker-grid"] .picture-img {
11052 max-height: 80px;
11053 }
11054