max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01EEmeiSJnmyL98QzA5Dwsvz
25 files changed,
+10032 insertions,
-19 deletions
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | use std::fmt::Write as _; | |
| 3 | 3 | use std::hash::{Hash, Hasher}; | |
| 4 | 4 | use std::process::Command; | |
| 5 | - | use std::{fs, path::Path}; | |
| 5 | + | use std::{fs, path::Path, path::PathBuf}; | |
| 6 | 6 | ||
| 7 | 7 | fn main() { | |
| 8 | 8 | // Set GIT_HASH env var for compile-time inclusion via option_env!() | |
| @@ -114,6 +114,8 @@ | |||
| 114 | 114 | ||
| 115 | 115 | cut_house_faces(); | |
| 116 | 116 | ||
| 117 | + | concat_site_sheet(); | |
| 118 | + | ||
| 117 | 119 | println!("cargo::rerun-if-changed=build.rs"); | |
| 118 | 120 | ||
| 119 | 121 | // The landing shots' own dimensions, so the description can reserve their | |
| @@ -587,6 +589,53 @@ | |||
| 587 | 589 | } | |
| 588 | 590 | } | |
| 589 | 591 | ||
| 592 | + | /// Join `css/*.css` into `static/style.css`. | |
| 593 | + | /// | |
| 594 | + | /// The parts are contiguous line ranges of what the sheet already was, loaded | |
| 595 | + | /// in name order, so the join is byte-identical and the split cannot change one | |
| 596 | + | /// pixel. `00-preamble.css` is the comment header and holds no rules; the | |
| 597 | + | /// `@layer components` opener and closer are emitted here rather than living in | |
| 598 | + | /// a part, so every part on disk is standalone-valid CSS. | |
| 599 | + | /// | |
| 600 | + | /// The output stays committed. It is what the head links, what | |
| 601 | + | /// `lint-frontend.sh` reads, what `frontend_payload` weighs and what | |
| 602 | + | /// `discover.rs` greps, and 11,000 lines of annotated CSS keep their blame. | |
| 603 | + | /// `lint-frontend.sh` rebuilds this join and diffs it, which closes the one | |
| 604 | + | /// door a committed generated file leaves open. | |
| 605 | + | fn concat_site_sheet() { | |
| 606 | + | let mut parts: Vec<PathBuf> = fs::read_dir("css") | |
| 607 | + | .expect("read css/") | |
| 608 | + | .map(|e| e.expect("css/ entry").path()) | |
| 609 | + | .filter(|p| p.extension().is_some_and(|e| e == "css")) | |
| 610 | + | .collect(); | |
| 611 | + | parts.sort(); | |
| 612 | + | println!("cargo::rerun-if-changed=css"); | |
| 613 | + | for part in &parts { | |
| 614 | + | println!("cargo::rerun-if-changed={}", part.display()); | |
| 615 | + | } | |
| 616 | + | ||
| 617 | + | let (preamble, rest) = parts.split_first().expect("css/ holds no parts"); | |
| 618 | + | assert!( | |
| 619 | + | preamble.file_name().is_some_and(|n| n == "00-preamble.css"), | |
| 620 | + | "css/ must sort with 00-preamble.css first, found {}", | |
| 621 | + | preamble.display() | |
| 622 | + | ); | |
| 623 | + | ||
| 624 | + | let mut out = fs::read_to_string(preamble).expect("read css/00-preamble.css"); | |
| 625 | + | out.push_str("@layer components {\n"); | |
| 626 | + | for part in rest { | |
| 627 | + | out.push_str( | |
| 628 | + | &fs::read_to_string(part).unwrap_or_else(|e| panic!("read {}: {e}", part.display())), | |
| 629 | + | ); | |
| 630 | + | } | |
| 631 | + | out.push_str("}\n"); | |
| 632 | + | ||
| 633 | + | // `write_if_changed` for the reason the JS block in `main` documents: this | |
| 634 | + | // is a file the script both writes and watches, and an unconditional write | |
| 635 | + | // rebuilds the crate on every cargo invocation forever. | |
| 636 | + | write_if_changed(Path::new("static/style.css"), &out); | |
| 637 | + | } | |
| 638 | + | ||
| 590 | 639 | /// Compile the TypeScript frontend (`frontend/`) to browser ESM in | |
| 591 | 640 | /// `static/dist/` via `npm run build` (which runs `tsc`). | |
| 592 | 641 | /// |
| @@ -2,11 +2,11 @@ | |||
| 2 | 2 | ||
| 3 | 3 | The MNW UI is composed from a small fixed set of primitives. Every screen is assembled from these, like an OS rendering applications, not like a website where each page reinvents its own widgets. If a screen needs something not in this charter, the answer is to extend the charter, not to write a one-off. | |
| 4 | 4 | ||
| 5 | - | Source of truth for visual identity: the cross-project brand system (internal). Source of truth for primitives: this file plus `static/style.css`. | |
| 5 | + | Source of truth for visual identity: the cross-project brand system (internal). Source of truth for primitives: this file plus `css/05-core.css`. | |
| 6 | 6 | ||
| 7 | 7 | ## Tokens | |
| 8 | 8 | ||
| 9 | - | Every visual value is a token defined in `:root` (`static/style.css`). No raw hex, no off-scale spacing, no bespoke shadow. | |
| 9 | + | Every visual value is a token defined in `:root` (`css/05-core.css`). No raw hex, no off-scale spacing, no bespoke shadow. | |
| 10 | 10 | ||
| 11 | 11 | | Tier | Token group | Where defined | | |
| 12 | 12 | |---|---|---| | |
| @@ -122,7 +122,7 @@ | |||
| 122 | 122 | | Breadcrumb | `.breadcrumb` | — | — | | |
| 123 | 123 | | Pagination | `_ui.html` macro `pagination` → `.pagination` | — | `.active` | | |
| 124 | 124 | ||
| 125 | - | Tokens live in `static/style.css` `:root`. The four parameterized macros (`empty_state`, `form_field`, `confirm_dialog`, `pagination`) are in `templates/partials/_ui.html`. | |
| 125 | + | Tokens live in `css/05-core.css` `:root`. The four parameterized macros (`empty_state`, `form_field`, `confirm_dialog`, `pagination`) are in `templates/partials/_ui.html`. | |
| 126 | 126 | ||
| 127 | 127 | ### Composition guide for new features | |
| 128 | 128 |
| @@ -10,10 +10,10 @@ | |||
| 10 | 10 | ||
| 11 | 11 | | Group | Tokens | File | | |
| 12 | 12 | |-------|--------|------| | |
| 13 | - | | Surfaces | `--surface-page`, `--surface-raised`, `--surface-sunken`, `--surface-overlay`, `--surface-well`, `--bevel-light`, `--bevel-dark` | `static/style.css` `:root` | | |
| 14 | - | | Content | `--content`, `--content-secondary`, `--content-muted`, `--content-on-action` | `static/style.css` `:root` | | |
| 15 | - | | Action and status | `--action`, `--action-hover`, `--success`, `--warning`, `--danger`, `--info` | `static/style.css` `:root` | | |
| 16 | - | | Lines and focus | `--border`, `--border-strong`, `--focus-ring`, `--hover-surface`, `--overlay` | `static/style.css` `:root` | | |
| 13 | + | | Surfaces | `--surface-page`, `--surface-raised`, `--surface-sunken`, `--surface-overlay`, `--surface-well`, `--bevel-light`, `--bevel-dark` | `css/05-core.css` `:root` | | |
| 14 | + | | Content | `--content`, `--content-secondary`, `--content-muted`, `--content-on-action` | `css/05-core.css` `:root` | | |
| 15 | + | | Action and status | `--action`, `--action-hover`, `--success`, `--warning`, `--danger`, `--info` | `css/05-core.css` `:root` | | |
| 16 | + | | Lines and focus | `--border`, `--border-strong`, `--focus-ring`, `--hover-surface`, `--overlay` | `css/05-core.css` `:root` | | |
| 17 | 17 | | Typeface | `--font-mono`, `--font-sans`, `--font-display` | `static/typography.css` (generated) | | |
| 18 | 18 | | Type size, spacing, radius | `--text-*`, `--gap-*`, `--radius-*` | `static/geometry.css` (generated) | | |
| 19 | 19 | ||
| @@ -260,7 +260,7 @@ | |||
| 260 | 260 | ||
| 261 | 261 | | What | Where | | |
| 262 | 262 | |------|-------| | |
| 263 | - | | CSS | `static/style.css`, `static/layout.css` | | |
| 263 | + | | CSS | `css/*.css` (joined into `static/style.css`), `static/layout.css` | | |
| 264 | 264 | | Generated tokens | `static/typography.css`, `static/geometry.css`, `static/timing.css` | | |
| 265 | 265 | | Document head and script tail | `src/shell.rs` | | |
| 266 | 266 | | Base template | `templates/base.html` | |
| @@ -25,6 +25,42 @@ | |||
| 25 | 25 | ||
| 26 | 26 | violations=0 | |
| 27 | 27 | ||
| 28 | + | # 0a. Every sheet the rules below read must exist. | |
| 29 | + | # | |
| 30 | + | # A missing generated sheet makes rules 1 and 2 grep nothing and pass, | |
| 31 | + | # which is exactly the silence they exist to break. Fail loudly instead. | |
| 32 | + | missing="" | |
| 33 | + | for sheet in $SITE_SHEETS $GENERATED_SHEETS; do | |
| 34 | + | [ -f "$sheet" ] || missing="$missing $sheet"$'\n' | |
| 35 | + | done | |
| 36 | + | if [ -n "$missing" ]; then | |
| 37 | + | echo "[0a] stylesheets missing; run 'cargo build' to generate them:" | |
| 38 | + | printf '%s' "$missing" | |
| 39 | + | exit 1 | |
| 40 | + | fi | |
| 41 | + | ||
| 42 | + | # 0b. static/style.css is generated by build.rs from css/*.css and committed. | |
| 43 | + | # | |
| 44 | + | # Committed so 11,000 lines of annotated CSS keep their blame and so every | |
| 45 | + | # guard, test and doc path that reads the built sheet keeps working. That | |
| 46 | + | # leaves one door open: editing the output instead of a part. This closes | |
| 47 | + | # it by rebuilding the join here and diffing. | |
| 48 | + | built=$( | |
| 49 | + | cat "$ROOT/css/00-preamble.css" | |
| 50 | + | echo "@layer components {" | |
| 51 | + | for part in "$ROOT"/css/*.css; do | |
| 52 | + | [ "$(basename "$part")" = "00-preamble.css" ] && continue | |
| 53 | + | cat "$part" | |
| 54 | + | done | |
| 55 | + | echo "}" | |
| 56 | + | ) | |
| 57 | + | if ! printf '%s\n' "$built" | diff -q - "$STATIC/style.css" >/dev/null; then | |
| 58 | + | echo "[0b] static/style.css does not match css/*.css." | |
| 59 | + | echo " It is generated: edit the part, not the output, then 'cargo build'." | |
| 60 | + | printf '%s\n' "$built" | diff - "$STATIC/style.css" | head -20 | |
| 61 | + | exit 1 | |
| 62 | + | fi | |
| 63 | + | ||
| 28 | 64 | report() { | |
| 29 | 65 | local rule="$1"; shift | |
| 30 | 66 | local msg="$1"; shift |
| @@ -1,6 +1,12 @@ | |||
| 1 | 1 | /* ============================================================================ | |
| 2 | 2 | MNW global stylesheet. Charter: docs/design-system.md. | |
| 3 | 3 | ||
| 4 | + | THIS FILE IS ONE PART OF THE SHEET, AND HOLDS NO RULES. | |
| 5 | + | ||
| 6 | + | The sheet is written as `css/*.css` and joined by build.rs into | |
| 7 | + | `static/style.css`. Edit a part, never the output. What each part holds, and | |
| 8 | + | why contiguity is the guarantee: css/README.md. | |
| 9 | + | ||
| 4 | 10 | Build new features by COMPOSING these primitives, in this order of preference: | |
| 5 | 11 | ||
| 6 | 12 | 1. Use a UTILITY class for one-off adjustments (.mb-section, .text-sm, .nowrap) | |
| @@ -18,8 +24,7 @@ | |||
| 18 | 24 | -------------------------------------------------------------------------- | |
| 19 | 25 | FILE STRUCTURE (search by section name for anchor) | |
| 20 | 26 | ||
| 21 | - | FOUNDATIONS | |
| 22 | - | @font-face declarations (top of file) | |
| 27 | + | FOUNDATIONS (05-core.css) | |
| 23 | 28 | :root tokens (color, type, space, radius, shadow) | |
| 24 | 29 | Element base (h1/h2/h3, p, a, button, input, table) | |
| 25 | 30 | ||
| @@ -118,11 +123,11 @@ | |||
| 118 | 123 | same layer, and they still beat this file on a tie because a layer resolves | |
| 119 | 124 | internally by source order and they load later. | |
| 120 | 125 | ||
| 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 | + | One thing stays OUT of the layer, deliberately: the theme block theming.rs | |
| 127 | + | injects into <head> is unlayered, which is load-bearing. An unlayered rule | |
| 128 | + | outranks every layer, so a creator's chosen theme keeps overriding this | |
| 129 | + | sheet's :root without needing to know the order. (The @font-face rules this | |
| 130 | + | used to name as the second exception left for typography.css.) */ | |
| 126 | 131 | @layer components { | |
| 127 | 132 | :root { | |
| 128 | 133 | /* Typography: nothing. All three tokens come from typography.css, which |
| @@ -52,7 +52,7 @@ | |||
| 52 | 52 | /// against. | |
| 53 | 53 | /// | |
| 54 | 54 | /// Lower it whenever the total falls. Raising it is a decision, not a fix. | |
| 55 | - | const CSS_KIB_HIGH_WATER: usize = 337; | |
| 55 | + | const CSS_KIB_HIGH_WATER: usize = 336; | |
| 56 | 56 | ||
| 57 | 57 | /// The sheets this repo writes by hand. | |
| 58 | 58 | /// |
| @@ -275,7 +275,7 @@ | |||
| 275 | 275 | /// the renderer's. | |
| 276 | 276 | /// | |
| 277 | 277 | /// The scope-to-class mapping moved with it. It lived in | |
| 278 | - | /// `static/style.css`, matching syntect's emitted class names against seven | |
| 278 | + | /// the site stylesheet, matching syntect's emitted class names against seven | |
| 279 | 279 | /// custom properties; it is [`syntax_of`] now, and the stylesheet keys off | |
| 280 | 280 | /// what quasi-webview writes. | |
| 281 | 281 | #[must_use] |
| @@ -49,7 +49,7 @@ | |||
| 49 | 49 | /// | |
| 50 | 50 | /// A markdown editor is one surface, so the sizes are named for the room they | |
| 51 | 51 | /// are given rather than for the screen that gives it. The classes are matched | |
| 52 | - | /// in `static/style.css`, under `RICH FIELD`. | |
| 52 | + | /// in `css/60-interaction.css`, under `RICH FIELD`. | |
| 53 | 53 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 54 | 54 | pub enum Height { | |
| 55 | 55 | /// A few lines, for an editor behind a disclosure the reader opened to add |
| @@ -1,0 +1,130 @@ | |||
| 1 | + | /* ============================================================================ | |
| 2 | + | MNW global stylesheet. Charter: docs/design-system.md. | |
| 3 | + | ||
| 4 | + | THIS FILE IS ONE PART OF THE SHEET, AND HOLDS NO RULES. | |
| 5 | + | ||
| 6 | + | The sheet is written as `css/*.css` and joined by build.rs into | |
| 7 | + | `static/style.css`. Edit a part, never the output. What each part holds, and | |
| 8 | + | why contiguity is the guarantee: css/README.md. | |
| 9 | + | ||
| 10 | + | Build new features by COMPOSING these primitives, in this order of preference: | |
| 11 | + | ||
| 12 | + | 1. Use a UTILITY class for one-off adjustments (.mb-section, .text-sm, .nowrap) | |
| 13 | + | 2. Use a LAYOUT PRIMITIVE to position content (.container, .stack-row, .field-row, .list-row) | |
| 14 | + | 3. Use a COMPONENT PRIMITIVE for a UI element (.card, .callout, .badge, .progress-bar, .empty-state, .input--sm, .small) | |
| 15 | + | 4. Extend a primitive with a modifier (.card--bordered, .callout--warning, .stack-row--bordered) | |
| 16 | + | 5. ONLY THEN consider a new class, and add it to the right section below | |
| 17 | + | ||
| 18 | + | A new class is a smell, not a goal. Before writing one: | |
| 19 | + | - grep this file for the visual shape you want; almost everything is here | |
| 20 | + | - if you can't find it, consider whether the shape belongs in the design system | |
| 21 | + | (then add it to the appropriate section here AND to docs/design-system.md) | |
| 22 | + | - page-scoped rules (`.foo-page .bar`) are a last resort, not a first move | |
| 23 | + | ||
| 24 | + | -------------------------------------------------------------------------- | |
| 25 | + | FILE STRUCTURE (search by section name for anchor) | |
| 26 | + | ||
| 27 | + | FOUNDATIONS (05-core.css) | |
| 28 | + | :root tokens (color, type, space, radius, shadow) | |
| 29 | + | Element base (h1/h2/h3, p, a, button, input, table) | |
| 30 | + | ||
| 31 | + | UTILITIES | |
| 32 | + | Spacing (.m-0, .mt-*, .mb-*, .ml-*, .my-*) | |
| 33 | + | Sizing (.col-*, .w-*, .maxw-*, .minw-*) | |
| 34 | + | Text (.text-sm, .text-xs, .text-center, .text-right, .fw-bold, | |
| 35 | + | .nowrap, .muted, .dimmed, .dimmer, .is-faded, .danger-text) | |
| 36 | + | State (.hidden, .is-selected, .is-active, .unstyled-link) | |
| 37 | + | Shapes (.square-cover, .scroll-x) | |
| 38 | + | ||
| 39 | + | LAYOUT PRIMITIVES (page-scaffolding shapes) | |
| 40 | + | .container (width set by body class) | |
| 41 | + | .stack-row + --bordered / --tight / --top (toolbar / header bar) | |
| 42 | + | .field-row + .form-group.is-grow (inline form row) | |
| 43 | + | .list-row + .list-row-title (vertical item list) | |
| 44 | + | .form-row (2-col grid form) | |
| 45 | + | .cover-row + .cover-thumb + .cover-empty (image picker) | |
| 46 | + | ||
| 47 | + | COMPONENT PRIMITIVES (reusable UI blocks) | |
| 48 | + | .content-section (light-bg page section box) | |
| 49 | + | .card / .card-muted / .card--bordered / .card--selectable | |
| 50 | + | .form-group / .form-section / details.form-section | |
| 51 | + | .section-header / .section-lead / .section-group-label | |
| 52 | + | .badge + data-tone (success/warning/danger) | |
| 53 | + | .callout + --danger / --warning / --solid-warning | |
| 54 | + | .banner + --info / --warning (full-bleed page-top notice) | |
| 55 | + | .alert + -note / -tip / -warning / -caution (left-border inline notice) | |
| 56 | + | .modal + .modal-overlay | |
| 57 | + | .empty-state + --compact / --chart / --lg | |
| 58 | + | .progress-bar-container + .progress-bar (--slim, --rounded, --highlight) | |
| 59 | + | .upload-status + -row + -msg.is-success / .is-error | |
| 60 | + | .field-status / .save-status + .success / .error / .saving | |
| 61 | + | .toast + .toast--success / --error / --warning | |
| 62 | + | .breadcrumb / .pagination | |
| 63 | + | .tabs + .tab.chosen (chosen is makeover's) | |
| 64 | + | ||
| 65 | + | CONTROL MODIFIERS (compose onto buttons/inputs) | |
| 66 | + | .small / .btn-compact / .btn-link / .btn-tiny (button sizes) | |
| 67 | + | .btn-danger / .saved (button intents) | |
| 68 | + | .input--xs / --sm / --mono / --upper / --numeric (input sizes/shapes) | |
| 69 | + | ||
| 70 | + | FEATURE SUBAPPS (their own CSS scope) | |
| 71 | + | .git-* (git source browser at /source/*) | |
| 72 | + | Pricing calculator on /pricing | |
| 73 | + | Markdown preview / docs-ui in /docs/* | |
| 74 | + | ||
| 75 | + | PAGE-SCOPED LAYOUTS (when truly page-specific) | |
| 76 | + | .item-page / .project-page / .article-page / .purchase-page / .cart-* / | |
| 77 | + | .library-page / .feed-page / .discover-page / .landing / .creators-page / | |
| 78 | + | .health-page / .import-page / .delete-account-page / .export-page / | |
| 79 | + | .receipt-page / .fan-plus-page / .stripe-disclaimer-page / .buy-page / | |
| 80 | + | .user-page / .collection-page / .changelog-page / .policy-page / | |
| 81 | + | .tag-tree-page / .project-blog-page / .project-paywall-page / | |
| 82 | + | .confirm-delete-page / .admin-page / .dashboard-page | |
| 83 | + | ||
| 84 | + | DASHBOARD TABS (one section per tab partial) | |
| 85 | + | Per-tab classes scoped under their canonical body class. Use modifier | |
| 86 | + | composition where possible; only add tab-specific rules when the shape | |
| 87 | + | genuinely diverges from a primitive. | |
| 88 | + | ||
| 89 | + | RESPONSIVE (@media at end of file) | |
| 90 | + | 768px tablet | |
| 91 | + | 480px phone | |
| 92 | + | ||
| 93 | + | -------------------------------------------------------------------------- | |
| 94 | + | When in doubt: read docs/design-system.md first. Update both files together | |
| 95 | + | when you add a primitive. | |
| 96 | + | ============================================================================ */ | |
| 97 | + | ||
| 98 | + | /* No `@font-face` here at all any more, and no font token either. All three | |
| 99 | + | faces are fetched by the generated `typography.css`: Quasi Mono and Quasi | |
| 100 | + | Body from makeover's house layer, and Young Serif from MNW's own override, | |
| 101 | + | declared in build.rs. What left first: four blocks for IBM Plex Mono and | |
| 102 | + | Lato, regular and bold, 101 KB of woff2 covering two weights, replaced by | |
| 103 | + | 81 KB carrying the whole 200-800 axis. What left second: the Young Serif | |
| 104 | + | block that used to sit here, and the `--font-display` beside it. | |
| 105 | + | ||
| 106 | + | Young Serif is still the brand tier and still MNW's alone. That has not | |
| 107 | + | changed and is not what moved. What changed is that the exemption is a | |
| 108 | + | mechanism now rather than a gap in the vocabulary: the face is declared | |
| 109 | + | once, as an override of the `display` slot, and reads as one. */ | |
| 110 | + | ||
| 111 | + | /* Everything below is in the `components` cascade layer. | |
| 112 | + | ||
| 113 | + | _head_assets.html declares the order once, `@layer makeover, base, | |
| 114 | + | components, responsive`, and until now three of those four names were | |
| 115 | + | empty: this file adopted none of them, so 11,788 unlayered lines outranked | |
| 116 | + | every named layer by construction and the generated sheets lost every | |
| 117 | + | contest they entered. | |
| 118 | + | ||
| 119 | + | Adopting `components` changes no rendering today. `components` already sits | |
| 120 | + | after `makeover` in the declared order, so this file wins the same contests | |
| 121 | + | it was winning before; what changes is that it wins them by a stated rule | |
| 122 | + | rather than by being unlayered. wizard.css and media-player.css adopt the | |
| 123 | + | same layer, and they still beat this file on a tie because a layer resolves | |
| 124 | + | internally by source order and they load later. | |
| 125 | + | ||
| 126 | + | One thing stays OUT of the layer, deliberately: the theme block theming.rs | |
| 127 | + | injects into <head> is unlayered, which is load-bearing. An unlayered rule | |
| 128 | + | outranks every layer, so a creator's chosen theme keeps overriding this | |
| 129 | + | sheet's :root without needing to know the order. (The @font-face rules this | |
| 130 | + | used to name as the second exception left for typography.css.) */ |
| @@ -1,0 +1,1644 @@ | |||
| 1 | + | :root { | |
| 2 | + | /* Typography: nothing. All three tokens come from typography.css, which | |
| 3 | + | loads before this file. | |
| 4 | + | ||
| 5 | + | `--font-mono` and `--font-sans` are the house slots and `--font-display` | |
| 6 | + | is MNW's own, an override declared in build.rs. Do not redefine any of the | |
| 7 | + | three here -- that is the divergence the unification deleted, and this | |
| 8 | + | file is the one that would win. A product face belongs in the override, | |
| 9 | + | where the whole tree can read it. */ | |
| 10 | + | ||
| 11 | + | /* -------------------------------------------------------------------- | |
| 12 | + | INTENT LAYER, the shared theme vocabulary (themeable) | |
| 13 | + | ||
| 14 | + | These are the intent tokens emitted by theme_common::intent_css_vars(): | |
| 15 | + | colors declared by ROLE (surface/content/action/status/line/category), | |
| 16 | + | plus the derived interactive states (hover/active/selection/row-stripe/ | |
| 17 | + | contrast) the crate computes once for every product. The values below | |
| 18 | + | are the platform default (shared/themes/makenotwork.toml resolved) so | |
| 19 | + | non-creator pages render without injection; creator profile/project/item | |
| 20 | + | pages inject a <style> that supersedes this block with the chosen theme. | |
| 21 | + | Generated. Keep in sync with `cargo run --example dump_intents makenotwork`. | |
| 22 | + | -------------------------------------------------------------------- */ | |
| 23 | + | --surface-page: #ddd6c9; | |
| 24 | + | --surface-raised: #efe9e0; | |
| 25 | + | --surface-sunken: #cdc5b5; | |
| 26 | + | --surface-overlay: #efe9e0; | |
| 27 | + | /* The content surface cut into a raised one, so a list reads as content in a | |
| 28 | + | container rather than as bands on a panel. Derived by makeover from | |
| 29 | + | surface-raised, away from resolved content, so it goes lighter on a light | |
| 30 | + | theme and darker on a dark one. */ | |
| 31 | + | --surface-well: #fffef5; | |
| 32 | + | /* The two edges of a bevel. makeover derives both from the theme's raised | |
| 33 | + | surface, so they arrive with the rest of the intent set through | |
| 34 | + | intent_css_vars() and every creator theme gains them automatically. */ | |
| 35 | + | --bevel-light: #fffef5; | |
| 36 | + | --bevel-dark: #b5afa6; | |
| 37 | + | --content: #3d3530; | |
| 38 | + | --content-secondary: #5c554f; | |
| 39 | + | --content-muted: #8a8480; | |
| 40 | + | --content-on-action: #ffffff; | |
| 41 | + | --action: #6c5ce7; | |
| 42 | + | --action-hover: #7a6cf8; | |
| 43 | + | --danger: #c0392b; | |
| 44 | + | --success: #27ae60; | |
| 45 | + | --warning: #f59e0b; | |
| 46 | + | --info: #17a2b8; | |
| 47 | + | --border: #b3ab97; | |
| 48 | + | --border-strong: #a39c88; | |
| 49 | + | --focus-ring: #6c5ce7; | |
| 50 | + | --hover-surface: #cdc5b5; | |
| 51 | + | --category-one: #c0392b; | |
| 52 | + | --category-two: #27ae60; | |
| 53 | + | --category-three: #6c5ce7; | |
| 54 | + | --category-four: #f59e0b; | |
| 55 | + | --category-five: #a29bfe; | |
| 56 | + | --category-six: #17a2b8; | |
| 57 | + | --overlay: rgba(3, 1, 0, 0.5); | |
| 58 | + | /* What a surface floating over the page casts onto it. Same tone as the | |
| 59 | + | scrim above and pinned near-black on every theme, because a shadow is | |
| 60 | + | absence of light rather than a tint. layout.css composes the geometry | |
| 61 | + | into --elevation-overlay; a consumer that needs a different geometry | |
| 62 | + | builds its own from this token rather than from a literal. */ | |
| 63 | + | --elevation: rgba(3, 1, 0, 0.18); | |
| 64 | + | ||
| 65 | + | /* -------------------------------------------------------------------- | |
| 66 | + | APP-LOCAL CONSTANTS, not theme colors, not derivable from intents. | |
| 67 | + | ||
| 68 | + | Component CSS references the intent tokens above directly (no brand | |
| 69 | + | aliases). Status backgrounds derive from the intents inline via | |
| 70 | + | color-mix(); these few stay fixed: | |
| 71 | + | - pure black/white contrast anchors | |
| 72 | + | - Stripe's brand color (an external constant) | |
| 73 | + | - diff add/del, a hard readability convention that must not re-theme | |
| 74 | + | -------------------------------------------------------------------- */ | |
| 75 | + | --primary-dark: #000000; | |
| 76 | + | --primary-light: #ffffff; | |
| 77 | + | --stripe: #635BFF; | |
| 78 | + | ||
| 79 | + | /* The faint accent tint every selection and highlight recipe fills with. | |
| 80 | + | Derived from --action, so it re-tints with the creator's theme. Named | |
| 81 | + | because the charter names it and because a dozen rules read it. */ | |
| 82 | + | --highlight-faint: color-mix(in oklch, var(--action) 8%, transparent); | |
| 83 | + | ||
| 84 | + | /* Diff / code review, fixed green-add / red-del convention */ | |
| 85 | + | --diff-add: #2d7d2d; | |
| 86 | + | --diff-add-bg: rgba(45, 125, 45, 0.08); | |
| 87 | + | --diff-del: #a83232; | |
| 88 | + | --diff-del-bg: rgba(168, 50, 50, 0.08); | |
| 89 | + | ||
| 90 | + | /* Radius lives in geometry.css now, generated from makeover-geometry: | |
| 91 | + | --radius-square / -fine / -control / -panel / -round. Containers are | |
| 92 | + | square and controls keep a corner, so the rounding survives only where a | |
| 93 | + | thing is meant to be pressed. */ | |
| 94 | + | ||
| 95 | + | /* Syntax highlighting for the git source browser: base16 Tomorrow, used | |
| 96 | + | whole rather than assembled colour by colour. It stays fixed across all | |
| 97 | + | 31 themes on purpose, the same argument as the diff add/del pair above: | |
| 98 | + | a reader recognises a highlighting palette, and re-tinting it per theme | |
| 99 | + | costs that recognition to gain nothing. Named so it is legible as one | |
| 100 | + | palette instead of seven hex literals scattered through the git rules. */ | |
| 101 | + | --syntax-keyword: #8959a8; | |
| 102 | + | --syntax-string: #718c00; | |
| 103 | + | --syntax-comment: #8e908c; | |
| 104 | + | --syntax-constant: #f5871f; | |
| 105 | + | --syntax-entity: #4271ae; | |
| 106 | + | --syntax-variable: #c82829; | |
| 107 | + | --syntax-support: #3e999f; | |
| 108 | + | ||
| 109 | + | /* Stacking order, named for what floats over what. Local rather than | |
| 110 | + | generated: this is a claim about MNW's own furniture, not an invariant | |
| 111 | + | like spacing. The numbers are spaced so a value can be slotted between | |
| 112 | + | two of them without renumbering, and nothing outside this block should | |
| 113 | + | write a z-index literal. Anything not listed here does not float. */ | |
| 114 | + | --z-raised: 1; /* lifted within the flow: a saved badge on a card */ | |
| 115 | + | --z-nav: 10; /* the nav panel that drops out of the mobile header */ | |
| 116 | + | --z-header: 11; /* the header's own toggle and search panel, both of | |
| 117 | + | which have to stay above the panel they open */ | |
| 118 | + | --z-dropdown: 20; /* menus and suggestion lists opened from a control */ | |
| 119 | + | --z-picker: 30; /* pickers that open over a dropdown */ | |
| 120 | + | --z-context: 40; /* right-click / overflow context menus */ | |
| 121 | + | --z-overlay: 50; /* full-page scrims */ | |
| 122 | + | --z-modal: 60; /* dialogs, above their scrim */ | |
| 123 | + | --z-toast: 70; /* transient messages, above a dialog */ | |
| 124 | + | --z-banner: 80; /* page-level interruptions, above everything */ | |
| 125 | + | --z-skip-link: 90; /* the keyboard escape hatch outranks all of it */ | |
| 126 | + | ||
| 127 | + | /* Depth, and there are only two kinds of it. | |
| 128 | + | ||
| 129 | + | A surface IN the page takes a bevel: an edge, not a distance. That is | |
| 130 | + | --shadow-raised/-card/-inset, aliasing the pair layout.css derives from | |
| 131 | + | each theme's own raised surface. --shadow-raised and --shadow-card | |
| 132 | + | collapse onto one value because an edge has no heavier and lighter | |
| 133 | + | version to pick between. | |
| 134 | + | ||
| 135 | + | A surface floating OVER the page takes --elevation-overlay, also from | |
| 136 | + | layout.css, derived from the theme's page colour. Menus, toasts, | |
| 137 | + | popovers, dropdowns and pickers float. A card, a plate and a framed | |
| 138 | + | image do not; they are .raised. | |
| 139 | + | ||
| 140 | + | There used to be a third kind, --shadow-1/-2/-3, a blurred scale of | |
| 141 | + | rgba-black literals picked against parchment and then applied to all 31 | |
| 142 | + | themes a creator can select. It was the same defect the bevel trio was | |
| 143 | + | rewritten to undo, one scale later, and the elevation intent is what | |
| 144 | + | replaced it. No literal survives here now, so every consumer re-themes | |
| 145 | + | without being opened. Geometry stays consumer-side: a site that needs a | |
| 146 | + | different blur composes it from var(--elevation), never from a literal. */ | |
| 147 | + | --shadow-raised: var(--bevel-raised); | |
| 148 | + | --shadow-card: var(--bevel-raised); | |
| 149 | + | --shadow-inset: var(--bevel-inset); | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | /* Reset */ | |
| 153 | + | * { | |
| 154 | + | margin: 0; | |
| 155 | + | padding: 0; | |
| 156 | + | box-sizing: border-box; | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | /* Base styles */ | |
| 160 | + | body { | |
| 161 | + | margin: 0; | |
| 162 | + | background-color: var(--surface-page); | |
| 163 | + | color: var(--content); | |
| 164 | + | font-family: var(--font-sans); | |
| 165 | + | line-height: 1.6; | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | /* Padded page layout for most content pages */ | |
| 169 | + | .padded-page { | |
| 170 | + | padding: var(--gap-pane); | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | /* Centered page layout for landing, login, signup */ | |
| 174 | + | .centered-page { | |
| 175 | + | display: flex; | |
| 176 | + | flex-direction: column; | |
| 177 | + | justify-content: center; | |
| 178 | + | align-items: center; | |
| 179 | + | min-height: 100vh; | |
| 180 | + | gap: var(--gap-page); | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | .centered-page h1 { | |
| 184 | + | font-size: var(--text-hero); | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | .message-container { | |
| 188 | + | max-width: 500px; | |
| 189 | + | text-align: center; | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | .centered-wrapper { | |
| 193 | + | justify-content: center; | |
| 194 | + | text-align: center; | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | h1 { | |
| 198 | + | font-family: var(--font-display); | |
| 199 | + | font-weight: normal; | |
| 200 | + | color: var(--content); | |
| 201 | + | text-align: center; | |
| 202 | + | font-size: var(--text-display); | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | h2, | |
| 206 | + | h3 { | |
| 207 | + | font-family: var(--font-mono); | |
| 208 | + | font-weight: normal; | |
| 209 | + | color: var(--content); | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | /* Heading classes (charter: docs/design-system.md, every h1/h2 in a | |
| 213 | + | template must carry one of these so the role is explicit). | |
| 214 | + | ||
| 215 | + | .brand-h1: the "Makenot.work" wordmark on auth/wizard pages. | |
| 216 | + | .page-title: page-level h1 (Young Serif, centered). | |
| 217 | + | .subtitle-h2: page subtitle h2 used under .brand-h1 in auth/wizards. | |
| 218 | + | .subsection-title: h2 inside dashboards, tabs, and prose partials | |
| 219 | + | (mono, no border, the default-h2 role made explicit). | |
| 220 | + | .section-header: h2 prose sub-section heading with bottom border | |
| 221 | + | (existing rule, see SECTIONS block). */ | |
| 222 | + | .brand-h1, | |
| 223 | + | .page-title { | |
| 224 | + | font-family: var(--font-display); | |
| 225 | + | font-weight: normal; | |
| 226 | + | color: var(--content); | |
| 227 | + | text-align: center; | |
| 228 | + | font-size: var(--text-display); | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | .subtitle-h2 { | |
| 232 | + | font-family: var(--font-mono); | |
| 233 | + | font-weight: normal; | |
| 234 | + | color: var(--content); | |
| 235 | + | text-align: center; | |
| 236 | + | font-size: var(--text-head); | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | .subsection-title { | |
| 240 | + | font-family: var(--font-mono); | |
| 241 | + | font-weight: normal; | |
| 242 | + | color: var(--content); | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | p { | |
| 246 | + | font-family: var(--font-sans); | |
| 247 | + | color: var(--content); | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | a { | |
| 251 | + | color: var(--content); | |
| 252 | + | text-decoration: none; | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | a:hover { | |
| 256 | + | text-decoration: underline; | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | /* The signature dot */ | |
| 260 | + | .dot { | |
| 261 | + | color: var(--action); | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | /* Container, default 1200px max-width. Modifier classes for narrower pages, | |
| 265 | + | or comma-grouped page-scoped overrides below for body-class-based control. */ | |
| 266 | + | .container { | |
| 267 | + | max-width: 1200px; | |
| 268 | + | margin: 0 auto; | |
| 269 | + | padding: var(--gap-page); | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | /* Per-page container width overrides (selected via body class). One rule | |
| 273 | + | per width tier; new pages should add their body class here rather than | |
| 274 | + | defining a new .foo-page .container rule. */ | |
| 275 | + | .delete-account-page .container, | |
| 276 | + | .receipt-page .container { max-width: 600px; margin: 0 auto; } | |
| 277 | + | ||
| 278 | + | /* The profile is described, so it has no container: the measure lands on the | |
| 279 | + | element the renderer draws the page into. The width is this page's own and | |
| 280 | + | the rest of the block was the design system's. */ | |
| 281 | + | .user-page main { max-width: 600px; } | |
| 282 | + | ||
| 283 | + | .import-page .container, | |
| 284 | + | .export-page .container { max-width: 800px; margin: 0 auto; } | |
| 285 | + | ||
| 286 | + | .fan-plus-page main, | |
| 287 | + | .creators-page main { max-width: 900px; margin: 0 auto; } | |
| 288 | + | ||
| 289 | + | /* READING COLUMN, the 680px measure every long-form surface uses. Narrower | |
| 290 | + | than .container and with tighter horizontal padding, so it is its own | |
| 291 | + | primitive rather than a .container width tier. | |
| 292 | + | ||
| 293 | + | These four wrappers were four identical rules until 2026-08-16. They are | |
| 294 | + | grouped rather than replaced by one class because the charter treats a new | |
| 295 | + | class as a smell, and because grouping keeps each selector's own | |
| 296 | + | specificity intact. A fifth long-form surface joins this list; it does not | |
| 297 | + | copy the recipe. | |
| 298 | + | ||
| 299 | + | .media-container's rule used to live in media-player.css. It is here now so | |
| 300 | + | the measure has one definition; that sheet keeps its descendant rules. */ | |
| 301 | + | .article-page .article-container, | |
| 302 | + | .doc-container, | |
| 303 | + | .docs-index, | |
| 304 | + | .media-container { | |
| 305 | + | max-width: 680px; | |
| 306 | + | margin: 0 auto; | |
| 307 | + | padding: var(--gap-page) var(--gap-pane) var(--gap-page); | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | /* =========================================== | |
| 311 | + | BUTTONS | |
| 312 | + | =========================================== */ | |
| 313 | + | ||
| 314 | + | button { | |
| 315 | + | color: var(--content); | |
| 316 | + | background: var(--surface-overlay); | |
| 317 | + | padding: 6px 12px; | |
| 318 | + | border: 1px solid var(--content); | |
| 319 | + | font-family: inherit; | |
| 320 | + | cursor: pointer; | |
| 321 | + | box-shadow: var(--shadow-raised); | |
| 322 | + | transition: box-shadow 0.1s ease, background 0.2s ease; | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | button:hover { | |
| 326 | + | background: var(--hover-surface); | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | button:active { | |
| 330 | + | box-shadow: var(--shadow-inset); | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | /* A described act carries the generated `.button` class, and the element rule | |
| 334 | + | above sets the same surface, bevel and tone the generated class does. This | |
| 335 | + | file is in @layer components, which the order statement puts after | |
| 336 | + | `makeover`, so the element rule wins whatever the specificity. Without the | |
| 337 | + | handoff below a described act renders in --surface-overlay with | |
| 338 | + | --shadow-raised and, worse, in --content rather than its tone: a | |
| 339 | + | destructive act looks identical to an ordinary one. Same mechanism and same | |
| 340 | + | remedy as `.tab.chosen` further down; read the comment there. | |
| 341 | + | ||
| 342 | + | The split is deliberate. The app keeps padding, border and font from the | |
| 343 | + | element rule, makeover keeps surface, bevel and tone. That is the | |
| 344 | + | arrangement that already makes `.field` work, where the app's input rule | |
| 345 | + | happens to set only geometry and typography. | |
| 346 | + | ||
| 347 | + | Tone is handed back on `[data-tone]` rather than on `.button`, because an | |
| 348 | + | untoned button has no colour in the layer to roll back to and would land on | |
| 349 | + | the UA default instead of --content. */ | |
| 350 | + | .button, | |
| 351 | + | .chip, | |
| 352 | + | .segment, | |
| 353 | + | .toggle { | |
| 354 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 355 | + | background: revert-layer; | |
| 356 | + | box-shadow: revert-layer; | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | .button:hover, | |
| 360 | + | .chip:hover, | |
| 361 | + | .segment:hover, | |
| 362 | + | .toggle:hover { | |
| 363 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 364 | + | background: revert-layer; | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | .button:active, | |
| 368 | + | .chip:active, | |
| 369 | + | .segment:active, | |
| 370 | + | .toggle:active { | |
| 371 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 372 | + | background: revert-layer; | |
| 373 | + | box-shadow: revert-layer; | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | .button[data-tone] { | |
| 377 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 378 | + | color: revert-layer; | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | /* A described link, same handoff on the `a` and `button` element rules. | |
| 382 | + | `a { color: var(--content); text-decoration: none }` renders a described | |
| 383 | + | link as body text -- the reader cannot tell it is a link at all, which is | |
| 384 | + | the same defect as the lost tone above and worse for being silent. | |
| 385 | + | ||
| 386 | + | `button.link` is the second arm: a link that posts is a button in the | |
| 387 | + | markup, and makeover already flattens it (`background: none`, no border, no | |
| 388 | + | padding, `font: inherit`). The element rule's chrome beat all four, so it | |
| 389 | + | rendered as an ordinary raised button. Only the properties makeover sets on | |
| 390 | + | this class are handed back; `font` is handed back on the button arm alone, | |
| 391 | + | because an `a.link` has no lower-layer font to roll back to and would land | |
| 392 | + | on the UA default face. */ | |
| 393 | + | .link { | |
| 394 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 395 | + | color: revert-layer; | |
| 396 | + | text-decoration: revert-layer; | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | button.link { | |
| 400 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 401 | + | background: revert-layer; | |
| 402 | + | border: revert-layer; | |
| 403 | + | padding: revert-layer; | |
| 404 | + | font: revert-layer; | |
| 405 | + | box-shadow: revert-layer; | |
| 406 | + | cursor: revert-layer; | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | /* A described facet value, the third arm of the same handoff. makeover 0.67.0 | |
| 410 | + | names the element-chrome reset, and a facet control withdraws the button's | |
| 411 | + | fill, stroke and bevel with it: a value reads flat until it is hovered, | |
| 412 | + | pressed or in force. The element rule above supplies all three, so without | |
| 413 | + | this a described facet renders as an ordinary raised button and | |
| 414 | + | `[aria-pressed="true"]` is its only correct state -- which is the whole of | |
| 415 | + | what the panel had to say and the one thing it could not. | |
| 416 | + | ||
| 417 | + | Wider than the `.button` group above by one property. That group keeps the | |
| 418 | + | app's border deliberately, because a described act is still a button. Here | |
| 419 | + | the flat rest look IS the withdrawal of the border, so it is handed back | |
| 420 | + | with the other two. */ | |
| 421 | + | .facet-take, | |
| 422 | + | .facet-prune { | |
| 423 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 424 | + | background: revert-layer; | |
| 425 | + | border: revert-layer; | |
| 426 | + | box-shadow: revert-layer; | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | .facet-take:hover, | |
| 430 | + | .facet-prune:hover { | |
| 431 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 432 | + | background: revert-layer; | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | .facet-take:active, | |
| 436 | + | .facet-prune:active, | |
| 437 | + | .facet-take[aria-pressed="true"], | |
| 438 | + | .facet-prune[aria-pressed="true"] { | |
| 439 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 440 | + | background: revert-layer; | |
| 441 | + | box-shadow: revert-layer; | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | /* A described field on a `select` or a `textarea`. The input rule further down | |
| 445 | + | sets padding and font only, which is why the shipped `.field` precedents | |
| 446 | + | survived; these two also set `background` and `box-shadow`, so a described | |
| 447 | + | field on either renders in --surface-sunken rather than the well. The focus | |
| 448 | + | ring is handed back with them: makeover insets it (`calc(-1 * 2px)`) so it | |
| 449 | + | does not collide with the bevel, and the app's outer 2px was winning. */ | |
| 450 | + | .field { | |
| 451 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 452 | + | background: revert-layer; | |
| 453 | + | box-shadow: revert-layer; | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | .field:focus-visible { | |
| 457 | + | /* respec-ok: a layer handoff, not a value. */ | |
| 458 | + | outline-offset: revert-layer; | |
| 459 | + | } | |
| 460 | + | ||
| 461 | + | .btn-primary { | |
| 462 | + | background: var(--primary-dark); | |
| 463 | + | color: var(--primary-light); | |
| 464 | + | border: none; | |
| 465 | + | padding: var(--gap-section) var(--gap-pane); | |
| 466 | + | font-family: inherit; | |
| 467 | + | font-size: var(--text-body); | |
| 468 | + | cursor: pointer; | |
| 469 | + | box-shadow: var(--shadow-raised); | |
| 470 | + | transition: box-shadow 0.1s ease, background 0.2s ease; | |
| 471 | + | display: inline-block; | |
| 472 | + | text-decoration: none; | |
| 473 | + | } | |
| 474 | + | ||
| 475 | + | /* A solid-fill button shifts its own fill rather than taking --hover-surface. | |
| 476 | + | The charter's hover rule is "one step lighter than the resting fill", and on | |
| 477 | + | a neutral control that step is --hover-surface; on this one it is not, since | |
| 478 | + | the label is --primary-light and a parchment fill under it is unreadable. */ | |
| 479 | + | .btn-primary:hover { | |
| 480 | + | background: color-mix(in oklch, var(--primary-dark) 85%, var(--primary-light)); | |
| 481 | + | text-decoration: none; | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | .btn-primary:active { | |
| 485 | + | box-shadow: var(--shadow-inset); | |
| 486 | + | } | |
| 487 | + | ||
| 488 | + | .btn-secondary { | |
| 489 | + | background: var(--surface-sunken); | |
| 490 | + | color: var(--content); | |
| 491 | + | border: none; | |
| 492 | + | padding: var(--gap-section) var(--gap-pane); | |
| 493 | + | font-family: var(--font-mono); | |
| 494 | + | font-size: var(--text-body); | |
| 495 | + | cursor: pointer; | |
| 496 | + | box-shadow: var(--shadow-raised); | |
| 497 | + | transition: box-shadow 0.1s ease, background 0.2s ease; | |
| 498 | + | display: inline-block; | |
| 499 | + | text-decoration: none; | |
| 500 | + | } |
Lines truncated
| @@ -1,0 +1,539 @@ | |||
| 1 | + | /* =========================================== | |
| 2 | + | ITEM PAGE | |
| 3 | + | =========================================== */ | |
| 4 | + | ||
| 5 | + | /* Item detail page (templates/pages/item.html). Scoped under .item-page | |
| 6 | + | body class so the .item-title / .item-meta / .item-price / .item-footer | |
| 7 | + | names don't collide with the store-front item-card on project.html. */ | |
| 8 | + | .item-page .item-layout { | |
| 9 | + | display: grid; | |
| 10 | + | grid-template-columns: 400px 1fr; | |
| 11 | + | gap: var(--gap-page); | |
| 12 | + | margin-bottom: var(--gap-page); | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | .item-page .item-layout.no-media { grid-template-columns: 1fr; } | |
| 16 | + | ||
| 17 | + | .item-page .item-title { | |
| 18 | + | font-size: var(--text-title); | |
| 19 | + | margin-bottom: var(--gap-peer); | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | .item-page .item-creator { | |
| 23 | + | font-size: var(--text-body); | |
| 24 | + | opacity: 0.7; | |
| 25 | + | margin-bottom: var(--gap-pane); | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | .item-page .item-creator a { color: var(--content); } | |
| 29 | + | ||
| 30 | + | .item-page .item-price { | |
| 31 | + | font-size: var(--text-display); | |
| 32 | + | margin-bottom: var(--gap-pane); | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | .item-page .item-meta { | |
| 36 | + | display: grid; | |
| 37 | + | grid-template-columns: 1fr 1fr; | |
| 38 | + | gap: var(--gap-section); | |
| 39 | + | margin-bottom: var(--gap-pane); | |
| 40 | + | padding-bottom: var(--gap-pane); | |
| 41 | + | border-bottom: 1px solid var(--border); | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | .item-page .meta-item { font-size: var(--text-note); } | |
| 45 | + | .item-page .meta-label { opacity: 0.7; margin-bottom: var(--gap-bound); } | |
| 46 | + | .item-page .meta-value { font-weight: bold; } | |
| 47 | + | ||
| 48 | + | .item-page .item-tags { margin-bottom: var(--gap-pane); } | |
| 49 | + | ||
| 50 | + | /* purchase-box uses .card-muted for the box; only its h3/ul/li specifics remain. */ | |
| 51 | + | .purchase-box h3 { | |
| 52 | + | font-size: var(--text-body); | |
| 53 | + | font-weight: normal; | |
| 54 | + | margin-bottom: var(--gap-section); | |
| 55 | + | } | |
| 56 | + | .purchase-box ul { list-style: none; margin-bottom: var(--gap-section); } | |
| 57 | + | .purchase-box li { padding: var(--gap-bound) 0; } | |
| 58 | + | .purchase-box li::before { | |
| 59 | + | content: "→ "; | |
| 60 | + | opacity: 0.5; | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | .item-page .btn-primary { | |
| 64 | + | width: 100%; | |
| 65 | + | padding: var(--gap-section) var(--gap-page); | |
| 66 | + | font-size: var(--text-lead); | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | .item-page .btn-secondary { | |
| 70 | + | width: 100%; | |
| 71 | + | margin-top: var(--gap-peer); | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | .item-page .payment-note { | |
| 75 | + | font-size: var(--text-note); | |
| 76 | + | opacity: 0.6; | |
| 77 | + | text-align: center; | |
| 78 | + | margin-top: var(--gap-section); | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | /* bundle-child uses .list-row co-class; only the gap-4 override remains. */ | |
| 82 | + | .bundle-child { gap: var(--gap-section); padding: var(--gap-section) 0; } | |
| 83 | + | ||
| 84 | + | .item-page .bundle-child-type { | |
| 85 | + | font-size: var(--text-fine); | |
| 86 | + | padding: var(--gap-bound) var(--gap-group); | |
| 87 | + | background: var(--surface-sunken); | |
| 88 | + | white-space: nowrap; | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | .item-page .bundle-child-title { flex: 1; } | |
| 92 | + | .item-page .bundle-child-title a { color: var(--content); } | |
| 93 | + | .item-page .bundle-child-price { font-size: var(--text-note); opacity: 0.7; } | |
| 94 | + | ||
| 95 | + | .item-page .bundle-notice { | |
| 96 | + | background: var(--surface-sunken); | |
| 97 | + | padding: var(--gap-pane); | |
| 98 | + | margin-bottom: var(--gap-pane); | |
| 99 | + | text-align: center; | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | .item-page .bundle-notice a { color: var(--content); } | |
| 103 | + | ||
| 104 | + | .item-page .item-description p { margin-bottom: var(--gap-section); text-align: left; } | |
| 105 | + | .item-page .item-description ul { margin-left: var(--gap-pane); margin-bottom: var(--gap-section); } | |
| 106 | + | .item-page .item-description li { margin-bottom: var(--gap-peer); } | |
| 107 | + | ||
| 108 | + | .item-page .features-grid { | |
| 109 | + | display: grid; | |
| 110 | + | grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); | |
| 111 | + | gap: var(--gap-pane); | |
| 112 | + | } | |
| 113 | + | .item-page .version-number { font-size: var(--text-lead); } | |
| 114 | + | .item-page .item-footer { | |
| 115 | + | margin-top: var(--gap-page); | |
| 116 | + | padding-top: var(--gap-page); | |
| 117 | + | border-top: 1px solid var(--border); | |
| 118 | + | text-align: center; | |
| 119 | + | opacity: 0.6; | |
| 120 | + | font-size: var(--text-note); | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | .item-page .item-footer a { color: var(--content); } | |
| 124 | + | ||
| 125 | + | @media (max-width: 839px) { | |
| 126 | + | .item-page .item-layout { grid-template-columns: 1fr; } | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | @media (max-width: 599px) { | |
| 130 | + | .item-page .item-title { font-size: var(--text-head); } | |
| 131 | + | .item-page .item-price { font-size: var(--text-title); } | |
| 132 | + | .item-page .item-meta { grid-template-columns: 1fr; gap: var(--gap-peer); } | |
| 133 | + | .item-page .item-media, | |
| 134 | + | .item-page .item-details { padding: var(--gap-section); } | |
| 135 | + | .item-page .item-description { padding: var(--gap-section); } | |
| 136 | + | .item-page .item-description h2 { font-size: var(--text-subhead); } | |
| 137 | + | .item-page .purchase-box { padding: var(--gap-section); } | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | /* =========================================== | |
| 141 | + | PROJECT PAGE (store front) | |
| 142 | + | =========================================== | |
| 143 | + | ||
| 144 | + | The store front is described (`src/quasi/project.rs`), so the header, the | |
| 145 | + | item cards, the two view copies, the tab strip and the tier cards are all | |
| 146 | + | the design system's markup now: a rule restating one of them fails the | |
| 147 | + | drift check in build.rs. What is this page's rather than the system's is | |
| 148 | + | how wide it runs, which is the measure on the body, and nothing else was | |
| 149 | + | left. */ | |
| 150 | + | ||
| 151 | + | /* =========================================== | |
| 152 | + | LIBRARY DOWNLOADS PAGE | |
| 153 | + | =========================================== */ | |
| 154 | + | ||
| 155 | + | /* Library downloads page (templates/pages/library_downloads.html). | |
| 156 | + | Scoped under .library-page body class. Class names like .item-footer, | |
| 157 | + | .bundle-child, etc. overlap with .item-page; the scope keeps them | |
| 158 | + | distinct. */ | |
| 159 | + | .library-page .library-header { | |
| 160 | + | display: grid; | |
| 161 | + | grid-template-columns: 200px 1fr; | |
| 162 | + | gap: var(--gap-pane); | |
| 163 | + | margin-bottom: var(--gap-page); | |
| 164 | + | align-items: center; | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | .library-page .library-header.no-cover { grid-template-columns: 1fr; } | |
| 168 | + | ||
| 169 | + | .library-page .library-cover img { | |
| 170 | + | width: 100%; | |
| 171 | + | aspect-ratio: 1 / 1; | |
| 172 | + | object-fit: cover; | |
| 173 | + | display: block; | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | .library-page .library-title { | |
| 177 | + | font-size: var(--text-title); | |
| 178 | + | margin-bottom: var(--gap-bound); | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | /* Byline sits under the centered H1 (global `h1 { text-align: center }`), | |
| 182 | + | so center it too, otherwise it floats flush-left while the title is | |
| 183 | + | centered above. Same for the back-link breadcrumb. When a cover is | |
| 184 | + | present the grid scope below restores left-alignment in the text column. */ | |
| 185 | + | .library-page .library-creator { | |
| 186 | + | font-size: var(--text-body); | |
| 187 | + | opacity: 0.7; | |
| 188 | + | margin-bottom: var(--gap-section); | |
| 189 | + | text-align: center; | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | .library-page .library-creator a { color: var(--content); } | |
| 193 | + | ||
| 194 | + | .library-page .library-back { | |
| 195 | + | font-size: var(--text-note); | |
| 196 | + | opacity: 0.7; | |
| 197 | + | text-align: center; | |
| 198 | + | } | |
| 199 | + | .library-page .library-back a { color: var(--content); } | |
| 200 | + | ||
| 201 | + | /* When a cover is present, the title block sits in the right grid column; | |
| 202 | + | left-align in that context so the column reads as a coherent block next | |
| 203 | + | to the cover instead of a centered island. */ | |
| 204 | + | .library-page .library-header:not(.no-cover) .library-title, | |
| 205 | + | .library-page .library-header:not(.no-cover) .library-creator { | |
| 206 | + | text-align: left; | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | .library-page .downloads-hero h2 { | |
| 210 | + | font-size: var(--text-head); | |
| 211 | + | margin-bottom: var(--gap-section); | |
| 212 | + | padding-bottom: var(--gap-peer); | |
| 213 | + | border-bottom: 1px solid var(--border); | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | .library-page .download-row { | |
| 217 | + | display: flex; | |
| 218 | + | align-items: center; | |
| 219 | + | gap: var(--gap-section); | |
| 220 | + | padding: var(--gap-group) 0; | |
| 221 | + | border-bottom: 1px solid var(--border); | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | .library-page .download-row:last-child { border-bottom: none; } | |
| 225 | + | ||
| 226 | + | .library-page .download-version { | |
| 227 | + | min-width: 4em; | |
| 228 | + | font-family: var(--font-mono); | |
| 229 | + | font-size: var(--text-note); | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | .library-page .download-label { flex: 1; } | |
| 233 | + | ||
| 234 | + | .library-page .download-size { | |
| 235 | + | font-size: var(--text-note); | |
| 236 | + | opacity: 0.6; | |
| 237 | + | min-width: 5em; | |
| 238 | + | text-align: right; | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | .library-page .download-notice { | |
| 242 | + | background: var(--surface-sunken); | |
| 243 | + | padding: var(--gap-pane); | |
| 244 | + | margin-bottom: var(--gap-page); | |
| 245 | + | font-size: var(--text-note); | |
| 246 | + | opacity: 0.8; | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | .library-page .download-notice strong { font-family: var(--font-mono); } | |
| 250 | + | ||
| 251 | + | /* Inline download list on library_audio.html / library_video.html | |
| 252 | + | (below the media player). */ | |
| 253 | + | ||
| 254 | + | .library-page .library-downloads h3 { | |
| 255 | + | font-size: var(--text-lead); | |
| 256 | + | margin-bottom: var(--gap-section); | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | .library-page .library-section h2 { | |
| 260 | + | font-size: var(--text-subhead); | |
| 261 | + | margin-bottom: var(--gap-section); | |
| 262 | + | padding-bottom: var(--gap-peer); | |
| 263 | + | border-bottom: 1px solid var(--border); | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | .library-page .bundle-child { | |
| 267 | + | display: flex; | |
| 268 | + | align-items: center; | |
| 269 | + | gap: var(--gap-section); | |
| 270 | + | padding: var(--gap-section) 0; | |
| 271 | + | border-bottom: 1px solid var(--border); | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | .library-page .bundle-child:last-child { border-bottom: none; } | |
| 275 | + | ||
| 276 | + | .library-page .bundle-child-type { | |
| 277 | + | font-size: var(--text-fine); | |
| 278 | + | padding: var(--gap-bound) var(--gap-group); | |
| 279 | + | background: var(--surface-sunken); | |
| 280 | + | white-space: nowrap; | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | .library-page .bundle-child-title { flex: 1; } | |
| 284 | + | .library-page .bundle-child-title a { color: var(--content); } | |
| 285 | + | ||
| 286 | + | .library-page .item-footer { | |
| 287 | + | margin-top: var(--gap-page); | |
| 288 | + | padding-top: var(--gap-page); | |
| 289 | + | border-top: 1px solid var(--border); | |
| 290 | + | text-align: center; | |
| 291 | + | opacity: 0.6; | |
| 292 | + | font-size: var(--text-note); | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | .library-page .item-footer a { color: var(--content); } | |
| 296 | + | ||
| 297 | + | @media (max-width: 599px) { | |
| 298 | + | .library-page .library-header { grid-template-columns: 1fr; } | |
| 299 | + | .library-page .library-cover img { max-width: 200px; } | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | /* =========================================== | |
| 303 | + | ARTICLE & BLOG READER | |
| 304 | + | =========================================== */ | |
| 305 | + | ||
| 306 | + | /* Article reader pages (templates/pages/text_reader.html, blog_post.html, | |
| 307 | + | library_text.html). Long-form article layout, centered narrow column, | |
| 308 | + | author header, large heading, magazine-style body type. Scoped under | |
| 309 | + | .article-page body class. The container itself takes the shared reading | |
| 310 | + | column near the top of this file. */ | |
| 311 | + | ||
| 312 | + | .article-page .author-header { | |
| 313 | + | display: flex; | |
| 314 | + | align-items: center; | |
| 315 | + | gap: var(--gap-section); | |
| 316 | + | margin-bottom: var(--gap-page); | |
| 317 | + | } | |
| 318 | + | ||
| 319 | + | .article-page .author-avatar { | |
| 320 | + | width: 48px; | |
| 321 | + | height: 48px; | |
| 322 | + | background: var(--surface-sunken); | |
| 323 | + | border-radius: var(--radius-round); | |
| 324 | + | display: flex; | |
| 325 | + | align-items: center; | |
| 326 | + | justify-content: center; | |
| 327 | + | font-family: var(--font-display); | |
| 328 | + | font-size: var(--text-subhead); | |
| 329 | + | color: var(--content); | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | .article-page .author-info { flex: 1; } | |
| 333 | + | ||
| 334 | + | .article-page .author-name { | |
| 335 | + | font-family: var(--font-mono); | |
| 336 | + | font-size: var(--text-body); | |
| 337 | + | color: var(--content); | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | ||
| 341 | + | .article-page .author-name a:hover { text-decoration: underline; } | |
| 342 | + | ||
| 343 | + | .article-page .article-meta { | |
| 344 | + | font-family: var(--font-mono); | |
| 345 | + | font-size: var(--text-note); | |
| 346 | + | color: var(--content-muted); | |
| 347 | + | } | |
| 348 | + | ||
| 349 | + | .article-page .article-title { | |
| 350 | + | font-family: var(--font-display); | |
| 351 | + | font-size: var(--text-display); | |
| 352 | + | font-weight: normal; | |
| 353 | + | line-height: 1.2; | |
| 354 | + | margin-bottom: var(--gap-pane); | |
| 355 | + | color: var(--content); | |
| 356 | + | text-align: left; | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | .article-page .article-deck { | |
| 360 | + | font-family: var(--font-mono); | |
| 361 | + | font-size: var(--text-subhead); | |
| 362 | + | color: var(--content-muted); | |
| 363 | + | margin-bottom: var(--gap-page); | |
| 364 | + | line-height: 1.6; | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | .article-page .article-body { | |
| 368 | + | font-family: var(--font-sans); | |
| 369 | + | font-size: var(--text-lead); | |
| 370 | + | line-height: 1.9; | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | .article-page .article-body p { margin-bottom: var(--gap-pane); } | |
| 374 | + | ||
| 375 | + | .article-page .article-body h1 { | |
| 376 | + | font-family: var(--font-display); | |
| 377 | + | font-size: var(--text-title); | |
| 378 | + | font-weight: normal; | |
| 379 | + | margin-top: var(--gap-page); | |
| 380 | + | margin-bottom: var(--gap-section); | |
| 381 | + | color: var(--content); | |
| 382 | + | text-align: left; | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | .article-page .article-body h2 { | |
| 386 | + | font-family: var(--font-mono); | |
| 387 | + | font-size: var(--text-head); | |
| 388 | + | font-weight: normal; | |
| 389 | + | margin-top: var(--gap-page); | |
| 390 | + | margin-bottom: var(--gap-section); | |
| 391 | + | color: var(--content); | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | .article-page .article-body h3 { | |
| 395 | + | font-family: var(--font-mono); | |
| 396 | + | font-size: var(--text-subhead); | |
| 397 | + | font-weight: normal; | |
| 398 | + | margin-top: var(--gap-page); | |
| 399 | + | margin-bottom: var(--gap-section); | |
| 400 | + | color: var(--content); | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | .article-page .article-body blockquote { | |
| 404 | + | border-left: 3px solid var(--action); | |
| 405 | + | padding-left: var(--gap-pane); | |
| 406 | + | margin: var(--gap-page) 0; | |
| 407 | + | font-style: italic; | |
| 408 | + | color: var(--content-muted); | |
| 409 | + | } | |
| 410 | + | ||
| 411 | + | .article-page .article-body ul, | |
| 412 | + | .article-page .article-body ol { | |
| 413 | + | margin-bottom: var(--gap-pane); | |
| 414 | + | padding-left: var(--gap-pane); | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | .article-page .article-body li { margin-bottom: var(--gap-peer); } | |
| 418 | + | ||
| 419 | + | .article-page .article-body a { | |
| 420 | + | color: var(--action); | |
| 421 | + | text-decoration: underline; | |
| 422 | + | text-decoration-color: color-mix(in oklch, var(--action) 40%, transparent); | |
| 423 | + | text-underline-offset: 2px; | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | .article-page .article-body a:hover { | |
| 427 | + | text-decoration-color: var(--action); | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | .article-page .article-body strong { font-weight: bold; } | |
| 431 | + | .article-page .article-body em { font-style: italic; } | |
| 432 | + | ||
| 433 | + | .article-page .article-body code { | |
| 434 | + | font-family: var(--font-mono); | |
| 435 | + | background: var(--surface-sunken); | |
| 436 | + | padding: var(--gap-bound) var(--gap-peer); | |
| 437 | + | font-size: var(--text-body); | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | .article-page .article-body pre { | |
| 441 | + | background: var(--surface-sunken); | |
| 442 | + | padding: var(--gap-section); | |
| 443 | + | overflow-x: auto; | |
| 444 | + | margin-bottom: var(--gap-pane); | |
| 445 | + | } | |
| 446 | + | ||
| 447 | + | .article-page .article-body pre code { | |
| 448 | + | background: none; | |
| 449 | + | padding: 0; | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | .article-page .article-body hr { | |
| 453 | + | border: none; | |
| 454 | + | text-align: center; | |
| 455 | + | margin: var(--gap-page) 0; | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | .article-page .article-body hr::before { | |
| 459 | + | content: "* * *"; | |
| 460 | + | color: var(--content-muted); | |
| 461 | + | letter-spacing: 1rem; | |
| 462 | + | } | |
| 463 | + | ||
| 464 | + | .article-page .article-footer { | |
| 465 | + | margin-top: var(--gap-page); | |
| 466 | + | padding-top: var(--gap-page); | |
| 467 | + | border-top: 1px solid var(--border); | |
| 468 | + | } | |
| 469 | + | ||
| 470 | + | .article-page .author-bio { | |
| 471 | + | display: flex; | |
| 472 | + | gap: var(--gap-section); | |
| 473 | + | padding: var(--gap-pane); | |
| 474 | + | background: var(--surface-overlay); | |
| 475 | + | } | |
| 476 | + | ||
| 477 | + | .article-page .author-bio .author-avatar { | |
| 478 | + | width: 64px; | |
| 479 | + | height: 64px; | |
| 480 | + | font-size: var(--text-head); | |
| 481 | + | flex-shrink: 0; | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | .article-page .author-bio-content { flex: 1; } | |
| 485 | + | ||
| 486 | + | .article-page .author-bio-name { | |
| 487 | + | font-family: var(--font-mono); | |
| 488 | + | font-size: var(--text-lead); | |
| 489 | + | margin-bottom: var(--gap-peer); | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | ||
| 493 | + | .article-page .author-bio-name a:hover { text-decoration: underline; } | |
| 494 | + | ||
| 495 | + | .article-page .article-tags { | |
| 496 | + | margin-top: var(--gap-page); | |
| 497 | + | display: flex; | |
| 498 | + | gap: var(--gap-peer); | |
| 499 | + | flex-wrap: wrap; | |
| 500 | + | } |
Lines truncated