Skip to main content

max / makenotwork

Put every width breakpoint on a SizeClass boundary, or delete it Seven ad-hoc thresholds across the three hand-written stylesheets (480, 600, 640, 700, 768, 800, 900) carried 23 blocks. None of them came from anywhere; they were guesses about the world, repeated. Most of the blocks did not want a threshold. Five are dimensional and now say what they mean without a query: the do and contrast rails, the custom page editor's form-beside-preview, the item feature grid, and the wizard's pricing and content-choice cards are all repeat(auto-fit, minmax(N, 1fr)), where N is a claim about the content that can be checked by looking at one card. Two more were dead: the project page's items and tiers grids already collapsed on their own, since an auto-fill floor of 300px and 280px cannot hold two columns at 480px wide, so the overrides forcing 1fr never fired. The seventeen that remain are structural, and they change what exists rather than how big it is: a sidebar becoming a toggle-revealed panel, a table shedding columns, the nav becoming a hamburger. Those map onto Compact (max-width: 599px) and not-Expanded (max-width: 839px). Four bands move as a result. 481-599 picks up the phone rules, 769-839 picks up the tablet rules, 600 exactly loses the rules that used to include it, and 840-899 keeps the discover sidebar it used to lose. No generator path exists: a media condition cannot read a custom property and @custom-media has shipped nowhere, so every threshold is a hand-typed literal. build.rs now reads SizeClass directly and fails the build on any width condition that is not a boundary, which is what stops the next one from being typed and what breaks loudly if makeover-geometry moves a line. Also fixes what editing the per-page sheets exposed: wizard.css and media-player.css were linked with no ?v= and were not in the fingerprint, so changes to either served stale to any browser holding a cached copy. Both join the hash, and the seven pages that link them go through a generated _sheet.html macro, the same shape as _island.html.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-03 00:27 UTC
Signed with PGP, not checked
Commit: 95308d53f279ecfaa9177ab435528c2254cb4c4a
Parent: b2ca5a4
14 files changed, +161 insertions, -55 deletions
M .gitignore +1
@@ -45,6 +45,7 @@
45 45 # Generated template partials (build.rs output)
46 46 server/templates/_head_assets.html
47 47 server/templates/_island.html
48 + server/templates/_sheet.html
48 49
49 50 # Generated rustdoc output
50 51 server/rustdoc-out/
@@ -5205,6 +5205,7 @@
5205 5205 "log",
5206 5206 "makeover",
5207 5207 "makeover-build",
5208 + "makeover-geometry",
5208 5209 "memmap2",
5209 5210 "metrics",
5210 5211 "metrics-exporter-prometheus",
@@ -174,6 +174,10 @@
174 174 # (makeover-webview). The same generator GO and BB run; only the output paths
175 175 # differ, since the server serves its stylesheets rather than bundling them.
176 176 makeover-build = "0.7.0"
177 + # Read directly for SizeClass, which the breakpoint guard in build.rs compares
178 + # the stylesheets against. makeover-build does not re-export it, so the pin
179 + # lives here and has to track the one makeover-build resolves.
180 + makeover-geometry = "0.6"
177 181
178 182 [dev-dependencies]
179 183 tower = { version = "0.5.3", features = ["util"] }
@@ -35,12 +35,19 @@
35 35 makeover_build::layout_css("static/layout.css", &makeover_build::Emit::default());
36 36 println!("cargo::rerun-if-changed=build.rs");
37 37
38 + check_breakpoints();
39 +
38 40 // --- Static asset fingerprinting ---
39 41 // Hash the content of key static files to produce a version suffix.
40 42 // When any watched file changes, URLs in templates get a new ?v= param,
41 43 // busting browser caches automatically.
42 44 let static_files = [
43 45 "static/style.css",
46 + // The two per-page sheets. Linked from page templates rather than from
47 + // _head_assets.html, which is why they were outside the fingerprint
48 + // and served stale to any browser holding a cached copy.
49 + "static/wizard.css",
50 + "static/media-player.css",
44 51 "static/htmx.min.js",
45 52 "static/upload.js",
46 53 "static/passkey.js",
@@ -111,6 +118,108 @@
111 118 "#
112 119 .replace("__VER__", version);
113 120 write_if_changed(Path::new("templates/_island.html"), &island_partial);
121 +
122 + // Per-page stylesheet loader, same idea as the island macro. wizard.css and
123 + // media-player.css are linked by the pages that need them rather than by
124 + // _head_assets.html, so this is how they get the content hash.
125 + let sheet_partial = r#"{% macro sheet(name) -%}
126 + <link rel="stylesheet" href="/static/{{ name }}?v=__VER__">
127 + {%- endmacro %}
128 + "#
129 + .replace("__VER__", version);
130 + write_if_changed(Path::new("templates/_sheet.html"), &sheet_partial);
131 + }
132 +
133 + /// The hand-written stylesheets. Ordered, so the guard reports the same way
134 + /// twice. `geometry.css` and `layout.css` are excluded: they are generated.
135 + const HAND_WRITTEN_CSS: [&str; 3] = [
136 + "static/style.css",
137 + "static/wizard.css",
138 + "static/media-player.css",
139 + ];
140 +
141 + /// Fail the build on any width breakpoint that is not a `SizeClass` boundary.
142 + ///
143 + /// A media condition cannot read a custom property and `@custom-media` has
144 + /// shipped nowhere, so every threshold in the stylesheets is a hand-typed
145 + /// literal and there is no generator path. The guard is the substitute: bump
146 + /// makeover-geometry to a release that moves a boundary and the build breaks
147 + /// here, rather than the layout breaking quietly in a browser.
148 + ///
149 + /// Only the numbers are checked, not the surrounding syntax. That accepts
150 + /// `(min-width: 600px) and (max-width: 839px)` without having to parse it, and
151 + /// still catches the thing worth catching: a number nobody can trace to the
152 + /// scale.
153 + fn check_breakpoints() {
154 + use makeover_geometry::SizeClass;
155 +
156 + let compact_max = SizeClass::Medium.min_px() - 1;
157 + let expanded_min = SizeClass::Expanded.min_px();
158 + let allowed = [
159 + compact_max,
160 + SizeClass::Medium.min_px(),
161 + expanded_min - 1,
162 + expanded_min,
163 + ];
164 +
165 + let mut strays: Vec<String> = Vec::new();
166 + for path in HAND_WRITTEN_CSS {
167 + println!("cargo::rerun-if-changed={path}");
168 + let Ok(css) = fs::read_to_string(path) else {
169 + continue;
170 + };
171 + for (n, line) in css.lines().enumerate() {
172 + for px in width_conditions(line) {
173 + if !allowed.contains(&px) {
174 + strays.push(format!("{path}:{}: (…-width: {px}px)", n + 1));
175 + }
176 + }
177 + }
178 + }
179 +
180 + assert!(
181 + strays.is_empty(),
182 + "stylesheet width breakpoints that are not SizeClass boundaries \
183 + ({}, {}, {}, {}px):\n {}\n\
184 + Either move the rule to a boundary, or make it dimensional so it \
185 + needs no threshold at all: a grid wants \
186 + repeat(auto-fit, minmax(<content floor>, 1fr)) and a size wants \
187 + clamp(). A threshold is for what appears and disappears.",
188 + allowed[0],
189 + allowed[1],
190 + allowed[2],
191 + allowed[3],
192 + strays.join("\n ")
193 + );
194 + }
195 +
196 + /// Every pixel value used as a `min-width` or `max-width` media feature on one
197 + /// line. Deliberately narrow: it reads `width` features and ignores the
198 + /// `width` property, `min-width`/`max-width` declarations, and every other
199 + /// number in the sheet.
200 + fn width_conditions(line: &str) -> Vec<u16> {
201 + let mut found = Vec::new();
202 + for (i, _) in line.match_indices("-width:") {
203 + let prefix = &line[..i];
204 + if !(prefix.ends_with("min") || prefix.ends_with("max")) {
205 + continue;
206 + }
207 + // A media feature is parenthesised; the property form never is.
208 + let opened = prefix.trim_end_matches(['m', 'i', 'n', 'a', 'x']);
209 + if !opened.ends_with('(') {
210 + continue;
211 + }
212 + let rest = &line[i + "-width:".len()..];
213 + let digits: String = rest
214 + .trim_start()
215 + .chars()
216 + .take_while(char::is_ascii_digit)
217 + .collect();
218 + if let Ok(px) = digits.parse::<u16>() {
219 + found.push(px);
220 + }
221 + }
222 + found
114 223 }
115 224
116 225 /// Write `contents` to `path` only if it differs, to avoid needless rebuilds.
@@ -388,7 +388,7 @@
388 388 }
389 389
390 390 /* Responsive */
391 - @media (max-width: 600px) {
391 + @media (max-width: 599px) {
392 392 .media-title {
393 393 font-size: var(--text-title);
394 394 }
@@ -1705,7 +1705,7 @@
1705 1705
1706 1706 .item-page .features-grid {
1707 1707 display: grid;
1708 - grid-template-columns: repeat(2, 1fr);
1708 + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1709 1709 gap: var(--gap-pane);
1710 1710 }
1711 1711 .item-page .version-number { font-size: var(--text-lead); }
@@ -1720,12 +1720,11 @@
1720 1720
1721 1721 .item-page .item-footer a { color: var(--content); }
1722 1722
1723 - @media (max-width: 768px) {
1723 + @media (max-width: 839px) {
1724 1724 .item-page .item-layout { grid-template-columns: 1fr; }
1725 - .item-page .features-grid { grid-template-columns: 1fr; }
1726 1725 }
1727 1726
1728 - @media (max-width: 480px) {
1727 + @media (max-width: 599px) {
1729 1728 .item-page .item-title { font-size: var(--text-head); }
1730 1729 .item-page .item-price { font-size: var(--text-title); }
1731 1730 .item-page .item-meta { grid-template-columns: 1fr; gap: var(--gap-peer); }
@@ -1900,12 +1899,10 @@
1900 1899 opacity: 0.7;
1901 1900 }
1902 1901
1903 - @media (max-width: 480px) {
1902 + @media (max-width: 599px) {
1904 1903 .project-page .store-title { font-size: var(--text-title); }
1905 - .project-page .items-grid { grid-template-columns: 1fr; }
1906 1904 .project-page a.item-thumbnail { height: 200px; }
1907 1905 .project-page .store-description { font-size: var(--text-body); }
1908 - .project-page .tiers-grid { grid-template-columns: 1fr; }
1909 1906 }
1910 1907
1911 1908 /* Library downloads page (templates/pages/library_downloads.html).
@@ -2050,7 +2047,7 @@
2050 2047
2051 2048 .library-page .item-footer a { color: var(--content); }
2052 2049
2053 - @media (max-width: 600px) {
2050 + @media (max-width: 599px) {
2054 2051 .library-page .library-header { grid-template-columns: 1fr; }
2055 2052 .library-page .library-cover img { max-width: 200px; }
2056 2053 }
@@ -2290,7 +2287,7 @@
2290 2287 .article-page .blog-nav a { color: var(--content); text-decoration: none; }
2291 2288 .article-page .blog-nav a:hover { text-decoration: underline; }
2292 2289
2293 - @media (max-width: 600px) {
2290 + @media (max-width: 599px) {
2294 2291 .article-page .article-title { font-size: var(--text-title); }
2295 2292 .article-page .article-deck { font-size: var(--text-lead); }
2296 2293 .article-page .article-body { font-size: var(--text-body); }
@@ -3309,7 +3306,7 @@
3309 3306
3310 3307 .purchase-page .checkout-footer a { color: var(--content); }
3311 3308
3312 - @media (max-width: 480px) {
3309 + @media (max-width: 599px) {
3313 3310 .purchase-page .container { padding: 0 var(--gap-section); }
3314 3311 .purchase-page .item-summary { flex-direction: column; }
3315 3312 .purchase-page .item-thumbnail { width: 100%; height: 160px; }
@@ -3414,7 +3411,7 @@
3414 3411
3415 3412 .feed-page .pagination a:hover { background: var(--surface-sunken); }
3416 3413
3417 - @media (max-width: 600px) {
3414 + @media (max-width: 599px) {
3418 3415 .feed-page .table-header,
3419 3416 .feed-page .table-row { grid-template-columns: 1fr 70px; }
3420 3417 .feed-page .table-header span:nth-child(1),
@@ -3786,7 +3783,7 @@
3786 3783 .settings-nav a:hover { opacity: 1; }
3787 3784 .settings-nav a.is-selected { opacity: 1; background: var(--surface-sunken); }
3788 3785 .settings-body { flex: 1; min-width: 0; }
3789 - @media (max-width: 768px) {
3786 + @media (max-width: 839px) {
3790 3787 .settings-layout { flex-direction: column; gap: var(--gap-section); }
3791 3788 .settings-nav { display: flex; flex-wrap: wrap; gap: 0; min-width: 0; }
3792 3789 .settings-nav a { padding: var(--gap-peer) var(--gap-group); font-size: var(--text-note); }
@@ -3851,7 +3848,7 @@
3851 3848 border-color: var(--primary-dark);
3852 3849 }
3853 3850 .library-page .feed-pagination a:hover { background: var(--surface-sunken); }
3854 - @media (max-width: 600px) {
3851 + @media (max-width: 599px) {
3855 3852 .library-page .feed-table-header,
3856 3853 .library-page .feed-table-row {
3857 3854 grid-template-columns: 1fr 70px;
@@ -3906,7 +3903,7 @@
3906 3903 padding: var(--gap-peer) var(--gap-section);
3907 3904 font-size: var(--text-note);
3908 3905 }
3909 - @media (max-width: 768px) {
3906 + @media (max-width: 839px) {
3910 3907 .dashboard-user-page .project-card { flex-direction: column; }
3911 3908 .dashboard-user-page .project-actions { margin-top: var(--gap-section); }
3912 3909 }
@@ -3974,7 +3971,7 @@
3974 3971
3975 3972 .dashboard-item-page .section-header h2 { font-size: var(--text-subhead); }
3976 3973
3977 - @media (max-width: 768px) {
3974 + @media (max-width: 839px) {
3978 3975 .dashboard-item-page .section-header {
3979 3976 flex-direction: column;
3980 3977 align-items: flex-start;
@@ -6115,7 +6112,9 @@
6115 6112 margin: var(--gap-page) 0 var(--gap-pane);
6116 6113 padding: 0;
6117 6114 display: grid;
6118 - grid-template-columns: repeat(3, 1fr);
6115 + /* A do-card stops being readable under about 200px, which is a fact about
6116 + the card. Three across is what that yields at full width. */
6117 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
6119 6118 gap: var(--gap-section);
6120 6119 width: 100%;
6121 6120 }
@@ -6165,12 +6164,6 @@
6165 6164 border-bottom-color: currentColor;
6166 6165 }
6167 6166
6168 - @media (max-width: 640px) {
6169 - .do-rail {
6170 - grid-template-columns: 1fr;
6171 - }
6172 - }
6173 -
6174 6167 .do-card--wide {
6175 6168 margin-top: var(--gap-page);
6176 6169 width: 100%;
@@ -6181,7 +6174,9 @@
6181 6174 margin: var(--gap-section) 0 0;
6182 6175 padding: 0;
6183 6176 display: grid;
6184 - grid-template-columns: repeat(4, 1fr);
6177 + /* Title plus a short link list, so it holds up narrower than a do-card.
6178 + Four across at full width. */
6179 + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
6185 6180 gap: var(--gap-section);
6186 6181 width: 100%;
6187 6182 }
@@ -6223,12 +6218,6 @@
6223 6218 border-bottom-color: currentColor;
6224 6219 }
6225 6220
6226 - @media (max-width: 640px) {
6227 - .contrast-rail {
6228 - grid-template-columns: 1fr;
6229 - }
6230 - }
6231 -
6232 6221 .explore-links {
6233 6222 display: flex;
6234 6223 flex-wrap: wrap;
@@ -7173,7 +7162,7 @@
7173 7162 .site-footer-links a { margin: 0 var(--gap-section); color: inherit; text-decoration: none; }
7174 7163 .site-footer-links a:hover { text-decoration: underline; }
7175 7164
7176 - @media (max-width: 768px) {
7165 + @media (max-width: 839px) {
7177 7166 /* Git browser mobile */
7178 7167 .git-ref-bar { flex-wrap: wrap; gap: var(--gap-peer); }
7179 7168 .git-repo-name { font-size: var(--text-subhead); }
@@ -7306,7 +7295,7 @@
7306 7295 }
7307 7296
7308 7297 /* Doc pages, mobile */
7309 - @media (max-width: 600px) {
7298 + @media (max-width: 599px) {
7310 7299 .doc-title { font-size: var(--text-title); }
7311 7300 .doc-body { font-size: var(--text-body); }
7312 7301 .docs-index-title { font-size: var(--text-title); }
@@ -7316,7 +7305,7 @@
7316 7305 RESPONSIVE, 480px (phone)
7317 7306 =========================================== */
7318 7307
7319 - @media (max-width: 480px) {
7308 + @media (max-width: 599px) {
7320 7309 .container {
7321 7310 padding: var(--gap-section);
7322 7311 }
@@ -7584,7 +7573,7 @@
7584 7573 margin-left: var(--gap-bound);
7585 7574 }
7586 7575
7587 - @media (max-width: 900px) {
7576 + @media (max-width: 839px) {
7588 7577 .discover-filter-toggle { display: inline-block; }
7589 7578 .discover-layout { grid-template-columns: 1fr; }
7590 7579 .discover-sidebar { display: none; }
@@ -7600,7 +7589,7 @@
7600 7589 .table-header.projects-header span:nth-child(3), .table-header.projects-header span:nth-child(4) { display: none; }
7601 7590 }
7602 7591
7603 - @media (max-width: 480px) {
7592 + @media (max-width: 599px) {
7604 7593 .table-header, .table-row { grid-template-columns: 1fr 60px; }
7605 7594 .table-row-item { grid-template-columns: 1fr 30px; }
7606 7595 .table-row-item .table-row-link { grid-template-columns: 1fr 60px; }
@@ -11321,7 +11310,7 @@
11321 11310 outline-offset: 2px;
11322 11311 }
11323 11312
11324 - @media (max-width: 640px) {
11313 + @media (max-width: 599px) {
11325 11314 .carousel-nav { width: 2rem; height: 2rem; font-size: var(--text-subhead); }
11326 11315 }
11327 11316
@@ -11333,7 +11322,9 @@
11333 11322 .cp-editor-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--gap-section); flex-wrap: wrap; }
11334 11323 .cp-editor-actions { display: flex; gap: var(--gap-section); }
11335 11324 .cp-intro { color: var(--content-muted); margin: var(--gap-bound) 0 var(--gap-section); }
11336 - .cp-editor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-section); align-items: start; }
11325 + /* Form beside preview. A code textarea below about 380px is not editable, so
11326 + that number is the pane's, not the viewport's, and the pair stacks itself. */
11327 + .cp-editor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: var(--gap-section); align-items: start; }
11337 11328 .cp-editor-form { display: flex; flex-direction: column; gap: var(--gap-peer); }
11338 11329 .cp-code { width: 100%; min-height: 220px; font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--text-note); line-height: 1.4; resize: vertical; }
11339 11330 .cp-editor-buttons { display: flex; align-items: center; gap: var(--gap-section); margin-top: var(--gap-peer); }
@@ -11352,8 +11343,6 @@
11352 11343 .cp-status.cp-err { color: var(--warning); }
11353 11344 .cp-reset { margin-top: var(--gap-pane); }
11354 11345 .cp-locked-banner { margin: var(--gap-peer) 0 var(--gap-section); padding: var(--gap-section); border: 1px solid var(--warning); border-radius: var(--radius-control); background: color-mix(in oklch, var(--warning) 12%, var(--surface-page)); }
11355 - @media (max-width: 800px) { .cp-editor-grid { grid-template-columns: 1fr; } }
11356 -
11357 11346 /* Layout-shift fix (audit Run 17 Frontend): reserve space for a cover image
11358 11347 whose class didn't already set an aspect-ratio / fixed size. */
11359 11348 .proj-image-preview img { width: 120px; height: 120px; object-fit: cover; }
@@ -11431,6 +11420,6 @@
11431 11420 /* The narrow list layouts drop columns rather than wrap, and .row-info is the
11432 11421 column that survives. Keeping the note there would crowd the title on a
11433 11422 phone, where the tier heading above still carries the important half. */
11434 - @media (max-width: 700px) {
11423 + @media (max-width: 599px) {
11435 11424 .table-row .row-match-note { display: none; }
11436 11425 }
@@ -212,7 +212,7 @@
212 212
213 213 .pricing-cards {
214 214 display: grid;
215 - grid-template-columns: repeat(3, 1fr);
215 + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
216 216 gap: var(--gap-section);
217 217 margin-bottom: var(--gap-pane);
218 218 }
@@ -225,7 +225,7 @@
225 225
226 226 .content-choice-cards {
227 227 display: grid;
228 - grid-template-columns: 1fr 1fr;
228 + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
229 229 gap: var(--gap-section);
230 230 margin-bottom: var(--gap-pane);
231 231 }
@@ -475,7 +475,7 @@
475 475
476 476 /* Responsive */
477 477
478 - @media (max-width: 768px) {
478 + @media (max-width: 839px) {
479 479 .wizard-layout {
480 480 flex-direction: column;
481 481 }
@@ -505,11 +505,6 @@
505 505 white-space: nowrap;
506 506 }
507 507
508 - .pricing-cards,
509 - .content-choice-cards {
510 - grid-template-columns: 1fr;
511 - }
512 -
513 508 .type-grid {
514 509 grid-template-columns: repeat(2, 1fr);
515 510 }
@@ -1,4 +1,5 @@
1 1 {% extends "base.html" %}
2 + {% import "_sheet.html" as sheet %}
2 3
3 4 {% block title %}{{ item.title }} - Makenotwork{% endblock %}
4 5
@@ -48,7 +49,7 @@
48 49 }
49 50 }
50 51 </script>
51 - <link rel="stylesheet" href="/static/media-player.css">
52 + {% call sheet::sheet("media-player.css") %}{% endcall %}
52 53 {% endblock %}
53 54
54 55 {% block content %}
@@ -1,11 +1,12 @@
1 1 {% extends "base.html" %}
2 + {% import "_sheet.html" as sheet %}
2 3 {% import "_island.html" as island %}
3 4
4 5 {% block title %}{{ item.title }} - Library - Makenotwork{% endblock %}
5 6
6 7 {% block head %}
7 8 <meta name="robots" content="noindex">
8 - <link rel="stylesheet" href="/static/media-player.css">
9 + {% call sheet::sheet("media-player.css") %}{% endcall %}
9 10 {% endblock %}
10 11
11 12 {% block content %}
@@ -1,11 +1,12 @@
1 1 {% extends "base.html" %}
2 + {% import "_sheet.html" as sheet %}
2 3 {% import "_island.html" as island %}
3 4
4 5 {% block title %}{{ item.title }} - Library - Makenotwork{% endblock %}
5 6
6 7 {% block head %}
7 8 <meta name="robots" content="noindex">
8 - <link rel="stylesheet" href="/static/media-player.css">
9 + {% call sheet::sheet("media-player.css") %}{% endcall %}
9 10 {% endblock %}
10 11
11 12 {% block content %}
@@ -1,4 +1,5 @@
1 1 {% extends "base.html" %}
2 + {% import "_sheet.html" as sheet %}
2 3
3 4 {% block title %}{{ item.title }} - Makenotwork{% endblock %}
4 5
@@ -43,7 +44,7 @@
43 44 }{% endif %}
44 45 }
45 46 </script>
46 - <link rel="stylesheet" href="/static/media-player.css">
47 + {% call sheet::sheet("media-player.css") %}{% endcall %}
47 48 {% endblock %}
48 49
49 50 {% block content %}
@@ -1,10 +1,11 @@
1 1 {% extends "base.html" %}
2 + {% import "_sheet.html" as sheet %}
2 3
3 4 {% block title %}New Item - Makenotwork{% endblock %}
4 5 {% block body_attrs %} class="padded-page"{% endblock %}
5 6
6 7 {% block head %}
7 - <link rel="stylesheet" href="/static/wizard.css">
8 + {% call sheet::sheet("wizard.css") %}{% endcall %}
8 9 {% endblock %}
9 10
10 11 {% block content %}
@@ -1,10 +1,11 @@
1 1 {% extends "base.html" %}
2 + {% import "_sheet.html" as sheet %}
2 3
3 4 {% block title %}Join - Makenotwork{% endblock %}
4 5 {% block body_attrs %} class="padded-page"{% endblock %}
5 6
6 7 {% block head %}
7 - <link rel="stylesheet" href="/static/wizard.css">
8 + {% call sheet::sheet("wizard.css") %}{% endcall %}
8 9 {% endblock %}
9 10
10 11 {% block content %}
@@ -1,10 +1,11 @@
1 1 {% extends "base.html" %}
2 + {% import "_sheet.html" as sheet %}
2 3
3 4 {% block title %}New Project - Makenotwork{% endblock %}
4 5 {% block body_attrs %} class="padded-page"{% endblock %}
5 6
6 7 {% block head %}
7 - <link rel="stylesheet" href="/static/wizard.css">
8 + {% call sheet::sheet("wizard.css") %}{% endcall %}
8 9 {% endblock %}
9 10
10 11 {% block content %}