Skip to main content

max / makenotwork

Exorcise sweep: strip AI tells from multithreaded copy and seed content Same uncommitted sweep as the server commit, for multithreaded. Not my edits; verified rather than authored. Connective dashes rewritten in seed.rs content strings, templates, tests, docs, and config. cargo check --all-targets clean, cargo test --lib 162 passed 0 failed. Checked specifically: no <title> separators were touched. The em dash stays as a pure delimiter in mt page titles, which is the exemption Max ruled on 2026-07-27 (D2), since that rule targets connective use rather than delimiters.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-27 21:11 UTC
Signed with PGP, not checked
Commit: 38ba5338ea1d9cc79229dbe2ca7499f653a24c47
Parent: 562950d
20 files changed, +106 insertions, -116 deletions
@@ -17,7 +17,7 @@
17 17 # Platform admin (UUID of the MNW account that can access /_admin)
18 18 # PLATFORM_ADMIN_ID=00000000-0000-0000-0000-000000000000
19 19
20 - # S3 storage (optional — required for image uploads)
20 + # S3 storage (optional, required for image uploads)
21 21 # S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
22 22 # S3_BUCKET=mt-uploads
23 23 # S3_ACCESS_KEY=your-access-key
@@ -57,8 +57,8 @@
57 57 Multithreaded delegates all authentication to MNW via OAuth 2.0 with PKCE. There are no local passwords or signup forms. See [architecture.md § Authentication](docs/architecture.md#4-authentication) for the full flow (PKCE parameters, state nonce validation, retry behavior, session cycling).
58 58
59 59 **Extractors:**
60 - - `MaybeUser(Option<SessionUser>)` — optional auth, infallible (never rejects)
61 - - `PlatformAdmin(SessionUser)` — admin-only, returns 404 to hide admin routes from non-admins
60 + - `MaybeUser(Option<SessionUser>)`: optional auth, infallible (never rejects)
61 + - `PlatformAdmin(SessionUser)`: admin-only, returns 404 to hide admin routes from non-admins
62 62
63 63 ## Route Handlers
64 64
@@ -82,7 +82,7 @@
82 82
83 83 ### Error Handling
84 84
85 - Multithreaded uses `Result<impl IntoResponse, Response>` directly — no centralized `AppError` type. Errors are converted to `Response` inline via helper functions in `routes/helpers.rs`:
85 + Multithreaded uses `Result<impl IntoResponse, Response>` directly, with no centralized `AppError` type. Errors are converted to `Response` inline via helper functions in `routes/helpers.rs`:
86 86
87 87 ```rust
88 88 pub(crate) async fn get_community(db: &PgPool, slug: &str) -> Result<CommunityRow, Response> {
@@ -134,9 +134,9 @@
134 134 ```
135 135
136 136 Every full-page template struct needs:
137 - - `csrf_token: Option<String>` — for the CSRF meta tag
138 - - `session_user: Option<TemplateSessionUser>` — for header login state
139 - - `mnw_base_url: Arc<str>` — for links back to MNW
137 + - `csrf_token: Option<String>`: for the CSRF meta tag
138 + - `session_user: Option<TemplateSessionUser>`: for header login state
139 + - `mnw_base_url: Arc<str>`: for links back to MNW
140 140
141 141 ## Database Layer
142 142
@@ -183,8 +183,8 @@
183 183
184 184 MNW server can call MT's internal API (e.g., to auto-create communities for new projects). These requests use HMAC-SHA256 authentication:
185 185
186 - - `X-Internal-Timestamp` — Unix timestamp (must be within 60 seconds of server time)
187 - - `X-Internal-Signature` — HMAC-SHA256 of `"timestamp\nbody"` using shared secret
186 + - `X-Internal-Timestamp`: Unix timestamp (must be within 60 seconds of server time)
187 + - `X-Internal-Signature`: HMAC-SHA256 of `"timestamp\nbody"` using shared secret
188 188
189 189 The `InternalAuth` extractor validates both before allowing access.
190 190
@@ -1,5 +1,5 @@
1 1 #!/bin/bash
2 - # Multithreaded Deployment Script — Hetzner (alpha-west-1)
2 + # Multithreaded Deployment Script: Hetzner (alpha-west-1)
3 3 # Cross-compiles for x86_64 Linux on macOS, uploads, restarts.
4 4 # Run from the multithreaded project root.
5 5 #
@@ -54,8 +54,8 @@
54 54
55 55 Database access layer. Depends only on mt-core, sqlx, chrono, and uuid. Split into two modules:
56 56
57 - - `queries.rs` -- read-only functions returning `sqlx::FromRow` projection structs shaped for templates
58 - - `mutations.rs` -- write functions (insert, update, upsert, soft delete)
57 + - `queries.rs`: read-only functions returning `sqlx::FromRow` projection structs shaped for templates
58 + - `mutations.rs`: write functions (insert, update, upsert, soft delete)
59 59
60 60 All SQL uses positional parameters (`$1`, `$2`). No ORM, no query builder. Projection structs are purpose-built for each query, not generic domain models.
61 61
@@ -106,15 +106,15 @@
106 106
107 107 ### Extractors
108 108
109 - - `MaybeUser(Option<SessionUser>)` -- infallible, used on all routes. Returns `None` for anonymous users.
110 - - `PlatformAdmin(SessionUser)` -- returns 404 (not 403) to non-admins, hiding admin routes entirely.
109 + - `MaybeUser(Option<SessionUser>)`: infallible, used on all routes. Returns `None` for anonymous users.
110 + - `PlatformAdmin(SessionUser)`: returns 404 (not 403) to non-admins, hiding admin routes entirely.
111 111
112 112 ### Internal API authentication
113 113
114 114 MNW-to-MT requests (community creation, thread cross-posting) bypass OAuth and use HMAC-SHA256:
115 115
116 - - `X-Internal-Timestamp` -- Unix timestamp, rejected if >60 seconds from server time
117 - - `X-Internal-Signature` -- HMAC-SHA256 of `"timestamp\nbody"` using a shared secret
116 + - `X-Internal-Timestamp`: Unix timestamp, rejected if >60 seconds from server time
117 + - `X-Internal-Signature`: HMAC-SHA256 of `"timestamp\nbody"` using a shared secret
118 118
119 119 The `InternalAuth` extractor validates both before passing the request body to the handler. Constant-time comparison prevents timing attacks on the signature.
120 120
@@ -146,7 +146,7 @@
146 146
147 147 ### Community-scoped permissions
148 148
149 - All permissions (roles, bans, mutes) are scoped to a single community. A user can be an owner in one community, a banned user in another, and a regular member in a third. There is no global moderator role -- only the platform admin (a single user ID set via env var) has cross-community authority.
149 + All permissions (roles, bans, mutes) are scoped to a single community. A user can be an owner in one community, a banned user in another, and a regular member in a third. There is no global moderator role. Only the platform admin (a single user ID set via env var) has cross-community authority.
150 150
151 151 ### Immutable post bodies with footnotes and endorsements
152 152
@@ -162,7 +162,7 @@
162 162
163 163 ### Security headers
164 164
165 - Every response includes: Content-Security-Policy (default-src 'self', no frame-ancestors), X-Content-Type-Options (nosniff), X-Frame-Options (DENY), and Cache-Control (private, no-cache by default). CSP is strict -- no inline scripts, no external resources.
165 + Every response includes: Content-Security-Policy (default-src 'self', no frame-ancestors), X-Content-Type-Options (nosniff), X-Frame-Options (DENY), and Cache-Control (private, no-cache by default). CSP is strict: no inline scripts, no external resources.
166 166
167 167 ## 8. Scaling Considerations
168 168
@@ -194,5 +194,5 @@
194 194 Conventions for new migrations:
195 195
196 196 - **Index creation on populated tables must use `CREATE INDEX CONCURRENTLY`.** A plain `CREATE INDEX` takes an `ACCESS EXCLUSIVE` lock and blocks writes for the duration of the build. `CONCURRENTLY` cannot run inside a transaction, so such a migration must contain only that one statement. The historical index migrations (018 GIN full-text/trigram, 023/024/029 on `posts`/`threads`) were applied pre-population and predate this rule; they are safe as-is but are not the pattern to copy.
197 - - **Use `IF NOT EXISTS` / `IF EXISTS`** on `CREATE`/`DROP` so a partially-applied set can be re-run. Later migrations (024+) follow this; some early ones (005/006/008/009/011-019) do not — again, do not retro-edit, just follow the convention going forward.
197 + - **Use `IF NOT EXISTS` / `IF EXISTS`** on `CREATE`/`DROP` so a partially-applied set can be re-run. Later migrations (024+) follow this; some early ones (005/006/008/009/011-019) do not. Again, do not retro-edit, just follow the convention going forward.
198 198 - Prefer additive changes. Destructive changes (`DROP COLUMN`, `ALTER COLUMN ... DROP NOT NULL`) must carry a comment explaining why they are safe (see 027, 032).
@@ -77,20 +77,20 @@
77 77
78 78 ### Detailed Steps
79 79
80 - 1. **User clicks "Log in"** -- browser sends `GET /auth/login`.
81 - 2. **Generate PKCE material** -- 32-byte random verifier (base64url), SHA-256 challenge (base64url), 16-byte state nonce (hex). Verifier and state stored in session.
82 - 3. **Redirect to MNW** -- 302 to `{MNW_BASE_URL}/oauth/authorize` with query params: `response_type=code`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_method=S256`.
83 - 4. **MNW authorize endpoint** -- MNW shows its login/consent UI.
84 - 5. **User authenticates** -- enters credentials on MNW (or is already logged in).
85 - 6. **MNW redirects back** -- 302 to `redirect_uri` with `code` and `state` query params.
86 - 7. **Browser follows redirect** -- `GET /auth/callback?code=...&state=...`.
87 - 8. **Validate state and retrieve verifier** -- compare `state` param against session value (reject on mismatch). Retrieve PKCE verifier from session. Remove both from session.
88 - 9. **Token exchange** -- `POST {MNW_BASE_URL}/oauth/token` with JSON body: `grant_type=authorization_code`, `code`, `redirect_uri`, `code_verifier`, `client_id`. No client_secret (PKCE replaces it).
89 - 10. **Receive access token** -- MNW responds with `{ access_token: "..." }`.
90 - 11. **Fetch user info** -- `GET {MNW_BASE_URL}/oauth/userinfo` with `Authorization: Bearer {access_token}`.
91 - 12. **Receive user profile** -- `{ user_id, username, display_name, avatar_url }`.
92 - 13. **Local processing** -- upsert user into `users` table (keyed on `mnw_account_id`), check `suspended_at` (fail-closed: DB errors block login), save `SessionUser` (user_id, username, display_name) to session, cycle session ID to prevent fixation.
93 - 14. **Redirect home** -- 302 to `/`.
80 + 1. **User clicks "Log in"**: browser sends `GET /auth/login`.
81 + 2. **Generate PKCE material**: 32-byte random verifier (base64url), SHA-256 challenge (base64url), 16-byte state nonce (hex). Verifier and state stored in session.
82 + 3. **Redirect to MNW**: 302 to `{MNW_BASE_URL}/oauth/authorize` with query params: `response_type=code`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_method=S256`.
83 + 4. **MNW authorize endpoint**: MNW shows its login/consent UI.
84 + 5. **User authenticates**: enters credentials on MNW (or is already logged in).
85 + 6. **MNW redirects back**: 302 to `redirect_uri` with `code` and `state` query params.
86 + 7. **Browser follows redirect**: `GET /auth/callback?code=...&state=...`.
87 + 8. **Validate state and retrieve verifier**: compare `state` param against session value (reject on mismatch). Retrieve PKCE verifier from session. Remove both from session.
88 + 9. **Token exchange**: `POST {MNW_BASE_URL}/oauth/token` with JSON body: `grant_type=authorization_code`, `code`, `redirect_uri`, `code_verifier`, `client_id`. No client_secret (PKCE replaces it).
89 + 10. **Receive access token**: MNW responds with `{ access_token: "..." }`.
90 + 11. **Fetch user info**: `GET {MNW_BASE_URL}/oauth/userinfo` with `Authorization: Bearer {access_token}`.
91 + 12. **Receive user profile**: `{ user_id, username, display_name, avatar_url }`.
92 + 13. **Local processing**: upsert user into `users` table (keyed on `mnw_account_id`), check `suspended_at` (fail-closed: DB errors block login), save `SessionUser` (user_id, username, display_name) to session, cycle session ID to prevent fixation.
93 + 14. **Redirect home**: 302 to `/`.
94 94
95 95 ## Session Management
96 96
@@ -150,7 +150,7 @@
150 150 - **Refresh tokens are stored and rotated.** Only the scoped refresh token is
151 151 persisted (`mnw_refresh_token`). `POST /auth/refresh` trades it via
152 152 `grant_type=refresh_token` for a fresh access token **and a new refresh token**
153 - (rotation — the prior token is invalidated; reuse is theft-detectable on the
153 + (rotation: the prior token is invalidated; reuse is theft-detectable on the
154 154 MNW side). The new refresh token replaces the stored one.
155 155 - **A dead refresh token does not log the user out.** If refresh returns
156 156 `invalid_grant` (expired/rotated/revoked), MT clears the stored token and
@@ -163,7 +163,7 @@
163 163 refresh token is issued), uses the returned short-lived token for one userinfo
164 164 fetch, and stores nothing. If the MNW session has lapsed, MNW redirects back
165 165 with `error=login_required` and MT keeps the last-known perks.
166 - - The PKCE verifier is ephemeral -- generated at login initiation, consumed at
166 + - The PKCE verifier is ephemeral: generated at login initiation, consumed at
167 167 callback, never persisted beyond the session.
168 168
169 169 ## CSRF Protection
@@ -196,13 +196,13 @@
196 196
197 197 ## Logout
198 198
199 - `POST /auth/logout` -- flushes the entire session (removes all keys, deletes session row) and redirects to `/`.
199 + `POST /auth/logout` flushes the entire session (removes all keys, deletes session row) and redirects to `/`.
200 200
201 201 ## Key Paths
202 202
203 - - `src/auth.rs` -- PKCE helpers, session user, login/callback/logout handlers
204 - - `src/config.rs` -- `Config::from_env()`, all OAuth-related env vars
205 - - `src/csrf.rs` -- CSRF token generation, middleware, constant-time comparison
206 - - `src/internal_auth.rs` -- HMAC-SHA256 auth for MNW-to-MT internal API (separate from OAuth)
207 - - `src/main.rs` -- session store setup, session layer config, middleware stack
208 - - `deploy/deploy-hetzner.sh`, `deploy/deploy.sh` -- deploy scripts (production env vars are provisioned on-server, not committed to the repo)
203 + - `src/auth.rs`: PKCE helpers, session user, login/callback/logout handlers
204 + - `src/config.rs`: `Config::from_env()`, all OAuth-related env vars
205 + - `src/csrf.rs`: CSRF token generation, middleware, constant-time comparison
206 + - `src/internal_auth.rs`: HMAC-SHA256 auth for MNW-to-MT internal API (separate from OAuth)
207 + - `src/main.rs`: session store setup, session layer config, middleware stack
208 + - `deploy/deploy-hetzner.sh`, `deploy/deploy.sh`: deploy scripts (production env vars are provisioned on-server, not committed to the repo)
@@ -3,7 +3,7 @@
3 3 "version": "0.0.0",
4 4 "private": true,
5 5 "type": "module",
6 - "description": "Multithreaded frontend — TypeScript compiled by tsc into ../static/dist/ as browser ESM and loaded by Askama templates. One build-time dependency (typescript); no bundler, no runtime deps. Mirrors MNW/server/frontend. The legacy static/mt.js IIFE is not part of this build and is not being migrated.",
6 + "description": "Multithreaded frontend: TypeScript compiled by tsc into ../static/dist/ as browser ESM and loaded by Askama templates. One build-time dependency (typescript); no bundler, no runtime deps. Mirrors MNW/server/frontend. The legacy static/mt.js IIFE is not part of this build and is not being migrated.",
7 7 "scripts": {
8 8 "build": "tsc",
9 9 "typecheck": "tsc --noEmit",
@@ -25,7 +25,7 @@
25 25 pool,
26 26 "Rust Programming",
27 27 "rust",
28 - Some("All things Rust — language, ecosystem, tooling."),
28 + Some("All things Rust: language, ecosystem, tooling."),
29 29 )
30 30 .await;
31 31
@@ -152,7 +152,7 @@
152 152 pool,
153 153 rust_general,
154 154 users[0].id,
155 - "Welcome — read before posting",
155 + "Welcome, read before posting",
156 156 true,
157 157 false,
158 158 )
@@ -352,7 +352,7 @@
352 352 ],
353 353 ),
354 354 (
355 - "CPU overload — how do you deal with it?",
355 + "CPU overload: how do you deal with it?",
356 356 "My sessions keep hitting 100% CPU. Running a Ryzen 5 3600 with 16GB RAM. Is it time to upgrade or am I doing something wrong?",
357 357 &[
358 358 "Freeze tracks you are not actively working on. Most DAWs support this.",
@@ -552,7 +552,7 @@
552 552 "How to get better at arrangement",
553 553 "My loops sound great but full tracks feel flat. Any tips for arrangement?",
554 554 &[
555 - "Study arrangements of songs you like. Map them out on paper. Intro, verse, chorus, bridge — note what enters and exits.",
555 + "Study arrangements of songs you like. Map them out on paper. Intro, verse, chorus, bridge. Note what enters and exits.",
556 556 "Contrast. If the chorus is dense, strip back the verse. Dynamics come from what you remove.",
557 557 "Energy curve. Every section should either build tension or release it. Never stay flat.",
558 558 "Reference tracks. Drop one into your session and match the structure.",
@@ -747,7 +747,7 @@
747 747 ],
748 748 ),
749 749 (
750 - "Mixing with headphones — tips and tricks",
750 + "Mixing with headphones: tips and tricks",
751 751 "For those who primarily mix on headphones, what are your strategies?",
752 752 &[
753 753 "Crossfeed plugin. Simulates speaker crosstalk so panning sounds more natural.",
@@ -756,7 +756,7 @@
756 756 ],
757 757 ),
758 758 (
759 - "The loudness war is over — right?",
759 + "The loudness war is over, right?",
760 760 "With streaming normalization, does anyone still master to -6 LUFS?",
761 761 &[
762 762 "EDM and hip hop still push hard. Genre expectations matter more than streaming targets.",
@@ -809,7 +809,7 @@
809 809 ],
810 810 ),
811 811 (
812 - "Granular synthesis — practical uses?",
812 + "Granular synthesis: practical uses?",
813 813 "Granular synths look cool but I cannot figure out when to actually use them. What do you use granular for?",
814 814 &[
815 815 "Pads and textures. Take a field recording, granularize it, and you have a unique atmosphere.",
@@ -887,7 +887,7 @@
887 887 "Phase distortion synthesis",
888 888 "Casio CZ-series used phase distortion. Anyone still using this technique?",
889 889 &[
890 - "Underrated. It is different from FM — smoother harmonics, easier to control.",
890 + "Underrated. It is different from FM: smoother harmonics, easier to control.",
891 891 "There are a few VST recreations. CZ V from Arturia is faithful to the originals.",
892 892 "You can sort of approximate it in any synth by modulating the phase of one oscillator with another, but true PD has a distinct character.",
893 893 ],
@@ -903,7 +903,7 @@
903 903 ],
904 904 ),
905 905 (
906 - "Additive synthesis — is it practical?",
906 + "Additive synthesis: is it practical?",
907 907 "Additive seems powerful in theory but is it actually useful for sound design?",
908 908 &[
909 909 "Very useful for evolving pads and organ-like tones. Direct control over individual harmonics.",
@@ -16,7 +16,7 @@
16 16 }
17 17 })();
18 18
19 - /* --- TOAST NOTIFICATIONS */
19 + /* TOAST NOTIFICATIONS */
20 20
21 21 function showToast(message, type) {
22 22 var container = document.getElementById('notifications');
@@ -35,7 +35,7 @@
35 35 showToast(evt.detail.message || 'Action completed', evt.detail.type || 'info');
36 36 });
37 37
38 - /* --- INLINE FORM ERRORS
38 + /* INLINE FORM ERRORS
39 39
40 40 A failed submit (422) keeps the user on the page with their input intact and
41 41 shows a persistent error attached to the form, and when the handler names
@@ -100,7 +100,7 @@
100 100 }, 6000);
101 101 });
102 102
103 - /* --- HTMX FORM STATE (loading buttons) */
103 + /* HTMX FORM STATE (loading buttons) */
104 104
105 105 document.body.addEventListener('htmx:beforeRequest', function(evt) {
106 106 var form = evt.detail.elt.closest('form');
@@ -118,7 +118,7 @@
118 118 }
119 119 });
120 120
121 - /* --- SEARCH MODAL */
121 + /* SEARCH MODAL */
122 122
123 123 function openSearchModal() {
124 124 var modal = document.getElementById('search-modal');
@@ -174,7 +174,7 @@
174 174 });
175 175 })();
176 176
177 - /* --- KEYBOARD SHORTCUTS */
177 + /* KEYBOARD SHORTCUTS */
178 178
179 179 document.addEventListener('keydown', function(e) {
180 180 // Search shortcut: / key (when not in input)
@@ -198,7 +198,7 @@
198 198 }
199 199 });
200 200
201 - /* --- IMAGE CLICK: open full size in new tab */
201 + /* IMAGE CLICK: open full size in new tab */
202 202
203 203 document.addEventListener('click', function(e) {
204 204 var img = e.target;
@@ -207,7 +207,7 @@
207 207 }
208 208 });
209 209
210 - /* --- NAV TOGGLE */
210 + /* NAV TOGGLE */
211 211
212 212 document.addEventListener('click', function(e) {
213 213 var toggle = document.getElementById('nav-toggle');
@@ -216,7 +216,7 @@
216 216 }
217 217 });
218 218
219 - /* --- FORM POST WITH CSRF */
219 + /* FORM POST WITH CSRF */
220 220
221 221 document.addEventListener('submit', function(e) {
222 222 // Another handler may have already cancelled this submit; respect it.
@@ -266,7 +266,7 @@
266 266 }
267 267 });
268 268
269 - /* --- TOAST FROM URL PARAMETER */
269 + /* TOAST FROM URL PARAMETER */
270 270
271 271 (function() {
272 272 var p = new URLSearchParams(window.location.search).get('toast');
@@ -276,7 +276,7 @@
276 276 }
277 277 })();
278 278
279 - /* --- DRAFT AUTO-SAVE */
279 + /* DRAFT AUTO-SAVE */
280 280
281 281 (function() {
282 282 var body = document.getElementById('body') || document.getElementById('reply-body');
@@ -348,7 +348,7 @@
348 348 // above), so a validation failure keeps the user's text.
349 349 })();
350 350
351 - /* --- LOCAL UNREAD TRACKING (category pages) */
351 + /* LOCAL UNREAD TRACKING (category pages) */
352 352
353 353 (function() {
354 354 if (localStorage.getItem('mt_tracking_enabled') === 'false') return;
@@ -387,7 +387,7 @@
387 387 });
388 388 })();
389 389
390 - /* --- TRACKING OPT-OUT TOGGLE */
390 + /* TRACKING OPT-OUT TOGGLE */
391 391
392 392 (function() {
393 393 var checkbox = document.getElementById('tracking-opt-out');
@@ -403,7 +403,7 @@
403 403 });
404 404 })();
405 405
406 - /* --- IMAGE UPLOAD (drag-and-drop + paste) */
406 + /* IMAGE UPLOAD (drag-and-drop + paste) */
407 407
408 408 (function() {
409 409 var textarea = document.getElementById('body') || document.getElementById('reply-body');
@@ -493,7 +493,7 @@
493 493 });
494 494 })();
495 495
496 - /* --- SELECT-TO-QUOTE (thread pages) */
496 + /* SELECT-TO-QUOTE (thread pages) */
497 497
498 498 (function() {
499 499 var quoteBtn = null;
@@ -1,6 +1,6 @@
1 1 /* Multithreaded, Forum Stylesheet. Adapted from Makenot.work design language */
2 2
3 - /* --- FONTS */
3 + /* FONTS */
4 4
5 5 @font-face {
6 6 font-family: "Young Serif";
@@ -41,7 +41,7 @@
41 41 font-display: swap;
42 42 }
43 43
44 - /* --- VARIABLES */
44 + /* VARIABLES */
45 45
46 46 :root {
47 47 --background: #ede8e1;
@@ -66,7 +66,7 @@
66 66 --focus-ring: #6c5ce7;
67 67 }
68 68
69 - /* --- RESET */
69 + /* RESET */
70 70
71 71 * {
72 72 margin: 0;
@@ -74,7 +74,7 @@
74 74 box-sizing: border-box;
75 75 }
76 76
77 - /* --- BASE */
77 + /* BASE */
78 78
79 79 body {
80 80 margin: 0;
@@ -118,7 +118,7 @@
118 118 color: var(--highlight);
119 119 }
120 120
121 - /* --- LAYOUT */
121 + /* LAYOUT */
122 122
123 123 .container {
124 124 max-width: 1200px;
@@ -130,7 +130,7 @@
130 130 padding: 1.25rem;
131 131 }
132 132
133 - /* --- SITE HEADER & NAV */
133 + /* SITE HEADER & NAV */
134 134
135 135 .site-header {
136 136 display: flex;
@@ -216,7 +216,7 @@
216 216 transition: transform 0.2s ease, opacity 0.2s ease;
217 217 }
218 218
219 - /* --- BUTTONS */
219 + /* BUTTONS */
220 220
221 221 button {
222 222 color: var(--detail);
@@ -266,7 +266,7 @@
266 266 opacity: 0.8;
267 267 }
268 268
269 - /* --- FORMS */
269 + /* FORMS */
270 270
271 271 form {
272 272 display: flex;
@@ -393,7 +393,7 @@
393 393 width: 6rem;
394 394 }
395 395
396 - /* --- DIRECTORY TABLE (forum home) */
396 + /* DIRECTORY TABLE (forum home) */
397 397
398 398 .directory-table {
399 399 width: 100%;
@@ -482,7 +482,7 @@
482 482 color: var(--text-muted);
483 483 }
484 484
485 - /* --- DATA TABLE (dense thread listings) */
485 + /* DATA TABLE (dense thread listings) */
486 486
487 487 .data-table {
488 488 width: 100%;
@@ -602,7 +602,7 @@
602 602 background: var(--border);
603 603 }
604 604
605 - /* --- BADGES */
605 + /* BADGES */
606 606
607 607 .badge {
608 608 display: inline-block;
@@ -675,7 +675,7 @@
675 675 border: 1px solid var(--border);
676 676 }
677 677
678 - /* --- BREADCRUMBS */
678 + /* BREADCRUMBS */
679 679
680 680 .breadcrumb {
681 681 font-size: 0.8rem;
@@ -698,7 +698,7 @@
698 698 opacity: 0.5;
699 699 }
700 700
701 - /* --- POST ITEM (thread view) */
701 + /* POST ITEM (thread view) */
702 702
703 703 .post-list {
704 704 display: flex;
@@ -831,7 +831,7 @@
831 831 background: var(--light-background);
832 832 }
833 833
834 - /* --- DRAFT INDICATOR */
834 + /* DRAFT INDICATOR */
835 835
836 836 .draft-indicator {
837 837 font-family: "IBM Plex Mono", monospace;
@@ -849,7 +849,7 @@
849 849 color: var(--danger);
850 850 }
851 851
852 - /* --- REPLY FORM */
852 + /* REPLY FORM */
853 853
854 854 .reply-section {
855 855 padding: 1rem;
@@ -860,7 +860,7 @@
860 860 margin-bottom: 0.75rem;
861 861 }
862 862
863 - /* --- PAGE HEADER (title + actions row) */
863 + /* PAGE HEADER (title + actions row) */
864 864
865 865 .page-header {
866 866 display: flex;
@@ -880,7 +880,7 @@
880 880 margin: 0;
881 881 }
882 882
883 - /* --- TOAST NOTIFICATIONS */
883 + /* TOAST NOTIFICATIONS */
884 884
885 885 .toast-container {
886 886 position: fixed;
@@ -949,7 +949,7 @@
949 949 to { transform: translateX(100%); }
950 950 }
951 951
952 - /* --- SETTINGS PAGE */
952 + /* SETTINGS PAGE */
953 953
954 954 .settings-section {
955 955 margin-bottom: 2rem;
@@ -1025,7 +1025,7 @@
1025 1025 white-space: nowrap;
1026 1026 }
1027 1027
1028 - /* --- ACCESSIBILITY */
1028 + /* ACCESSIBILITY */
1029 1029
1030 1030 .skip-to-main {
1031 1031 position: absolute;
@@ -1047,7 +1047,7 @@
1047 1047 outline-offset: 2px;
1048 1048 }
1049 1049
1050 - /* --- PAGINATION */
1050 + /* PAGINATION */
1051 1051
1052 1052 .pagination {
1053 1053 display: flex;
@@ -1077,7 +1077,7 @@
1077 1077 color: var(--text-muted);
1078 1078 }
1079 1079
1080 - /* --- ERROR PAGES */
1080 + /* ERROR PAGES */
1081 1081
1082 1082 .error-page {
1083 1083 text-align: center;
@@ -1095,7 +1095,7 @@
1095 1095 margin-bottom: 1.5rem;
1096 1096 }
1097 1097
1098 - /* --- EMPTY STATE */
1098 + /* EMPTY STATE */
1099 1099
1100 1100 .empty-state {
1101 1101 text-align: center;
@@ -1104,7 +1104,7 @@
1104 1104 font-family: "IBM Plex Mono", monospace;
1105 1105 }
1106 1106
1107 - /* --- SITE FOOTER */
1107 + /* SITE FOOTER */
1108 1108
1109 1109 .site-footer {
1110 1110 text-align: center;
@@ -1137,7 +1137,7 @@
1137 1137 margin: 0 0.4rem;
1138 1138 }
1139 1139
1140 - /* --- FOOTNOTES + IMMUTABLE POSTS */
1140 + /* FOOTNOTES + IMMUTABLE POSTS */
1141 1141
1142 1142 .post-footnotes {
1143 1143 margin-top: 0.75rem;
@@ -1375,7 +1375,7 @@
1375 1375 background: var(--light-background);
1376 1376 }
1377 1377
1378 - /* --- UTILITY CLASSES */
1378 + /* UTILITY CLASSES */
1379 1379
1380 1380 .form-inline { display: inline; }
1381 1381 .form-inline-row { display: inline-flex; gap: 0.25rem; flex-direction: row; align-items: center; }
@@ -1403,7 +1403,7 @@
1403 1403 .community-desc { margin-bottom: 0.5rem; color: var(--text-muted); }
1404 1404 .activity-category { color: var(--text-muted); font-size: 0.85rem; }
1405 1405
1406 - /* --- LINK PREVIEWS */
1406 + /* LINK PREVIEWS */
1407 1407
1408 1408 .post-link-previews {
1409 1409 margin-top: 0.75rem;
@@ -1449,7 +1449,7 @@
1449 1449 margin-top: 0.15rem;
1450 1450 }
1451 1451
1452 - /* --- SEARCH MODAL */
1452 + /* SEARCH MODAL */
1453 1453
1454 1454 .search-toggle {
1455 1455 font-family: "IBM Plex Mono", monospace;
@@ -1585,7 +1585,7 @@
1585 1585 font-size: 0.85rem;
1586 1586 }
1587 1587
1588 - /* --- POST ENDORSEMENTS */
1588 + /* POST ENDORSEMENTS */
1589 1589
1590 1590 .endorsed {
1591 1591 color: var(--highlight);
@@ -1598,7 +1598,7 @@
1598 1598 margin-left: 0.25rem;
1599 1599 }
1600 1600
1601 - /* --- RESPONSIVE, 768px (tablet) */
1601 + /* RESPONSIVE, 768px (tablet) */
1602 1602
1603 1603 @media (max-width: 768px) {
1604 1604 .container {
@@ -1689,7 +1689,7 @@
1689 1689 }
1690 1690 }
1691 1691
1692 - /* --- RESPONSIVE, 480px (phone) */
1692 + /* RESPONSIVE, 480px (phone) */
1693 1693
1694 1694 @media (max-width: 480px) {
1695 1695 .container {