Skip to main content

max / makenotwork

Cut multithreaded's style.css into nine parts 1,896 lines and fifty-eight section banners appended in the order the forum grew. Nothing duplicates anything; what makes it a god file is that four independent features live in it that a person would look for separately. Same mechanism as the server: contiguous parts under css/, joined by build.rs into the committed static/style.css, byte-identical. This sheet uses no @layer, so the join is the whole of it and every part is standalone-valid CSS on its own. The parts sit at repo root rather than under static/, because src/static_assets.rs embeds static/ wholesale with include_dir! and parts kept there would ship twice. 70-chat.css is the clearest argument for doing this: the chat room is the most self-contained feature in the sheet and had no name. 00-foundations.css is the makeover conversion's whole target. The five @font-face blocks and the :root of raw hex are all in that one file and nowhere else, which is what the cut at 132 buys.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01EEmeiSJnmyL98QzA5Dwsvz
Author: Max Johnson <me@maxj.phd> · 2026-09-05 03:21 UTC
Signed with PGP, not checked
Commit: a3e985b764c59b734ee99d8075ff39bf011cb69f
Parent: 8fc63e4
11 files changed, +1961 insertions, -1 deletion
@@ -1,7 +1,7 @@
1 1 use std::collections::hash_map::DefaultHasher;
2 2 use std::fs;
3 3 use std::hash::{Hash, Hasher};
4 - use std::path::Path;
4 + use std::path::{Path, PathBuf};
5 5 use std::process::Command;
6 6
7 7 fn main() {
@@ -50,6 +50,10 @@
50 50 // `build_frontend` — a build-script rerun recompiles the crate, which is
51 51 // when `include_dir!` re-reads the emitted bundles.
52 52 println!("cargo::rerun-if-changed=static/fonts");
53 +
54 + // Before the hash below, so `?v=` reflects the sheet this run just wrote.
55 + concat_site_sheet();
56 +
53 57 let static_files = ["static/style.css", "static/htmx.min.js", "static/mt.js"];
54 58
55 59 let mut hasher = DefaultHasher::new();
@@ -98,6 +102,41 @@
98 102 }
99 103 }
100 104
105 + /// Join `css/*.css` into `static/style.css`.
106 + ///
107 + /// The parts are contiguous line ranges of what the sheet already was, loaded
108 + /// in name order, so the join is byte-identical and the split changed no
109 + /// rendering. This sheet uses no `@layer`, so the join is the whole of it and
110 + /// every part is standalone-valid CSS on its own.
111 + ///
112 + /// The parts live in `css/` rather than `static/css/` because
113 + /// `src/static_assets.rs` embeds `static/` wholesale with `include_dir!`, so
114 + /// parts kept there would ship twice.
115 + ///
116 + /// The output stays committed: it is what `include_dir!` embeds and what the
117 + /// head links, and 1,900 lines of annotated CSS keep their blame.
118 + fn concat_site_sheet() {
119 + let mut parts: Vec<PathBuf> = fs::read_dir("css")
120 + .expect("read css/")
121 + .map(|e| e.expect("css/ entry").path())
122 + .filter(|p| p.extension().is_some_and(|e| e == "css"))
123 + .collect();
124 + parts.sort();
125 + println!("cargo::rerun-if-changed=css");
126 +
127 + let mut out = String::new();
128 + for part in &parts {
129 + println!("cargo::rerun-if-changed={}", part.display());
130 + out.push_str(
131 + &fs::read_to_string(part).unwrap_or_else(|e| panic!("read {}: {e}", part.display())),
132 + );
133 + }
134 +
135 + // `write_if_changed`, not `fs::write`: this is a file the script both writes
136 + // and watches, and an unconditional write reruns the script on every build.
137 + write_if_changed(Path::new("static/style.css"), &out);
138 + }
139 +
101 140 /// Recursively hash every `.js` file under `dir` into `hasher`, in a
102 141 /// deterministic order. A missing directory is a no-op, the first build on a
103 142 /// fresh checkout runs before the frontend has been compiled.
@@ -1,0 +1,132 @@
1 + /* Multithreaded, Forum Stylesheet. Adapted from Makenot.work design language */
2 +
3 + /* FONTS */
4 +
5 + @font-face {
6 + font-family: "Young Serif";
7 + src: url("/static/fonts/ysrf.woff2") format("woff2"),
8 + url("/static/fonts/ysrf.ttf") format("truetype");
9 + font-display: swap;
10 + }
11 +
12 + @font-face {
13 + font-family: "IBM Plex Mono";
14 + src: url("/static/fonts/IBMPlexMono-Regular.woff2") format("woff2"),
15 + url("/static/fonts/IBMPlexMono-Regular.ttf") format("truetype");
16 + font-weight: normal;
17 + font-display: swap;
18 + }
19 +
20 + @font-face {
21 + font-family: "IBM Plex Mono";
22 + src: url("/static/fonts/IBMPlexMono-Bold.woff2") format("woff2"),
23 + url("/static/fonts/IBMPlexMono-Bold.ttf") format("truetype");
24 + font-weight: bold;
25 + font-display: swap;
26 + }
27 +
28 + @font-face {
29 + font-family: "Lato";
30 + src: url("/static/fonts/Lato-Regular.woff2") format("woff2"),
31 + url("/static/fonts/Lato-Regular.ttf") format("truetype");
32 + font-weight: normal;
33 + font-display: swap;
34 + }
35 +
36 + @font-face {
37 + font-family: "Lato";
38 + src: url("/static/fonts/Lato-Bold.woff2") format("woff2"),
39 + url("/static/fonts/Lato-Bold.ttf") format("truetype");
40 + font-weight: bold;
41 + font-display: swap;
42 + }
43 +
44 + /* VARIABLES */
45 +
46 + :root {
47 + --background: #ede8e1;
48 + --detail: #3d3530;
49 + --highlight: #6c5ce7;
50 + --light-background: #f4f0eb;
51 +
52 + --surface-muted: #ddd7c5;
53 + --surface-alt: #ebe7db;
54 + --surface-border: #d0cbb8;
55 + --border: #d0cbb8;
56 + --text-muted: #8a8480;
57 + --input-background: #e2dad2;
58 +
59 + --primary-dark: #000000;
60 + --primary-light: #ffffff;
61 + --success: #2d5016;
62 + --warning: #8b7355;
63 + --danger: #d62828;
64 + --error: #d62828;
65 +
66 + --focus-ring: #6c5ce7;
67 + }
68 +
69 + /* RESET */
70 +
71 + * {
72 + margin: 0;
73 + padding: 0;
74 + box-sizing: border-box;
75 + }
76 +
77 + /* BASE */
78 +
79 + body {
80 + margin: 0;
81 + background-color: var(--background);
82 + color: var(--detail);
83 + font-family: "Lato", sans-serif;
84 + line-height: 1.5;
85 + }
86 +
87 + h1 {
88 + font-family: "Young Serif", serif;
89 + font-weight: normal;
90 + color: var(--detail);
91 + text-align: left;
92 + font-size: 1.5rem;
93 + }
94 +
95 + h2,
96 + h3 {
97 + font-family: "IBM Plex Mono", monospace;
98 + font-weight: normal;
99 + color: var(--detail);
100 + }
101 +
102 + p {
103 + font-family: "Lato", sans-serif;
104 + color: var(--detail);
105 + }
106 +
107 + a {
108 + color: var(--detail);
109 + text-decoration: none;
110 + }
111 +
112 + a:hover {
113 + text-decoration: underline;
114 + }
115 +
116 + /* The signature dot */
117 + .dot {
118 + color: var(--highlight);
119 + }
120 +
121 + /* LAYOUT */
122 +
123 + .container {
124 + max-width: 1200px;
125 + margin: 0 auto;
126 + padding: 1rem;
127 + }
128 +
129 + .padded-page {
130 + padding: 1.25rem;
131 + }
132 +
@@ -1,0 +1,86 @@
1 + /* SITE HEADER & NAV */
2 +
3 + .site-header {
4 + display: flex;
5 + justify-content: space-between;
6 + align-items: center;
7 + padding: 0.75rem 1.25rem;
8 + margin-bottom: 0.5rem;
9 + }
10 +
11 + .site-header nav {
12 + margin: 0;
13 + }
14 +
15 + .site-header .nav-links {
16 + display: flex;
17 + gap: 1.5rem;
18 + align-items: center;
19 + }
20 +
21 + .nav-links a {
22 + color: var(--detail);
23 + text-decoration: none;
24 + font-family: "IBM Plex Mono", monospace;
25 + transition: opacity 0.2s ease;
26 + }
27 +
28 + .nav-links a:hover {
29 + opacity: 0.6;
30 + text-decoration: none;
31 + }
32 +
33 + .site-logo {
34 + font-family: "Young Serif", serif;
35 + font-size: 1.25rem;
36 + text-decoration: none;
37 + color: var(--detail);
38 + }
39 +
40 + .site-logo:hover {
41 + opacity: 0.8;
42 + text-decoration: none;
43 + }
44 +
45 + .link-button {
46 + background: none;
47 + border: none;
48 + color: var(--detail);
49 + font-family: "IBM Plex Mono", monospace;
50 + font-size: 1rem;
51 + cursor: pointer;
52 + padding: 0;
53 + text-decoration: none;
54 + transition: opacity 0.2s ease;
55 + }
56 +
57 + .link-button:hover {
58 + opacity: 0.6;
59 + text-decoration: underline;
60 + }
61 +
62 + .nav-form {
63 + display: contents;
64 + }
65 +
66 + /* Hamburger toggle */
67 + .nav-toggle-checkbox {
68 + display: none;
69 + }
70 +
71 + .nav-toggle-label {
72 + display: none;
73 + cursor: pointer;
74 + padding: 0.5rem;
75 + z-index: 11;
76 + }
77 +
78 + .nav-toggle-label span {
79 + display: block;
80 + width: 22px;
81 + height: 2px;
82 + background: var(--detail);
83 + margin: 5px 0;
84 + transition: transform 0.2s ease, opacity 0.2s ease;
85 + }
86 +
@@ -1,0 +1,195 @@
1 + /* BUTTONS */
2 +
3 + button {
4 + color: var(--detail);
5 + background: var(--light-background);
6 + padding: 6px 12px;
7 + border: 1px solid var(--detail);
8 + font-family: inherit;
9 + cursor: pointer;
10 + transition: opacity 0.2s ease;
11 + }
12 +
13 + button:hover {
14 + opacity: 0.8;
15 + }
16 +
17 + button.primary,
18 + .btn-primary {
19 + background: var(--primary-dark);
20 + color: var(--primary-light);
21 + border: none;
22 + padding: 0.5rem 1rem;
23 + font-family: inherit;
24 + font-size: 0.85rem;
25 + cursor: pointer;
26 + transition: opacity 0.2s ease;
27 + }
28 +
29 + button.primary:hover,
30 + .btn-primary:hover {
31 + opacity: 0.8;
32 + }
33 +
34 + button.secondary,
35 + .btn-secondary {
36 + background: var(--surface-muted);
37 + color: var(--detail);
38 + border: none;
39 + padding: 0.5rem 1rem;
40 + font-family: "IBM Plex Mono", monospace;
41 + font-size: 0.85rem;
42 + cursor: pointer;
43 + transition: opacity 0.2s ease;
44 + }
45 +
46 + button.secondary:hover,
47 + .btn-secondary:hover {
48 + opacity: 0.8;
49 + }
50 +
51 + /* Irreversible actions. The class was in the markup before it was in here (the
52 + admin clean-slate button, the chat wipe), rendering as an ordinary button,
53 + which is the one thing a destructive control must not look like. */
54 + button.danger {
55 + background: var(--danger);
56 + color: var(--primary-light);
57 + border: none;
58 + padding: 0.5rem 1rem;
59 + font-family: inherit;
60 + font-size: 0.85rem;
61 + cursor: pointer;
62 + transition: opacity 0.2s ease;
63 + }
64 +
65 + button.danger:hover {
66 + opacity: 0.8;
67 + }
68 +
69 + /* FORMS */
70 +
71 + form {
72 + display: flex;
73 + flex-direction: column;
74 + gap: 1rem;
75 + width: 100%;
76 + }
77 +
78 + .form-container {
79 + max-width: 600px;
80 + background: var(--light-background);
81 + padding: 1.5rem;
82 + }
83 +
84 + label {
85 + font-family: "IBM Plex Mono", monospace;
86 + color: var(--detail);
87 + font-size: 14px;
88 + }
89 +
90 + input[type="text"],
91 + input[type="email"],
92 + input[type="password"],
93 + input[type="number"],
94 + textarea,
95 + select {
96 + width: 100%;
97 + background: var(--input-background);
98 + border: none;
99 + padding: 0.6rem;
100 + font-family: "IBM Plex Mono", monospace;
101 + font-size: 0.85rem;
102 + color: var(--detail);
103 + }
104 +
105 + input::placeholder,
106 + textarea::placeholder {
107 + opacity: 0.5;
108 + }
109 +
110 + input[type="text"]:focus,
111 + input[type="email"]:focus,
112 + input[type="password"]:focus,
113 + input[type="number"]:focus,
114 + textarea:focus,
115 + select:focus {
116 + outline: none;
117 + border: 2px solid var(--highlight);
118 + }
119 +
120 + textarea {
121 + min-height: 150px;
122 + resize: vertical;
123 + }
124 +
125 + textarea.drag-over {
126 + border: 2px dashed var(--highlight);
127 + background: var(--surface-muted);
128 + }
129 +
130 + /* Inline form validation error, inserted by mt.js on a 422 submit, replacing
131 + the old error toast. Persists next to the form with the input preserved. */
132 + .form-error {
133 + background: var(--surface-muted);
134 + border-left: 3px solid var(--error);
135 + color: var(--error);
136 + padding: 0.6rem 0.75rem;
137 + margin-bottom: 0.75rem;
138 + font-size: 0.85rem;
139 + }
140 +
141 + input[aria-invalid="true"],
142 + textarea[aria-invalid="true"],
143 + select[aria-invalid="true"] {
144 + border: 2px solid var(--error);
145 + }
146 +
147 + select {
148 + cursor: pointer;
149 + }
150 +
151 + input[type="submit"],
152 + form button {
153 + font-family: "Young Serif", serif;
154 + font-size: 16px;
155 + color: var(--detail);
156 + background: var(--light-background);
157 + padding: 10px 20px;
158 + border: 1px solid var(--detail);
159 + cursor: pointer;
160 + }
161 +
162 + input[type="submit"]:hover,
163 + form button:hover {
164 + background: var(--background);
165 + }
166 +
167 + .form-row {
168 + display: grid;
169 + grid-template-columns: 1fr 1fr;
170 + gap: 1rem;
171 + }
172 +
173 + .form-group {
174 + display: flex;
175 + flex-direction: column;
176 + gap: 0.25rem;
177 + }
178 +
179 + .form-actions {
180 + display: flex;
181 + gap: 1rem;
182 + margin-top: 0.5rem;
183 + }
184 +
185 + .form-help {
186 + display: block;
187 + font-size: 0.8rem;
188 + color: var(--text-muted);
189 + margin-top: 0.25rem;
190 + }
191 +
192 + .input-narrow {
193 + width: 6rem;
194 + }
195 +
@@ -1,0 +1,305 @@
1 + /* DIRECTORY TABLE (forum home) */
2 +
3 + .directory-table {
4 + width: 100%;
5 + border-collapse: collapse;
6 + font-size: 0.85rem;
7 + font-family: "Lato", sans-serif;
8 + }
9 +
10 + .directory-table thead {
11 + background: var(--surface-muted);
12 + }
13 +
14 + .directory-table th {
15 + text-align: left;
16 + padding: 0.35rem 0.75rem;
17 + font-family: "IBM Plex Mono", monospace;
18 + font-size: 0.7rem;
19 + font-weight: normal;
20 + text-transform: uppercase;
21 + letter-spacing: 0.05em;
22 + color: var(--text-muted);
23 + }
24 +
25 + .directory-table tbody tr {
26 + transition: background 0.1s ease;
27 + }
28 +
29 + .directory-table tbody tr:nth-child(odd) {
30 + background: var(--surface-alt);
31 + }
32 +
33 + .directory-table tbody tr:nth-child(even) {
34 + background: var(--light-background);
35 + }
36 +
37 + .directory-table tbody tr:hover {
38 + background: var(--surface-muted);
39 + }
40 +
41 + .directory-table td {
42 + padding: 0.4rem 0.75rem;
43 + border-top: 1px solid var(--border);
44 + }
45 +
46 + .directory-name {
47 + font-family: "Young Serif", serif;
48 + font-size: 1rem;
49 + }
50 +
51 + .directory-name a {
52 + color: var(--detail);
53 + text-decoration: none;
54 + }
55 +
56 + .directory-name a:hover {
57 + text-decoration: underline;
58 + }
59 +
60 + .directory-desc {
61 + font-size: 0.8rem;
62 + color: var(--text-muted);
63 + margin-top: 0.1rem;
64 + }
65 +
66 + .directory-table .col-creator {
67 + white-space: nowrap;
68 + width: 130px;
69 + font-family: "IBM Plex Mono", monospace;
70 + font-size: 0.8rem;
71 + }
72 +
73 + .directory-table .col-type {
74 + white-space: nowrap;
75 + width: 100px;
76 + font-family: "IBM Plex Mono", monospace;
77 + font-size: 0.8rem;
78 + color: var(--text-muted);
79 + }
80 +
81 + .directory-table .col-items {
82 + white-space: nowrap;
83 + width: 60px;
84 + text-align: right;
85 + font-family: "IBM Plex Mono", monospace;
86 + font-size: 0.8rem;
87 + color: var(--text-muted);
88 + }
89 +
90 + /* DATA TABLE (dense thread listings) */
91 +
92 + .data-table {
93 + width: 100%;
94 + border-collapse: collapse;
95 + font-size: 0.85rem;
96 + font-family: "Lato", sans-serif;
97 + margin-bottom: 1rem;
98 + }
99 +
100 + .data-table thead {
101 + background: var(--surface-muted);
102 + }
103 +
104 + .data-table th {
105 + text-align: left;
106 + padding: 0.35rem 0.75rem;
107 + font-family: "IBM Plex Mono", monospace;
108 + font-size: 0.7rem;
109 + font-weight: normal;
110 + text-transform: uppercase;
111 + letter-spacing: 0.05em;
112 + color: var(--text-muted);
113 + }
114 +
115 + .data-table th.sortable {
116 + cursor: pointer;
117 + transition: background 0.2s ease;
118 + user-select: none;
119 + }
120 +
121 + .data-table th.sortable:hover {
122 + background: var(--border);
123 + }
124 +
125 + .data-table th.sortable a {
126 + color: inherit;
127 + text-decoration: none;
128 + display: block;
129 + }
130 +
131 + .data-table th.sortable a:hover {
132 + text-decoration: none;
133 + }
134 +
135 + .data-table th.sort-active {
136 + color: var(--detail);
137 + }
138 +
139 + .data-table tbody tr {
140 + transition: background 0.1s ease;
141 + }
142 +
143 + .data-table tbody tr:nth-child(odd) {
144 + background: var(--surface-alt);
145 + }
146 +
147 + .data-table tbody tr:nth-child(even) {
148 + background: var(--light-background);
149 + }
150 +
151 + .data-table tbody tr:hover {
152 + background: var(--surface-muted);
153 + }
154 +
155 + .data-table td {
156 + padding: 0.4rem 0.75rem;
157 + border-top: 1px solid var(--border);
158 + }
159 +
160 + /* Thread-specific table columns */
161 + .data-table .thread-title {
162 + font-weight: bold;
163 + }
164 +
165 + .data-table .thread-title a {
166 + color: var(--detail);
167 + text-decoration: none;
168 + }
169 +
170 + .data-table .thread-title a:hover {
171 + text-decoration: underline;
172 + }
173 +
174 + .data-table .col-replies,
175 + .data-table .col-activity {
176 + white-space: nowrap;
177 + text-align: right;
178 + width: 100px;
179 + }
180 +
181 + .data-table .col-author {
182 + white-space: nowrap;
183 + width: 130px;
184 + }
185 +
186 + /* Member list columns */
187 + .data-table .col-role {
188 + white-space: nowrap;
189 + width: 100px;
190 + }
191 +
192 + .data-table .col-joined {
193 + white-space: nowrap;
194 + text-align: right;
195 + width: 100px;
196 + font-family: "IBM Plex Mono", monospace;
197 + font-size: 0.8rem;
198 + color: var(--text-muted);
199 + }
200 +
201 + /* Pinned row highlight */
202 + .data-table tr.pinned {
203 + background: var(--surface-muted);
204 + }
205 +
206 + .data-table tr.pinned:hover {
207 + background: var(--border);
208 + }
209 +
210 + /* BADGES */
211 +
212 + .badge {
213 + display: inline-block;
214 + padding: 0.15rem 0.4rem;
215 + font-size: 0.7rem;
216 + font-family: "IBM Plex Mono", monospace;
217 + background: var(--primary-dark);
218 + color: var(--primary-light);
219 + }
220 +
221 + .badge-success {
222 + background: var(--success);
223 + }
224 +
225 + .badge-warning {
226 + background: var(--warning);
227 + }
228 +
229 + .badge-danger {
230 + background: var(--danger);
231 + }
232 +
233 + .badge-muted {
234 + background: var(--text-muted);
235 + }
236 +
237 + .badge-ban {
238 + background: var(--danger);
239 + color: var(--primary-light);
240 + }
241 +
242 + .badge-mute-type {
243 + background: var(--warning);
244 + color: var(--primary-light);
245 + }
246 +
247 + /* Role badges */
248 + .badge-role-owner {
249 + background: var(--primary-dark);
250 + color: var(--primary-light);
251 + }
252 +
253 + .badge-role-moderator {
254 + background: var(--warning);
255 + color: var(--primary-light);
256 + }
257 +
258 + .badge-role-member {
259 + background: var(--surface-muted);
260 + color: var(--detail);
261 + border: 1px solid var(--border);
262 + }
263 +
264 + .badge-role-guest {
265 + background: var(--surface-muted);
266 + color: var(--text-muted);
267 + border: 1px solid var(--border);
268 + }
269 +
270 + /* Thread status badges, text-only */
271 + .badge-pinned {
272 + background: var(--surface-muted);
273 + color: var(--detail);
274 + border: 1px solid var(--border);
275 + }
276 +
277 + .badge-locked {
278 + background: var(--surface-muted);
279 + color: var(--text-muted);
280 + border: 1px solid var(--border);
281 + }
282 +
283 + /* BREADCRUMBS */
284 +
285 + .breadcrumb {
286 + font-size: 0.8rem;
287 + color: var(--text-muted);
288 + margin-bottom: 0.5rem;
289 + font-family: "IBM Plex Mono", monospace;
290 + }
291 +
292 + .breadcrumb a {
293 + color: var(--detail);
294 + text-decoration: none;
295 + }
296 +
297 + .breadcrumb a:hover {
298 + text-decoration: underline;
299 + }
300 +
301 + .breadcrumb .sep {
302 + margin: 0 0.3rem;
303 + opacity: 0.5;
304 + }
305 +
@@ -1,0 +1,182 @@
1 + /* POST ITEM (thread view) */
2 +
3 + .post-list {
4 + display: flex;
5 + flex-direction: column;
6 + gap: 0;
7 + }
8 +
9 + .post-item {
10 + padding: 0.75rem 1rem;
11 + border-bottom: 1px solid var(--border);
12 + }
13 +
14 + .post-item:first-child {
15 + border-top: 1px solid var(--border);
16 + }
17 +
18 + .post-header {
19 + display: flex;
20 + justify-content: space-between;
21 + align-items: baseline;
22 + margin-bottom: 0.25rem;
23 + }
24 +
25 + .post-author {
26 + font-family: "IBM Plex Mono", monospace;
27 + font-weight: bold;
28 + font-size: 0.85rem;
29 + }
30 +
31 + .post-timestamp {
32 + font-family: "IBM Plex Mono", monospace;
33 + font-size: 0.75rem;
34 + color: var(--text-muted);
35 + }
36 +
37 + .post-edited {
38 + font-family: "IBM Plex Mono", monospace;
39 + font-size: 0.7rem;
40 + color: var(--text-muted);
41 + font-style: italic;
42 + margin-left: 0.5rem;
43 + }
44 +
45 + .post-body {
46 + font-size: 0.9rem;
47 + line-height: 1.5;
48 + }
49 +
50 + .post-body p {
51 + margin-bottom: 0.5rem;
52 + }
53 +
54 + .post-body p:last-child {
55 + margin-bottom: 0;
56 + }
57 +
58 + .post-body code {
59 + font-family: "IBM Plex Mono", monospace;
60 + font-size: 0.8rem;
61 + background: var(--surface-muted);
62 + padding: 0.1rem 0.35rem;
63 + }
64 +
65 + .post-body pre {
66 + background: var(--primary-dark);
67 + color: var(--primary-light);
68 + padding: 0.75rem;
69 + overflow-x: auto;
70 + margin: 0.5rem 0;
71 + font-family: "IBM Plex Mono", monospace;
72 + font-size: 0.8rem;
73 + line-height: 1.5;
74 + }
75 +
76 + .post-body pre code {
77 + background: none;
78 + padding: 0;
79 + color: inherit;
80 + }
81 +
82 + .post-body blockquote {
83 + border-left: 3px solid var(--border);
84 + padding-left: 1rem;
85 + margin: 0.5rem 0;
86 + color: var(--text-muted);
87 + }
88 +
89 + .post-body img {
90 + max-width: 100%;
91 + height: auto;
92 + border-radius: 4px;
93 + margin: 0.5rem 0;
94 + cursor: pointer;
95 + }
96 +
97 + /* Post action links (edit/delete) */
98 + .post-actions {
99 + margin-left: 0.75rem;
100 + }
101 +
102 + .post-action-link {
103 + font-family: "IBM Plex Mono", monospace;
104 + font-size: 0.7rem;
105 + color: var(--text-muted);
106 + background: none;
107 + border: none;
108 + padding: 0;
109 + cursor: pointer;
110 + text-decoration: none;
111 + margin-left: 0.5rem;
112 + }
113 +
114 + .post-action-link:hover {
115 + color: var(--detail);
116 + text-decoration: underline;
117 + }
118 +
119 + .post-action-delete:hover {
120 + color: var(--danger);
121 + }
122 +
123 + /* Deleted posts */
124 + .post-deleted .post-body {
125 + color: var(--text-muted);
126 + font-style: italic;
127 + }
128 +
129 + /* OP indicator */
130 + .post-item.op {
131 + background: var(--light-background);
132 + }
133 +
134 + /* DRAFT INDICATOR */
135 +
136 + .draft-indicator {
137 + font-family: "IBM Plex Mono", monospace;
138 + font-size: 0.75rem;
139 + color: var(--text-muted);
140 + margin-bottom: 0.25rem;
141 + }
142 +
143 + .draft-discard {
144 + cursor: pointer;
145 + color: var(--text-muted);
146 + }
147 +
148 + .draft-discard:hover {
149 + color: var(--danger);
150 + }
151 +
152 + /* REPLY FORM */
153 +
154 + .reply-section {
155 + padding: 1rem;
156 + margin-top: 0.5rem;
157 + }
158 +
159 + .reply-section h3 {
160 + margin-bottom: 0.75rem;
161 + }
162 +
163 + /* PAGE HEADER (title + actions row) */
164 +
165 + .page-header {
166 + display: flex;
167 + justify-content: space-between;
168 + align-items: center;
169 + margin-bottom: 0.5rem;
170 + }
171 +
172 + .page-header h1 {
173 + text-align: left;
174 + font-size: 1.5rem;
175 + margin: 0;
176 + }
177 +
178 + .page-header h2 {
179 + font-size: 1.2rem;
180 + margin: 0;
181 + }
182 +
@@ -1,0 +1,523 @@
1 + /* TOAST NOTIFICATIONS */
2 +
3 + .toast-container {
4 + position: fixed;
5 + bottom: 1.5rem;
6 + right: 1.5rem;
7 + z-index: 1000;
8 + display: flex;
9 + flex-direction: column;
10 + gap: 0.5rem;
11 + }
12 +
13 + .toast {
14 + background: var(--primary-dark);
15 + color: var(--primary-light);
16 + padding: 0.6rem 1rem;
17 + font-family: "IBM Plex Mono", monospace;
18 + font-size: 0.85rem;
19 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
20 + animation: toast-in 0.3s ease-out;
21 + max-width: 300px;
22 + }
23 +
24 + .toast-success {
25 + background: var(--success);
26 + }
27 +
28 + .toast-error {
29 + background: var(--error);
30 + }
31 +
32 + .toast-warning {
33 + background: var(--warning);
34 + }
35 +
36 + .toast-info {
37 + background: var(--primary-dark);
38 + }
39 +
40 + .toast-retry-btn {
41 + background: none;
42 + border: 1px solid currentColor;
43 + color: inherit;
44 + font-family: "IBM Plex Mono", monospace;
45 + font-size: 0.75rem;
46 + font-weight: 600;
47 + padding: 0.1rem 0.4rem;
48 + margin-left: 0.75rem;
49 + cursor: pointer;
50 + }
51 +
52 + .toast-retry-btn:hover {
53 + background: rgba(255, 255, 255, 0.15);
54 + }
55 +
56 + .toast.fade-out {
57 + animation: toast-out 0.3s ease-in forwards;
58 + }
59 +
60 + @keyframes toast-in {
61 + from { transform: translateX(100%); }
62 + to { transform: translateX(0); }
63 + }
64 +
65 + @keyframes toast-out {
66 + from { transform: translateX(0); }
67 + to { transform: translateX(100%); }
68 + }
69 +
70 + /* SETTINGS PAGE */
71 +
72 + .settings-section {
73 + margin-bottom: 2rem;
74 + }
75 +
76 + .settings-section h3 {
77 + margin-bottom: 0.75rem;
78 + }
79 +
80 + .settings-table {
81 + width: 100%;
82 + border-collapse: collapse;
83 + font-size: 0.85rem;
84 + font-family: "Lato", sans-serif;
85 + margin-bottom: 1rem;
86 + }
87 +
88 + .settings-table thead {
89 + background: var(--surface-muted);
90 + }
91 +
92 + .settings-table th {
93 + text-align: left;
94 + padding: 0.35rem 0.75rem;
95 + font-family: "IBM Plex Mono", monospace;
96 + font-size: 0.7rem;
97 + font-weight: normal;
98 + text-transform: uppercase;
99 + letter-spacing: 0.05em;
100 + color: var(--text-muted);
101 + }
102 +
103 + .settings-table tbody tr {
104 + transition: background 0.1s ease;
105 + }
106 +
107 + .settings-table tbody tr:nth-child(odd) {
108 + background: var(--surface-alt);
109 + }
110 +
111 + .settings-table tbody tr:nth-child(even) {
112 + background: var(--light-background);
113 + }
114 +
115 + .settings-table tbody tr:hover {
116 + background: var(--surface-muted);
117 + }
118 +
119 + .settings-table td {
120 + padding: 0.4rem 0.75rem;
121 + border-top: 1px solid var(--border);
122 + }
123 +
124 + .settings-cat-name {
125 + font-family: "Young Serif", serif;
126 + font-size: 1rem;
127 + }
128 +
129 + .settings-cat-desc {
130 + display: block;
131 + font-size: 0.8rem;
132 + color: var(--text-muted);
133 + margin-top: 0.1rem;
134 + }
135 +
136 + .settings-mono {
137 + font-family: "IBM Plex Mono", monospace;
138 + font-size: 0.8rem;
139 + color: var(--text-muted);
140 + }
141 +
142 + .settings-actions {
143 + white-space: nowrap;
144 + }
145 +
146 + /* ACCESSIBILITY */
147 +
148 + .skip-to-main {
149 + position: absolute;
150 + top: -100px;
151 + left: 0;
152 + background: var(--primary-dark);
153 + color: var(--primary-light);
154 + padding: 8px;
155 + z-index: 100;
156 + transition: top 0.2s ease;
157 + }
158 +
159 + .skip-to-main:focus {
160 + top: 0;
161 + }
162 +
163 + *:focus-visible {
164 + outline: 2px solid var(--focus-ring);
165 + outline-offset: 2px;
166 + }
167 +
168 + /* PAGINATION */
169 +
170 + .pagination {
171 + display: flex;
172 + justify-content: center;
173 + align-items: center;
174 + gap: 1rem;
175 + padding: 0.75rem 0;
176 + font-family: "IBM Plex Mono", monospace;
177 + font-size: 0.85rem;
178 + }
179 +
180 + .pagination-link {
181 + color: var(--detail);
182 + text-decoration: none;
183 + padding: 0.3rem 0.6rem;
184 + border: 1px solid var(--border);
185 + background: var(--light-background);
186 + transition: background 0.2s ease;
187 + }
188 +
189 + .pagination-link:hover {
190 + background: var(--surface-muted);
191 + text-decoration: none;
192 + }
193 +
194 + .pagination-info {
195 + color: var(--text-muted);
196 + }
197 +
198 + /* ERROR PAGES */
199 +
200 + .error-page {
201 + text-align: center;
202 + padding: 3rem 1rem;
203 + }
204 +
205 + .error-page h2 {
206 + font-family: "Young Serif", serif;
207 + font-size: 3rem;
208 + margin-bottom: 0.5rem;
209 + }
210 +
211 + .error-page p {
212 + color: var(--text-muted);
213 + margin-bottom: 1.5rem;
214 + }
215 +
216 + /* EMPTY STATE */
217 +
218 + .empty-state {
219 + text-align: center;
220 + padding: 1rem;
221 + color: var(--text-muted);
222 + font-family: "IBM Plex Mono", monospace;
223 + }
224 +
225 + /* SITE FOOTER */
226 +
227 + .site-footer {
228 + text-align: center;
229 + padding: 1rem;
230 + margin-top: 1rem;
231 + border-top: 1px solid var(--border);
232 + font-family: "IBM Plex Mono", monospace;
233 + font-size: 0.75rem;
234 + color: var(--text-muted);
235 + }
236 +
237 + .site-footer a {
238 + color: var(--detail);
239 + text-decoration: none;
240 + }
241 +
242 + .site-footer a:hover {
243 + text-decoration: underline;
244 + }
245 +
246 + .site-footer-links {
247 + margin-bottom: 0.5rem;
248 + }
249 +
250 + .site-footer-links a {
251 + margin: 0 0.75rem;
252 + }
253 +
254 + .footer-sep {
255 + margin: 0 0.4rem;
256 + }
257 +
258 + /* FOOTNOTES + IMMUTABLE POSTS */
259 +
260 + .post-footnotes {
261 + margin-top: 0.75rem;
262 + padding-top: 0.5rem;
263 + border-top: 1px solid var(--border);
264 + }
265 +
266 + .footnote {
267 + font-size: 0.85rem;
268 + color: var(--text-muted);
269 + margin-bottom: 0.5rem;
270 + }
271 +
272 + .footnote p {
273 + margin: 0.15rem 0;
274 + }
275 +
276 + .footnote-prefix {
277 + font-family: "IBM Plex Mono", monospace;
278 + font-size: 0.75rem;
279 + }
280 +
281 + .footnote-form-toggle {
282 + margin-top: 0.5rem;
283 + }
284 +
285 + .footnote-form-toggle textarea {
286 + width: 100%;
287 + min-height: 60px;
288 + margin-top: 0.5rem;
289 + }
290 +
291 + .badge-unread {
292 + background: var(--highlight);
293 + color: var(--primary-light);
294 + font-size: 0.75rem;
295 + padding: 0.1rem 0.4rem;
296 + border-radius: 3px;
297 + font-family: "IBM Plex Mono", monospace;
298 + }
299 +
300 + tr.unread .thread-title a {
301 + font-weight: 700;
302 + }
303 +
304 + tr.unread .thread-title::before {
305 + content: "";
306 + display: inline-block;
307 + width: 6px;
308 + height: 6px;
309 + background: var(--highlight);
310 + border-radius: 50%;
311 + margin-right: 0.35rem;
312 + vertical-align: middle;
313 + }
314 +
315 + /* Mention indicator on thread listings */
316 + .badge-mention {
317 + color: var(--highlight);
318 + font-size: 0.75rem;
319 + font-weight: 700;
320 + font-family: "IBM Plex Mono", monospace;
321 + background: none;
322 + border: none;
323 + padding: 0;
324 + }
325 +
326 + tr.mentioned {
327 + border-left: 3px solid var(--highlight);
328 + }
329 +
330 + /* Tracking settings */
331 + .tracking-settings {
332 + display: flex;
333 + align-items: center;
334 + gap: 0.75rem;
335 + margin-bottom: 1rem;
336 + font-size: 0.85rem;
337 + }
338 +
339 + .toggle-label {
340 + display: flex;
341 + align-items: center;
342 + gap: 0.4rem;
343 + cursor: pointer;
344 + }
345 +
346 + .toggle-checkbox {
347 + accent-color: var(--highlight);
348 + }
349 +
350 + /* Info/static page sections */
351 + .info-section h3 {
352 + margin-top: 1.5rem;
353 + margin-bottom: 0.5rem;
354 + }
355 +
356 + .info-section ul {
357 + padding-left: 1.5rem;
358 + margin-bottom: 0.75rem;
359 + }
360 +
361 + .info-section li {
362 + margin-bottom: 0.3rem;
363 + line-height: 1.5;
364 + }
365 +
366 + .tag-filter {
367 + margin-bottom: 1rem;
368 + display: flex;
369 + flex-wrap: wrap;
370 + align-items: center;
371 + gap: 0.35rem;
372 + }
373 +
374 + .tag-filter-label {
375 + font-family: "IBM Plex Mono", monospace;
376 + font-size: 0.8rem;
377 + color: var(--text-muted);
378 + margin-right: 0.25rem;
379 + }
380 +
381 + .tag-chip {
382 + display: inline-block;
383 + padding: 0.15rem 0.5rem;
384 + font-size: 0.8rem;
385 + border: 1px solid var(--border);
386 + border-radius: 3px;
387 + text-decoration: none;
388 + color: var(--text-primary);
389 + }
390 +
391 + .tag-chip:hover {
392 + background: var(--surface-muted);
393 + }
394 +
395 + .tag-chip.tag-active {
396 + background: var(--primary-dark);
397 + color: var(--primary-light);
398 + border-color: var(--primary-dark);
399 + }
400 +
401 + .tag-badge {
402 + display: inline-block;
403 + font-size: 0.7rem;
404 + padding: 0.05rem 0.35rem;
405 + background: var(--surface-muted);
406 + border-radius: 2px;
407 + margin-left: 0.25rem;
408 + vertical-align: middle;
409 + font-family: "IBM Plex Mono", monospace;
410 + color: var(--text-muted);
411 + }
412 +
413 + .tag-fieldset {
414 + border: none;
415 + padding: 0;
416 + }
417 +
418 + .tag-fieldset legend {
419 + font-weight: 600;
420 + margin-bottom: 0.35rem;
421 + }
422 +
423 + .tag-checkbox {
424 + display: inline-block;
425 + margin-right: 1rem;
426 + font-size: 0.9rem;
427 + cursor: pointer;
428 + }
429 +
430 + .flag-form-toggle {
431 + margin-top: 0.5rem;
432 + }
433 +
434 + .flag-form {
435 + margin-top: 0.5rem;
436 + display: flex;
437 + flex-direction: column;
438 + gap: 0.35rem;
439 + }
440 +
441 + .flag-form label {
442 + font-size: 0.85rem;
443 + cursor: pointer;
444 + }
445 +
446 + .flag-form textarea {
447 + width: 100%;
448 + min-height: 40px;
449 + margin-top: 0.25rem;
450 + }
451 +
452 + .flag-btn {
453 + align-self: flex-start;
454 + font-size: 0.8rem;
455 + padding: 0.25rem 0.75rem;
456 + }
457 +
458 + .post-removed .post-body {
459 + color: var(--text-muted);
460 + font-style: italic;
461 + }
462 +
463 + .quote-attribution {
464 + display: block;
465 + font-size: 0.8rem;
466 + font-family: "IBM Plex Mono", monospace;
467 + color: var(--text-muted);
468 + margin-top: 0.25rem;
469 + font-style: normal;
470 + }
471 +
472 + .quote-attribution a {
473 + color: var(--text-muted);
474 + text-decoration: none;
475 + }
476 +
477 + .quote-attribution a:hover {
478 + text-decoration: underline;
479 + }
480 +
481 + .quote-btn {
482 + position: absolute;
483 + z-index: 100;
484 + background: var(--surface);
485 + border: 1px solid var(--border);
486 + padding: 0.25rem 0.5rem;
487 + font-size: 0.75rem;
488 + font-family: "IBM Plex Mono", monospace;
489 + cursor: pointer;
490 + }
491 +
492 + .quote-btn:hover {
493 + background: var(--light-background);
494 + }
495 +
496 + /* UTILITY CLASSES */
497 +
498 + .form-inline { display: inline; }
499 + .form-inline-row { display: inline-flex; gap: 0.25rem; flex-direction: row; align-items: center; }
500 + .btn-secondary { text-decoration: none; }
Lines truncated
@@ -1,0 +1,195 @@
1 + /* LINK PREVIEWS */
2 +
3 + .post-link-previews {
4 + margin-top: 0.75rem;
5 + display: flex;
6 + flex-direction: column;
7 + gap: 0.5rem;
8 + }
9 +
10 + .link-preview-card {
11 + display: block;
12 + padding: 0.5rem 0.75rem;
13 + border: 1px solid var(--border);
14 + background: var(--light-background);
15 + text-decoration: none;
16 + color: var(--detail);
17 + transition: background 0.2s ease;
18 + }
19 +
20 + .link-preview-card:hover {
21 + background: var(--surface-muted);
22 + text-decoration: none;
23 + }
24 +
25 + .lp-title {
26 + display: block;
27 + font-family: "IBM Plex Mono", monospace;
28 + font-size: 0.85rem;
29 + font-weight: bold;
30 + }
31 +
32 + .lp-desc {
33 + display: block;
34 + font-size: 0.8rem;
35 + color: var(--text-muted);
36 + margin-top: 0.15rem;
37 + }
38 +
39 + .lp-url {
40 + display: block;
41 + font-size: 0.7rem;
42 + font-family: "IBM Plex Mono", monospace;
43 + color: var(--text-muted);
44 + margin-top: 0.15rem;
45 + }
46 +
47 + /* SEARCH MODAL */
48 +
49 + .search-toggle {
50 + font-family: "IBM Plex Mono", monospace;
51 + font-size: 0.85rem;
52 + color: var(--text-muted);
53 + background: none;
54 + border: 1px solid var(--border);
55 + padding: 0.25rem 0.75rem;
56 + cursor: pointer;
57 + transition: opacity 0.2s ease;
58 + }
59 +
60 + .search-toggle:hover {
61 + opacity: 0.7;
62 + }
63 +
64 + .search-modal {
65 + position: fixed;
66 + top: 0;
67 + left: 0;
68 + right: 0;
69 + bottom: 0;
70 + z-index: 200;
71 + display: flex;
72 + justify-content: center;
73 + padding-top: 10vh;
74 + }
75 +
76 + .search-modal[hidden] {
77 + display: none;
78 + }
79 +
80 + .search-modal-backdrop {
81 + position: fixed;
82 + top: 0;
83 + left: 0;
84 + right: 0;
85 + bottom: 0;
86 + background: rgba(0, 0, 0, 0.3);
87 + }
88 +
89 + .search-modal-content {
90 + position: relative;
91 + width: 100%;
92 + max-width: 600px;
93 + max-height: 70vh;
94 + display: flex;
95 + flex-direction: column;
96 + background: var(--light-background);
97 + border: 1px solid var(--border);
98 + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
99 + z-index: 1;
100 + }
101 +
102 + .search-input {
103 + width: 100%;
104 + padding: 0.75rem 1rem;
105 + font-size: 1rem;
106 + font-family: "IBM Plex Mono", monospace;
107 + background: var(--background);
108 + border: none;
109 + border-bottom: 1px solid var(--border);
110 + color: var(--detail);
111 + outline: none;
112 + }
113 +
114 + .search-input:focus {
115 + border-bottom-color: var(--highlight);
116 + }
117 +
118 + .search-results-container {
119 + overflow-y: auto;
120 + max-height: calc(70vh - 3rem);
121 + }
122 +
123 + .search-results {
124 + list-style: none;
125 + margin: 0;
126 + padding: 0;
127 + }
128 +
129 + .search-result {
130 + border-bottom: 1px solid var(--border);
131 + }
132 +
133 + .search-result:last-child {
134 + border-bottom: none;
135 + }
136 +
137 + .search-result-link {
138 + display: block;
139 + padding: 0.6rem 1rem;
140 + text-decoration: none;
141 + color: var(--detail);
142 + transition: background 0.1s ease;
143 + }
144 +
145 + .search-result-link:hover,
146 + .search-result.search-active .search-result-link {
147 + background: var(--surface-muted);
148 + text-decoration: none;
149 + }
150 +
151 + .search-result-title {
152 + display: block;
153 + font-weight: bold;
154 + font-size: 0.9rem;
155 + }
156 +
157 + .search-result-meta {
158 + display: block;
159 + font-family: "IBM Plex Mono", monospace;
160 + font-size: 0.7rem;
161 + color: var(--text-muted);
162 + margin-top: 0.15rem;
163 + }
164 +
165 + .search-result-snippet {
166 + display: block;
167 + font-size: 0.8rem;
168 + color: var(--text-muted);
169 + margin-top: 0.15rem;
170 + overflow: hidden;
171 + text-overflow: ellipsis;
172 + white-space: nowrap;
173 + }
174 +
175 + .search-empty {
176 + padding: 1.5rem 1rem;
177 + text-align: center;
178 + color: var(--text-muted);
179 + font-family: "IBM Plex Mono", monospace;
180 + font-size: 0.85rem;
181 + }
182 +
183 + /* POST ENDORSEMENTS */
184 +
185 + .endorsed {
186 + color: var(--highlight);
187 + font-weight: bold;
188 + }
189 +
190 + .endorsement-count {
191 + color: var(--detail);
192 + font-size: 0.85rem;
193 + margin-left: 0.25rem;
194 + }
195 +
@@ -1,0 +1,145 @@
1 + /* CHAT ROOM */
2 +
3 + .chat-room {
4 + display: flex;
5 + flex-direction: column;
6 + border: 1px solid var(--border);
7 + background: var(--light-background);
8 + }
9 +
10 + /* The log scrolls, the composer does not. A fixed height rather than a growing
11 + page is what lets the island keep the newest message in view without moving
12 + the thing the reader is typing into. */
13 + .chat-log {
14 + list-style: none;
15 + margin: 0;
16 + padding: 0.5rem;
17 + height: 60vh;
18 + min-height: 15rem;
19 + overflow-y: auto;
20 + overscroll-behavior: contain;
21 + }
22 +
23 + .chat-message {
24 + display: flex;
25 + align-items: baseline;
26 + gap: 0.4rem;
27 + padding: 0.25rem 0.35rem;
28 + line-height: 1.45;
29 + }
30 +
31 + .chat-message:hover {
32 + background: var(--surface-alt);
33 + }
34 +
35 + .chat-avatar {
36 + align-self: center;
37 + border-radius: 50%;
38 + flex-shrink: 0;
39 + }
40 +
41 + .chat-author {
42 + font-weight: bold;
43 + color: var(--detail);
44 + white-space: nowrap;
45 + }
46 +
47 + .chat-time {
48 + font-family: "IBM Plex Mono", monospace;
49 + font-size: 0.75rem;
50 + color: var(--text-muted);
51 + white-space: nowrap;
52 + }
53 +
54 + .chat-body {
55 + flex: 1;
56 + min-width: 0;
57 + overflow-wrap: anywhere;
58 + }
59 +
60 + /* Sent, not yet echoed back by the server. Dimmed rather than hidden: the
61 + message is on screen the instant it is typed, and the dimming is the only
62 + thing saying it is not confirmed. */
63 + .chat-message.is-pending {
64 + opacity: 0.55;
65 + }
66 +
67 + .chat-message.is-failed {
68 + opacity: 1;
69 + border-left: 2px solid var(--danger);
70 + }
71 +
72 + /* Someone named this reader. A tint and a rule rather than anything louder:
73 + chat scrolls, the highlight has to survive being one of forty messages, and
74 + there is deliberately no notification behind it. */
75 + .chat-message.is-mention {
76 + background: var(--surface-alt);
77 + border-left: 2px solid var(--highlight);
78 + }
79 +
80 + .chat-error {
81 + color: var(--danger);
82 + font-size: 0.85rem;
83 + }
84 +
85 + /* Delete, and the retry on a failed send. Both stay out of the way until the
86 + message is under the pointer, and both are always reachable by keyboard. */
87 + .chat-remove,
88 + .chat-retry {
89 + background: none;
90 + border: 1px solid transparent;
91 + color: var(--text-muted);
92 + cursor: pointer;
93 + font-family: "IBM Plex Mono", monospace;
94 + font-size: 0.75rem;
95 + padding: 0 0.3rem;
96 + }
97 +
98 + .chat-remove {
99 + opacity: 0;
100 + margin-left: auto;
101 + }
102 +
103 + .chat-message:hover .chat-remove,
104 + .chat-remove:focus-visible {
105 + opacity: 1;
106 + }
107 +
108 + .chat-remove:hover,
109 + .chat-retry:hover {
110 + color: var(--danger);
111 + border-color: var(--border);
112 + }
113 +
114 + .chat-status {
115 + margin: 0;
116 + padding: 0.35rem 0.6rem;
117 + border-top: 1px solid var(--border);
118 + background: var(--surface-alt);
119 + color: var(--text-muted);
120 + font-size: 0.85rem;
121 + }
122 +
123 + .chat-composer {
124 + display: flex;
125 + gap: 0.5rem;
126 + padding: 0.5rem;
127 + border-top: 1px solid var(--border);
128 + }
129 +
130 + .chat-composer input[type="text"] {
131 + flex: 1;
132 + min-width: 0;
133 + background: var(--input-background);
134 + border: 1px solid var(--border);
135 + color: var(--detail);
136 + padding: 0.45rem 0.6rem;
137 + }
138 +
139 + .chat-notice {
140 + padding: 0.6rem;
141 + border-top: 1px solid var(--border);
142 + color: var(--text-muted);
143 + font-size: 0.9rem;
144 + }
145 +
@@ -1,0 +1,133 @@
1 + /* RESPONSIVE, 768px (tablet) */
2 +
3 + @media (max-width: 768px) {
4 + .container {
5 + padding: 0.75rem;
6 + }
7 +
8 + .padded-page {
9 + padding: 0.75rem;
10 + }
11 +
12 + .form-row {
13 + grid-template-columns: 1fr;
14 + }
15 +
16 + .data-table th,
17 + .data-table td {
18 + padding: 0.35rem 0.5rem;
19 + font-size: 0.8rem;
20 + }
21 +
22 + .directory-table th,
23 + .directory-table td {
24 + padding: 0.35rem 0.5rem;
25 + font-size: 0.8rem;
26 + }
27 +
28 + .page-header {
29 + flex-direction: column;
30 + align-items: flex-start;
31 + gap: 0.5rem;
32 + }
33 +
34 + /* Hamburger menu */
35 + .nav-toggle-label {
36 + display: block;
37 + }
38 +
39 + .site-header nav {
40 + display: none;
41 + position: absolute;
42 + top: 100%;
43 + left: 0;
44 + right: 0;
45 + background: var(--background);
46 + border-bottom: 1px solid var(--border);
47 + padding: 0.75rem 1.25rem;
48 + z-index: 10;
49 + }
50 +
51 + .site-header {
52 + position: relative;
53 + }
54 +
55 + .nav-toggle-checkbox:checked ~ nav {
56 + display: block;
57 + }
58 +
59 + .site-header .nav-links {
60 + flex-direction: column;
61 + gap: 0.75rem;
62 + }
63 +
64 + /* Animate hamburger to X */
65 + .nav-toggle-checkbox:checked ~ .nav-toggle-label span:nth-child(1) {
66 + transform: rotate(45deg) translate(5px, 5px);
67 + }
68 +
69 + .nav-toggle-checkbox:checked ~ .nav-toggle-label span:nth-child(2) {
70 + opacity: 0;
71 + }
72 +
73 + .nav-toggle-checkbox:checked ~ .nav-toggle-label span:nth-child(3) {
74 + transform: rotate(-45deg) translate(5px, -5px);
75 + }
76 +
77 + /* Hide less important columns on mobile */
78 + .data-table .col-author {
79 + display: none;
80 + }
81 +
82 + .directory-table .col-type,
83 + .directory-table .col-items {
84 + display: none;
85 + }
86 +
87 + .col-replies {
88 + width: auto;
89 + }
90 + }
91 +
92 + /* RESPONSIVE, 480px (phone) */
93 +
94 + @media (max-width: 480px) {
95 + .container {
96 + padding: 0.5rem;
97 + }
98 +
99 + .padded-page {
100 + padding: 0.5rem;
101 + }
102 +
103 + h1 {
104 + font-size: 1.6rem;
105 + }
106 +
107 + .toast-container {
108 + left: 0.75rem;
109 + right: 0.75rem;
110 + }
111 +
112 + .toast {
113 + max-width: 100%;
114 + }
115 +
116 + .post-header {
117 + flex-direction: column;
118 + gap: 0.2rem;
119 + }
120 +
121 + /* The composer and the on-screen keyboard together take most of a phone, so
122 + the log gives up height rather than pushing the input off the bottom. */
123 + .chat-log {
124 + height: 55vh;
125 + padding: 0.35rem;
126 + }
127 +
128 + /* No hover on a touch screen, so a delete control that only appears on hover
129 + is a control that does not exist. */
130 + .chat-remove {
131 + opacity: 1;
132 + }
133 + }
@@ -1,0 +1,48 @@
1 + # The site stylesheet
2 +
3 + `static/style.css` is generated. It is the join of the files here, in name
4 + order, produced by `build.rs` and committed.
5 +
6 + Committed rather than gitignored because 1,900 lines of annotated CSS keep their
7 + `git blame` that way, and because `src/static_assets.rs` embeds `static/`
8 + wholesale with `include_dir!`, so the built sheet has to exist on disk at compile
9 + time. Edit a part, never the output.
10 +
11 + The parts live here rather than under `static/` for the same `include_dir!`
12 + reason: anything under `static/` ships, so parts kept there would be served as
13 + well as embedded.
14 +
15 + This sheet uses no `@layer`, so the join is the whole of it and every file here
16 + is standalone-valid CSS on its own.
17 +
18 + ## The parts
19 +
20 + | Part | What it holds |
21 + |---|---|
22 + | `00-foundations.css` | The five `@font-face` blocks, the `:root` palette, reset, base, signature dot, layout. The makeover conversion's whole target: everything it replaces is in this file and nothing else is |
23 + | `10-chrome.css` | Site header and nav, hamburger toggle |
24 + | `20-controls.css` | Buttons, forms |
25 + | `30-listings.css` | Directory and data tables, thread and member columns, pinned rows, badges, breadcrumbs. Badges and breadcrumbs sit here because every one of them is worn by a row in those tables |
26 + | `40-threads.css` | Post item, post actions, deleted posts, OP indicator, draft indicator, reply form, page header |
27 + | `50-furniture.css` | Toast, settings, accessibility, pagination, error pages, empty state, footer, footnotes, immutable posts, mentions, tracking settings, info pages, utility classes. A grab bag of thirteen small sections, none worth its own file |
28 + | `60-search.css` | Link previews, search modal, post endorsements |
29 + | `70-chat.css` | The chat room: scrolling log against a non-growing composer, dimmed un-echoed send, named-reader tint, delete and failed-send retry |
30 + | `90-responsive.css` | The 768px and 480px blocks |
31 +
32 + `90-responsive.css` stays one file. Distributing the two blocks into the feature
33 + files is legal, but each reaches across six of the eight other parts, so it means
34 + splitting each into six fragments and re-proving order for every one, to end up
35 + with eight files instead of nine. Both breakpoints are also off the makeover size
36 + classes (768/480 against the server's 839/599), and this is where that gets
37 + reconciled when the conversion lands.
38 +
39 + ## Contiguity is the guarantee
40 +
41 + Every part is a contiguous line range of the single file this used to be, and
42 + they load in the original order, so the join is byte-identical and the split
43 + changed no rendering.
44 +
45 + That also says what a safe edit is. CSS resolves ties by source order, so moving
46 + a rule between parts can change which of two equal-specificity rules wins. A move
47 + earlier in source order needs its own tie check and its own commit; adding a rule
48 + to the part it belongs in needs neither.