Skip to main content

max / makenotwork

docs: drop makenotwork.html pitch; add substitute_dir CLI The standalone pitch deck duplicated content already covered by the live site and was bound to drift. Removing it. substitute_dir is a docengine bin that walks a markdown tree and applies Assumptions::substitute to every file. Intended for the _private/docs/ mirror, which is not part of the live site-docs pipeline. --check mode exits non-zero on any change or unresolved placeholder so it can run in CI later. Code spans and fenced blocks are already preserved by substitute(), so syntax-example placeholders pass through unchanged. Initial run over _private/docs/mnw (114 files): zero changes, zero errors.
Author: Max Johnson <me@maxj.phd> · 2026-06-04 02:49 UTC
Commit: e53c5667e21ca5450b8d812566a288fea6afcf2e
Parent: da2d8e4
2 files changed, +118 insertions, -500 deletions
@@ -1,867 +0,0 @@
1 - <!DOCTYPE html>
2 - <html lang="en">
3 - <head>
4 - <meta charset="UTF-8">
5 - <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 - <title>Makenot.work — Pitch</title>
7 - <style>
8 - @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Lato:wght@300;400;700&family=Young+Serif&display=swap');
9 -
10 - :root {
11 - --bg: #ede8e1;
12 - --bg-warm: #e4ddd3;
13 - --bg-deep: #d8cfc3;
14 - --text: #3d3530;
15 - --text-secondary: #5d524a;
16 - --text-muted: #8a7e74;
17 - --accent: #6c5ce7;
18 - --accent-light: #8577ed;
19 - --green: #4caf50;
20 - --red: #d62828;
21 - --border: #c9c0b4;
22 - --border-dark: #a89d91;
23 - }
24 -
25 - * { margin: 0; padding: 0; box-sizing: border-box; }
26 -
27 - @page { size: letter; margin: 0; }
28 -
29 - body {
30 - font-family: 'Lato', sans-serif;
31 - color: var(--text);
32 - background: var(--bg);
33 - line-height: 1.6;
34 - -webkit-print-color-adjust: exact;
35 - print-color-adjust: exact;
36 - }
37 -
38 - .page {
39 - width: 8.5in;
40 - min-height: 11in;
41 - margin: 0 auto;
42 - padding: 0.5in 0.75in;
43 - background: var(--bg);
44 - page-break-after: always;
45 - position: relative;
46 - }
47 -
48 - .page:last-child { page-break-after: auto; }
49 -
50 - /* ── Hero ── */
51 -
52 - .hero {
53 - text-align: center;
54 - padding: 0.25in 0 0.2in;
55 - border-bottom: 2px solid var(--border);
56 - margin-bottom: 0.2in;
57 - }
58 -
59 - .hero h1 {
60 - font-family: 'Young Serif', serif;
61 - font-size: 44px;
62 - font-weight: 400;
63 - color: var(--text);
64 - margin-bottom: 4px;
65 - letter-spacing: -0.5px;
66 - }
67 -
68 - .hero h1 .dot { color: var(--accent); }
69 -
70 - .hero .tagline {
71 - font-family: 'IBM Plex Mono', monospace;
72 - font-size: 14px;
73 - color: var(--text-secondary);
74 - letter-spacing: 0.3px;
75 - }
76 -
77 - .hero .sub {
78 - font-size: 12.5px;
79 - color: var(--text-muted);
80 - margin-top: 8px;
81 - }
82 -
83 - /* ── Sections ── */
84 -
85 - h2 {
86 - font-family: 'IBM Plex Mono', monospace;
87 - font-size: 13.5px;
88 - font-weight: 600;
89 - text-transform: uppercase;
90 - letter-spacing: 1.5px;
91 - color: var(--accent);
92 - margin-bottom: 10px;
93 - padding-bottom: 4px;
94 - border-bottom: 1px solid var(--border);
95 - }
96 -
97 - h3 {
98 - font-family: 'IBM Plex Mono', monospace;
99 - font-size: 12px;
100 - font-weight: 600;
101 - color: var(--text);
102 - margin-bottom: 4px;
103 - margin-top: 12px;
104 - }
105 -
106 - h3:first-child { margin-top: 0; }
107 -
108 - .intro {
109 - font-size: 14.5px;
110 - line-height: 1.7;
111 - margin-bottom: 0.2in;
112 - }
113 -
114 - /* ── Stats ── */
115 -
116 - .stats {
117 - display: grid;
118 - grid-template-columns: repeat(4, 1fr);
119 - gap: 10px;
120 - margin-bottom: 0.2in;
121 - }
122 -
123 - .stat {
124 - text-align: center;
125 - background: var(--bg-warm);
126 - border: 1px solid var(--border);
127 - border-radius: 6px;
128 - padding: 10px 6px;
129 - }
130 -
131 - .stat .num {
132 - font-family: 'Young Serif', serif;
133 - font-size: 26px;
134 - color: var(--accent);
135 - display: block;
136 - line-height: 1;
137 - margin-bottom: 3px;
138 - }
139 -
140 - .stat .label {
141 - font-family: 'IBM Plex Mono', monospace;
142 - font-size: 9px;
143 - color: var(--text-muted);
144 - text-transform: uppercase;
145 - letter-spacing: 0.4px;
146 - }
147 -
148 - /* ── Feature grid ── */
149 -
150 - .features {
151 - display: grid;
152 - grid-template-columns: 1fr 1fr;
153 - gap: 12px;
154 - margin-bottom: 0.18in;
155 - }
156 -
157 - .feature-card {
158 - background: var(--bg-warm);
159 - border: 1px solid var(--border);
160 - border-radius: 6px;
161 - padding: 11px 13px;
162 - }
163 -
164 - .feature-card h3 {
165 - margin: 0 0 4px 0;
166 - font-size: 11.5px;
167 - color: var(--accent);
168 - }
169 -
170 - .feature-card ul { list-style: none; padding: 0; }
171 -
172 - .feature-card li {
173 - font-size: 11px;
174 - line-height: 1.4;
175 - color: var(--text-secondary);
176 - padding: 1.5px 0 1.5px 12px;
177 - position: relative;
178 - }
179 -
180 - .feature-card li::before {
181 - content: '';
182 - position: absolute;
183 - left: 0;
184 - top: 7px;
185 - width: 5px;
186 - height: 5px;
187 - background: var(--accent);
188 - border-radius: 50%;
189 - }
190 -
191 - /* ── Feature section (full width, 2-col) ── */
192 -
193 - .feature-section { margin-bottom: 0.16in; }
194 -
195 - .feature-section ul {
196 - list-style: none;
197 - padding: 0;
198 - columns: 2;
199 - column-gap: 22px;
200 - }
201 -
202 - .feature-section li {
203 - font-size: 11.5px;
204 - line-height: 1.4;
205 - color: var(--text-secondary);
206 - padding: 2px 0 2px 12px;
207 - position: relative;
208 - break-inside: avoid;
209 - }
210 -
211 - .feature-section li::before {
212 - content: '';
213 - position: absolute;
214 - left: 0;
215 - top: 7.5px;
216 - width: 5px;
217 - height: 5px;
218 - background: var(--accent);
219 - border-radius: 50%;
220 - }
221 -
222 - /* ── Highlight box ── */
223 -
224 - .highlight-box {
225 - background: var(--bg-warm);
226 - border-left: 3px solid var(--accent);
227 - padding: 10px 14px;
228 - margin-bottom: 0.16in;
229 - border-radius: 0 6px 6px 0;
230 - }
231 -
232 - .highlight-box p {
233 - font-size: 12px;
234 - line-height: 1.55;
235 - color: var(--text-secondary);
236 - }
237 -
238 - .highlight-box strong { color: var(--text); }
239 -
240 - /* ── Pricing grid ── */
241 -
242 - .pricing {
243 - display: grid;
244 - grid-template-columns: repeat(4, 1fr);
245 - gap: 10px;
246 - margin-bottom: 0.18in;
247 - }
248 -
249 - .tier {
250 - background: var(--bg-warm);
251 - border: 1px solid var(--border);
252 - border-radius: 6px;
253 - padding: 11px 10px;
254 - text-align: center;
255 - }
256 -
257 - .tier .tier-price {
258 - font-family: 'Young Serif', serif;
259 - font-size: 24px;
260 - color: var(--accent);
261 - display: block;
262 - line-height: 1;
263 - margin-bottom: 2px;
264 - }
265 -
266 - .tier .tier-period {
267 - font-family: 'IBM Plex Mono', monospace;
268 - font-size: 9px;
269 - color: var(--text-muted);
270 - text-transform: uppercase;
271 - display: block;
272 - margin-bottom: 5px;
273 - }
274 -
275 - .tier .tier-name {
276 - font-family: 'IBM Plex Mono', monospace;
277 - font-size: 11px;
278 - font-weight: 600;
279 - color: var(--text);
280 - display: block;
281 - margin-bottom: 4px;
282 - }
283 -
284 - .tier .tier-desc {
285 - font-size: 10px;
286 - color: var(--text-secondary);
287 - line-height: 1.35;
288 - }
289 -
290 - /* ── Monetization models ── */
291 -
292 - .models {
293 - display: grid;
294 - grid-template-columns: repeat(4, 1fr);
295 - gap: 10px;
296 - margin-bottom: 0.18in;
297 - }
298 -
299 - .model {
300 - background: var(--bg-warm);
301 - border: 1px solid var(--border);
302 - border-radius: 6px;
303 - padding: 10px 11px;
304 - text-align: center;
305 - }
306 -
307 - .model .model-name {
308 - font-family: 'IBM Plex Mono', monospace;
309 - font-size: 11px;
310 - font-weight: 600;
311 - color: var(--text);
312 - display: block;
313 - margin-bottom: 4px;
314 - }
315 -
316 - .model .model-desc {
317 - font-size: 10px;
318 - color: var(--text-muted);
319 - line-height: 1.35;
320 - }
321 -
322 - /* ── Content types ── */
323 -
324 - .content-types {
325 - display: grid;
326 - grid-template-columns: repeat(5, 1fr);
327 - gap: 8px;
328 - margin-bottom: 0.18in;
329 - }
330 -
331 - .content-type {
332 - background: var(--bg-warm);
333 - border: 1px solid var(--border);
334 - border-radius: 5px;
335 - padding: 7px 6px;
336 - text-align: center;
337 - }
338 -
339 - .content-type .type-name {
340 - font-family: 'IBM Plex Mono', monospace;
341 - font-size: 10.5px;
342 - font-weight: 600;
343 - color: var(--text);
344 - display: block;
345 - margin-bottom: 2px;
346 - }
347 -
348 - .content-type .type-detail {
349 - font-size: 9px;
350 - color: var(--text-muted);
351 - line-height: 1.3;
352 - }
353 -
354 - /* ── Comparison table ── */
355 -
356 - .comparison {
357 - width: 100%;
358 - border-collapse: collapse;
359 - margin-bottom: 0.16in;
360 - font-size: 11px;
361 - }
362 -
363 - .comparison th {
364 - font-family: 'IBM Plex Mono', monospace;
365 - font-size: 10px;
366 - font-weight: 600;
367 - text-align: left;
368 - padding: 6px 7px;
369 - background: var(--bg-deep);
370 - color: var(--text);
371 - border-bottom: 1px solid var(--border-dark);
372 - }
373 -
374 - .comparison td {
375 - padding: 5px 7px;
376 - border-bottom: 1px solid var(--border);
377 - color: var(--text-secondary);
378 - vertical-align: top;
379 - }
380 -
381 - .comparison .check { color: var(--green); font-weight: 700; font-size: 12px; }
382 - .comparison .dash { color: var(--text-muted); }
383 - .comparison tr:last-child td { border-bottom: none; }
384 -
385 - /* ── Two-column ── */
386 -
387 - .two-col {
388 - display: grid;
389 - grid-template-columns: 1fr 1fr;
390 - gap: 12px;
391 - margin-bottom: 0.16in;
392 - }
393 -
394 - .two-col .col {
395 - background: var(--bg-warm);
396 - border: 1px solid var(--border);
397 - border-radius: 6px;
398 - padding: 11px 13px;
399 - }
400 -
401 - .two-col .col h3 {
402 - margin: 0 0 4px 0;
403 - font-size: 11.5px;
404 - color: var(--accent);
405 - }
406 -
407 - .two-col .col p {
408 - font-size: 11px;
409 - line-height: 1.45;
410 - color: var(--text-secondary);
411 - }
412 -
413 - /* ── Footer ── */
414 -
415 - .footer {
416 - position: absolute;
417 - bottom: 0.4in;
418 - left: 0.75in;
419 - right: 0.75in;
420 - display: flex;
421 - justify-content: space-between;
422 - align-items: center;
423 - padding-top: 8px;
424 - border-top: 1px solid var(--border);
425 - }
426 -
427 - .footer .left, .footer .right {
428 - font-family: 'IBM Plex Mono', monospace;
429 - font-size: 10px;
430 - color: var(--text-muted);
431 - }
432 -
433 - .footer-inline {
434 - display: flex;
435 - justify-content: space-between;
436 - align-items: center;
437 - padding-top: 8px;
438 - border-top: 1px solid var(--border);
439 - margin-top: auto;
440 - }
441 -
442 - .footer-inline .left, .footer-inline .right {
443 - font-family: 'IBM Plex Mono', monospace;
444 - font-size: 10px;
445 - color: var(--text-muted);
446 - }
447 -
448 - @media print { body { background: white; } .page { box-shadow: none; } }
449 - @media screen { body { background: #ccc; padding: 20px 0; } .page { box-shadow: 0 2px 20px rgba(0,0,0,0.15); margin-bottom: 20px; } }
450 - </style>
451 - </head>
452 - <body>
453 -
454 - <!-- ═══════════════════════════════════════════════════════════════════════ -->
455 - <!-- PAGE 1 -->
456 - <!-- ═══════════════════════════════════════════════════════════════════════ -->
457 - <div class="page">
458 -
459 - <div class="hero">
460 - <h1>Makenot<span class="dot">.</span>work</h1>
461 - <div class="tagline">Fair distribution for creatives of all kinds.</div>
462 - <div class="sub">A creator platform where the only fee is Stripe's ~3% processing. No revenue share, no lock-in, full data export.</div>
463 - </div>
464 -
465 - <div class="stats">
466 - <div class="stat">
467 - <span class="num">0%</span>
468 - <span class="label">Platform fee</span>
469 - </div>
470 - <div class="stat">
471 - <span class="num">10</span>
472 - <span class="label">Content types</span>
473 - </div>
474 - <div class="stat">
475 - <span class="num">4</span>
476 - <span class="label">Pricing models</span>
477 - </div>
478 - <div class="stat">
479 - <span class="num">100%</span>
480 - <span class="label">Data exportable</span>
481 - </div>
482 - </div>
483 -
484 - <div class="intro">
485 - Makenot.work is a platform for selling digital work. Music, software, writing, video, courses, sample packs, plugins, presets &mdash; whatever you make. You pay a flat monthly fee based on your content type. Your buyers pay Stripe's processing fee and nothing else. We never take a percentage of your revenue.
486 - </div>
487 -
488 - <h2>Creator Tiers</h2>
489 -
490 - <div class="pricing">
491 - <div class="tier">
492 - <span class="tier-price">$16</span>
493 - <span class="tier-period">/ month</span>
494 - <span class="tier-name">Basic</span>
495 - <div class="tier-desc">Text, blogs, newsletters, community forums.</div>
496 - </div>
497 - <div class="tier">
498 - <span class="tier-price">$24</span>
499 - <span class="tier-period">/ month</span>
500 - <span class="tier-name">Small Files</span>
Lines truncated
@@ -0,0 +1,118 @@
1 + //! Apply `Assumptions::substitute` to every markdown file under a
2 + //! directory tree. Intended for the `_private/docs/` mirror, which
3 + //! references business numbers but isn't part of the public site-docs
4 + //! pipeline that runs substitution at boot.
5 + //!
6 + //! Usage:
7 + //!
8 + //! substitute_dir <assumptions.toml> <docs_root> [--check]
9 + //!
10 + //! --check Don't write; exit non-zero if any file would change or
11 + //! any placeholder failed to resolve.
12 + //!
13 + //! Code spans / fenced code blocks are already preserved by
14 + //! `Assumptions::substitute` (they hold syntax examples, not live
15 + //! placeholders), so docs that only mention `{{ derived.X }}` in
16 + //! backticks come through unchanged.
17 +
18 + use docengine::Assumptions;
19 + use std::fs;
20 + use std::path::{Path, PathBuf};
21 + use std::process::ExitCode;
22 +
23 + fn main() -> ExitCode {
24 + let args: Vec<String> = std::env::args().skip(1).collect();
25 + if args.len() < 2 || args.iter().any(|a| a == "-h" || a == "--help") {
26 + eprintln!(
27 + "usage: substitute_dir <assumptions.toml> <docs_root> [--check]"
28 + );
29 + return ExitCode::from(2);
30 + }
31 + let assumptions_path = PathBuf::from(&args[0]);
32 + let root = PathBuf::from(&args[1]);
33 + let check_only = args.iter().any(|a| a == "--check");
34 +
35 + let assumptions = match Assumptions::load(&assumptions_path) {
36 + Ok(a) => a,
37 + Err(e) => {
38 + eprintln!("load {}: {e}", assumptions_path.display());
39 + return ExitCode::from(2);
40 + }
41 + };
42 + if let Err(e) = assumptions.validate() {
43 + eprintln!("validate {}: {e}", assumptions_path.display());
44 + return ExitCode::from(2);
45 + }
46 +
47 + let mut files = Vec::new();
48 + if let Err(e) = collect_markdown(&root, &mut files) {
49 + eprintln!("walk {}: {e}", root.display());
50 + return ExitCode::from(2);
51 + }
52 +
53 + let mut changed: Vec<PathBuf> = Vec::new();
54 + let mut errors: Vec<(PathBuf, String)> = Vec::new();
55 + for path in &files {
56 + let body = match fs::read_to_string(path) {
57 + Ok(s) => s,
58 + Err(e) => {
59 + errors.push((path.clone(), format!("read: {e}")));
60 + continue;
61 + }
62 + };
63 + match assumptions.substitute(&body) {
64 + Ok(resolved) if resolved != body => {
65 + if check_only {
66 + changed.push(path.clone());
67 + } else if let Err(e) = fs::write(path, &resolved) {
68 + errors.push((path.clone(), format!("write: {e}")));
69 + } else {
70 + changed.push(path.clone());
71 + }
72 + }
73 + Ok(_) => {}
74 + Err(e) => errors.push((path.clone(), e.to_string())),
75 + }
76 + }
77 +
78 + println!(
79 + "scanned {} markdown files under {}",
80 + files.len(),
81 + root.display()
82 + );
83 + if !changed.is_empty() {
84 + println!(
85 + "{} {} file(s):",
86 + if check_only { "would change" } else { "wrote" },
87 + changed.len()
88 + );
89 + for p in &changed {
90 + println!(" {}", p.display());
91 + }
92 + }
93 + if !errors.is_empty() {
94 + eprintln!("{} error(s):", errors.len());
95 + for (p, e) in &errors {
96 + eprintln!(" {}: {e}", p.display());
97 + }
98 + return ExitCode::from(1);
99 + }
100 + if check_only && !changed.is_empty() {
101 + return ExitCode::from(1);
102 + }
103 + ExitCode::SUCCESS
104 + }
105 +
106 + fn collect_markdown(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
107 + for entry in fs::read_dir(dir)? {
108 + let entry = entry?;
109 + let path = entry.path();
110 + let ft = entry.file_type()?;
111 + if ft.is_dir() {
112 + collect_markdown(&path, out)?;
113 + } else if ft.is_file() && path.extension().is_some_and(|e| e == "md") {
114 + out.push(path);
115 + }
116 + }
117 + Ok(())
118 + }