Skip to main content

max / makenotwork

Give the embeds makeover's values instead of copies of them The five embed templates carry their own <style> block because they render inside an iframe on somebody else's page and cannot link a stylesheet. That part was right. What was wrong is that being self-contained meant the values had been copied by hand, so nothing kept them in step, and they had already drifted: #5a4bd6 was the button hover violet in all five and matches no token anywhere in the tree. The real --action-hover is #7a6cf8. That is the visible change here. Both halves already had a delivery path, so this is wiring rather than generation. Colour arrives at render time from theming::theme_css(None), the same makeover-resolved :root block every page injects, platform default only: serving a creator's chosen theme into a third-party page is a separate product question. Spacing arrives at compile time from a new static/embed-geometry.css, written by build.rs from makeover-geometry at Pointer density and included with include_str!. Pointer density and no @media block, deliberately. An embed body is height: 100vh inside an iframe the host page sized, so growing the gaps on a coarse pointer clips rather than reflows and the host author never sees it. Revisit if embeds ever gain a resize protocol. Padding and gaps move onto the gap scale, which also puts them on rem for the first time, so they now respect a reader's root font size. Every value has an exact token except the 14px button and body padding, which snaps to --gap-section at 12px. Fixed dimensions the host page was built around are untouched, as are radius and the type scale. Two tests guard it: one asserts no template writes a hex literal of its own once the injected :root blocks are stripped, the other asserts both blocks were injected at all, so the first cannot pass by stripping everything.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-06 15:11 UTC
Signed with PGP, not checked
Commit: 71386fc5c632e82e2f46986628d3acfa8474b1a4
Parent: 355dea4
11 files changed, +305 insertions, -55 deletions
M .gitignore +1
@@ -12,6 +12,7 @@
12 12 # makeover-webview; the crates are the source, a checked-in copy would drift)
13 13 server/static/geometry.css
14 14 server/static/layout.css
15 + server/static/embed-geometry.css
15 16
16 17 # Environment files (contain secrets)
17 18 server/.env
M server/build.rs +18 -1
@@ -33,6 +33,19 @@
33 33 // on. GO passes `.ui-mode-mobile` because it has one.
34 34 makeover_build::geometry_css("static/geometry.css", None);
35 35 makeover_build::layout_css("static/layout.css", &makeover_build::Emit::default());
36 +
37 + // The embeds get their own copy of the spacing layer, because they are
38 + // served into third-party iframes and cannot link a stylesheet. Pointer
39 + // density only, and no `@media` block: an embed body is `height: 100vh`
40 + // inside an iframe the host page sized, so growing the gaps on a coarse
41 + // pointer clips rather than reflows, and the host author never sees it
42 + // happen. Revisit if embeds ever gain a resize protocol.
43 + fs::write(
44 + "static/embed-geometry.css",
45 + makeover_geometry::geometry_css_vars(makeover_geometry::Density::Pointer),
46 + )
47 + .expect("write static/embed-geometry.css");
48 +
36 49 println!("cargo::rerun-if-changed=build.rs");
37 50
38 51 check_breakpoints();
@@ -68,7 +81,11 @@
68 81 // Same treatment for the two generated stylesheets: read-only, so a bump
69 82 // of makeover-geometry or makeover-webview busts `?v=` without the build
70 83 // script watching a file it writes itself.
71 - for path in ["static/geometry.css", "static/layout.css"] {
84 + for path in [
85 + "static/geometry.css",
86 + "static/layout.css",
87 + "static/embed-geometry.css",
88 + ] {
72 89 if let Ok(content) = fs::read(path) {
73 90 content.hash(&mut hasher);
74 91 }
@@ -4,6 +4,26 @@
4 4
5 5 use askama::Template;
6 6
7 + /// The spacing layer the embeds inline, written by `build.rs` from
8 + /// makeover-geometry at Pointer density.
9 + ///
10 + /// An embed cannot link a stylesheet: it renders inside an iframe on somebody
11 + /// else's page, so its whole design system has to arrive in the document. That
12 + /// is why these five templates carry a `<style>` block when nothing else in the
13 + /// tree does. What it must not mean is that the values were copied by hand,
14 + /// which is what it meant until this existed.
15 + pub const EMBED_GEOMETRY_CSS: &str = include_str!("../../static/embed-geometry.css");
16 +
17 + /// The colour layer for an embed: the platform default theme, resolved through
18 + /// makeover the same way every page's is.
19 + ///
20 + /// Always the default. Serving a creator's chosen theme into a third-party page
21 + /// is a separate product question, and this is not the place to answer it by
22 + /// accident.
23 + pub fn embed_theme_css() -> &'static str {
24 + crate::theming::theme_css(None)
25 + }
26 +
7 27 #[derive(Template)]
8 28 #[template(path = "embed/item_button.html")]
9 29 pub struct EmbedItemButtonTemplate {
@@ -12,6 +32,9 @@
12 32 pub purchase_url: String,
13 33 pub button_text: String,
14 34 pub cover_image_url: Option<String>,
35 + /// Design-system layers, inlined because an embed cannot link a sheet.
36 + pub theme_css: &'static str,
37 + pub geometry_css: &'static str,
15 38 }
16 39
17 40 #[derive(Template)]
@@ -27,6 +50,9 @@
27 50 /// Empty string = no description block.
28 51 pub description_excerpt: String,
29 52 pub is_horizontal: bool,
53 + /// Design-system layers, inlined because an embed cannot link a sheet.
54 + pub theme_css: &'static str,
55 + pub geometry_css: &'static str,
30 56 }
31 57
32 58 #[derive(Template)]
@@ -39,6 +65,9 @@
39 65 pub creator_display_name: String,
40 66 pub cover_image_url: Option<String>,
41 67 pub preview_url: String,
68 + /// Design-system layers, inlined because an embed cannot link a sheet.
69 + pub theme_css: &'static str,
70 + pub geometry_css: &'static str,
42 71 }
43 72
44 73 #[derive(Template)]
@@ -48,6 +77,9 @@
48 77 pub username: String,
49 78 pub tip_url: String,
50 79 pub avatar_url: Option<String>,
80 + /// Design-system layers, inlined because an embed cannot link a sheet.
81 + pub theme_css: &'static str,
82 + pub geometry_css: &'static str,
51 83 }
52 84
53 85 #[derive(Template)]
@@ -62,4 +94,159 @@
62 94 pub description_excerpt: String,
63 95 pub item_count: usize,
64 96 pub category_label: String,
97 + /// Design-system layers, inlined because an embed cannot link a sheet.
98 + pub theme_css: &'static str,
99 + pub geometry_css: &'static str,
100 + }
101 +
102 + #[cfg(test)]
103 + mod tests {
104 + use super::*;
105 +
106 + /// Everything a template needs that is not the design system. The values are
107 + /// deliberately hex-free so a literal in the output came from the CSS.
108 + fn sample() -> (
109 + EmbedItemButtonTemplate,
110 + EmbedItemCardTemplate,
111 + EmbedItemPlayerTemplate,
112 + EmbedTipButtonTemplate,
113 + EmbedProjectCardTemplate,
114 + ) {
115 + let url = || "https://makenot.work/i/one".to_string();
116 + (
117 + EmbedItemButtonTemplate {
118 + title: "Item".into(),
119 + price_display: "$9".into(),
120 + purchase_url: url(),
121 + button_text: "Buy".into(),
122 + cover_image_url: Some(url()),
123 + theme_css: embed_theme_css(),
124 + geometry_css: EMBED_GEOMETRY_CSS,
125 + },
126 + EmbedItemCardTemplate {
127 + title: "Item".into(),
128 + price_display: "$9".into(),
129 + purchase_url: url(),
130 + button_text: "Buy".into(),
131 + cover_image_url: Some(url()),
132 + creator_display_name: "Creator".into(),
133 + profile_url: url(),
134 + description_excerpt: "About it.".into(),
135 + is_horizontal: true,
136 + theme_css: embed_theme_css(),
137 + geometry_css: EMBED_GEOMETRY_CSS,
138 + },
139 + EmbedItemPlayerTemplate {
140 + title: "Item".into(),
141 + price_display: "$9".into(),
142 + purchase_url: url(),
143 + button_text: "Buy".into(),
144 + creator_display_name: "Creator".into(),
145 + cover_image_url: Some(url()),
146 + preview_url: url(),
147 + theme_css: embed_theme_css(),
148 + geometry_css: EMBED_GEOMETRY_CSS,
149 + },
150 + EmbedTipButtonTemplate {
151 + display_name: "Creator".into(),
152 + username: "creator".into(),
153 + tip_url: url(),
154 + avatar_url: Some(url()),
155 + theme_css: embed_theme_css(),
156 + geometry_css: EMBED_GEOMETRY_CSS,
157 + },
158 + EmbedProjectCardTemplate {
159 + title: "Project".into(),
160 + creator_display_name: "Creator".into(),
161 + profile_url: url(),
162 + project_url: url(),
163 + cover_image_url: Some(url()),
164 + description_excerpt: "About it.".into(),
165 + item_count: 3,
166 + category_label: "Music".into(),
167 + theme_css: embed_theme_css(),
168 + geometry_css: EMBED_GEOMETRY_CSS,
169 + },
170 + )
171 + }
172 +
173 + /// Strip the injected `:root { ... }` blocks. What is left is the template's
174 + /// own CSS, which is where a hand-copied value would be.
175 + fn without_injected_roots(rendered: &str) -> String {
176 + let mut out = String::with_capacity(rendered.len());
177 + let mut rest = rendered;
178 + while let Some(start) = rest.find(":root") {
179 + out.push_str(&rest[..start]);
180 + let Some(open) = rest[start..].find('{') else {
181 + break;
182 + };
183 + let Some(close) = rest[start + open..].find('}') else {
184 + break;
185 + };
186 + rest = &rest[start + open + close + 1..];
187 + }
188 + out.push_str(rest);
189 + out
190 + }
191 +
192 + fn hex_literals(css: &str) -> Vec<String> {
193 + css.split('#')
194 + .skip(1)
195 + .map(|tail| {
196 + tail.chars()
197 + .take_while(|c| c.is_ascii_hexdigit())
198 + .collect::<String>()
199 + })
200 + .filter(|run| run.len() == 3 || run.len() == 6)
201 + .map(|run| format!("#{run}"))
202 + .collect()
203 + }
204 +
205 + /// The regression guard the drift got past. `#5a4bd6` sat in all five
206 + /// templates as the button hover violet and matched no token anywhere in the
207 + /// tree: the real `--action-hover` is a different colour. Nothing could have
208 + /// caught it, because nothing was looking.
209 + #[test]
210 + fn no_embed_template_writes_its_own_colour() {
211 + let (button, card, player, tip, project) = sample();
212 + for (name, rendered) in [
213 + ("item_button", button.render().expect("render")),
214 + ("item_card", card.render().expect("render")),
215 + ("item_player", player.render().expect("render")),
216 + ("tip_button", tip.render().expect("render")),
217 + ("project_card", project.render().expect("render")),
218 + ] {
219 + let own = without_injected_roots(&rendered);
220 + let found = hex_literals(&own);
221 + assert!(
222 + found.is_empty(),
223 + "{name} writes its own colour: {found:?}. Use the token instead; \
224 + a literal here drifts from the theme and nothing will report it.",
225 + );
226 + }
227 + }
228 +
229 + /// The stripper only earns its assertion if the blocks it strips were there.
230 + /// Without this, a handler that stopped passing `theme_css` would leave the
231 + /// test above passing on an empty document.
232 + #[test]
233 + fn every_embed_injects_both_design_system_layers() {
234 + let (button, card, player, tip, project) = sample();
235 + for (name, rendered) in [
236 + ("item_button", button.render().expect("render")),
237 + ("item_card", card.render().expect("render")),
238 + ("item_player", player.render().expect("render")),
239 + ("tip_button", tip.render().expect("render")),
240 + ("project_card", project.render().expect("render")),
241 + ] {
242 + assert!(
243 + rendered.contains("--action-hover"),
244 + "{name} did not inject the theme layer",
245 + );
246 + assert!(
247 + rendered.contains("--geometry-base"),
248 + "{name} did not inject the spacing layer",
249 + );
250 + }
251 + }
65 252 }
@@ -5,24 +5,31 @@
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 6 <title>{{ title }}: Makenotwork</title>
7 7 <style>
8 + /* The design system, inlined: an embed renders in an iframe on somebody
9 + else's page and cannot link a stylesheet. Colour comes from makeover at
10 + render time (platform default theme), spacing from makeover-geometry at
11 + build time (Pointer density, no touch block). Do not write a literal
12 + below: nothing would catch it drifting. */
13 + {{ theme_css|safe }}
14 + {{ geometry_css|safe }}
8 15 * { margin: 0; padding: 0; box-sizing: border-box; }
9 16 body {
10 17 font-family: Lato, -apple-system, sans-serif;
11 - background: #ddd6c9; color: #3d3530;
18 + background: var(--surface-page); color: var(--content);
12 19 display: flex; align-items: center;
13 - height: 100vh; padding: 8px 12px;
20 + height: 100vh; padding: var(--step-base) var(--gap-section);
14 21 }
15 - .embed-button { display: flex; align-items: center; gap: 10px; width: 100%; }
22 + .embed-button { display: flex; align-items: center; gap: var(--gap-group); width: 100%; }
16 23 .cover { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
17 24 .info { flex: 1; min-width: 0; }
18 25 .title { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
19 26 .price { font-size: 12px; opacity: 0.7; font-family: 'IBM Plex Mono', monospace; }
20 27 .buy-btn {
21 - background: #6c5ce7; color: #fff; border: none; border-radius: 4px;
22 - padding: 8px 14px; font-size: 12px; font-weight: 600;
28 + background: var(--action); color: var(--content-on-action); border: none; border-radius: 4px;
29 + padding: var(--step-base) var(--gap-section); font-size: 12px; font-weight: 600;
23 30 cursor: pointer; text-decoration: none; white-space: nowrap;
24 31 }
25 - .buy-btn:hover { background: #5a4bd6; }
32 + .buy-btn:hover { background: var(--action-hover); }
26 33 </style>
27 34 </head>
28 35 <body>
@@ -5,38 +5,45 @@
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 6 <title>{{ title }}: Makenotwork</title>
7 7 <style>
8 + /* The design system, inlined: an embed renders in an iframe on somebody
9 + else's page and cannot link a stylesheet. Colour comes from makeover at
10 + render time (platform default theme), spacing from makeover-geometry at
11 + build time (Pointer density, no touch block). Do not write a literal
12 + below: nothing would catch it drifting. */
13 + {{ theme_css|safe }}
14 + {{ geometry_css|safe }}
8 15 * { margin: 0; padding: 0; box-sizing: border-box; }
9 16 body {
10 17 font-family: Lato, -apple-system, sans-serif;
11 - background: #ddd6c9; color: #3d3530;
18 + background: var(--surface-page); color: var(--content);
12 19 display: flex; align-items: center; justify-content: center;
13 - min-height: 100vh; padding: 12px;
20 + min-height: 100vh; padding: var(--gap-section);
14 21 }
15 22 .card {
16 - background: #fff; border-radius: 8px; overflow: hidden;
17 - border: 1px solid rgba(61,53,48,0.1);
23 + background: var(--surface-well); border-radius: 8px; overflow: hidden;
24 + border: 1px solid var(--border);
18 25 display: flex; flex-direction: {% if is_horizontal %}row{% else %}column{% endif %}; width: 100%;
19 26 max-width: {% if is_horizontal %}600px{% else %}350px{% endif %};
20 27 }
21 28 .cover {
22 29 {% if is_horizontal %}width: 150px; height: 200px;{% else %}width: 100%; height: 200px;{% endif %}
23 - background: #efe9e0; flex-shrink: 0;
30 + background: var(--surface-raised); flex-shrink: 0;
24 31 }
25 32 .cover img { width: 100%; height: 100%; object-fit: cover; }
26 - .body { padding: 14px; display: flex; flex-direction: column; flex: 1; min-width: 0; }
27 - .title { font-family: 'Young Serif', Georgia, serif; font-size: 15px; font-weight: bold; margin-bottom: 4px; }
28 - .creator { font-family: 'IBM Plex Mono', monospace; font-size: 11px; opacity: 0.6; margin-bottom: 8px; }
33 + .body { padding: var(--gap-section); display: flex; flex-direction: column; flex: 1; min-width: 0; }
34 + .title { font-family: 'Young Serif', Georgia, serif; font-size: 15px; font-weight: bold; margin-bottom: var(--gap-bound); }
35 + .creator { font-family: 'IBM Plex Mono', monospace; font-size: 11px; opacity: 0.6; margin-bottom: var(--step-base); }
29 36 .creator a { color: inherit; text-decoration: none; }
30 37 .creator a:hover { text-decoration: underline; }
31 - .desc { font-size: 12px; opacity: 0.8; line-height: 1.4; margin-bottom: 12px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
38 + .desc { font-size: 12px; opacity: 0.8; line-height: 1.4; margin-bottom: var(--gap-section); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
32 39 .footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; }
33 40 .price { font-family: 'IBM Plex Mono', monospace; font-size: 14px; }
34 41 .buy-btn {
35 - background: #6c5ce7; color: #fff; border: none; border-radius: 4px;
36 - padding: 8px 14px; font-size: 12px; font-weight: 600;
42 + background: var(--action); color: var(--content-on-action); border: none; border-radius: 4px;
43 + padding: var(--step-base) var(--gap-section); font-size: 12px; font-weight: 600;
37 44 cursor: pointer; text-decoration: none; white-space: nowrap;
38 45 }
39 - .buy-btn:hover { background: #5a4bd6; }
46 + .buy-btn:hover { background: var(--action-hover); }
40 47 </style>
41 48 </head>
42 49 <body>
@@ -5,43 +5,50 @@
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 6 <title>{{ title }}: Makenotwork</title>
7 7 <style>
8 + /* The design system, inlined: an embed renders in an iframe on somebody
9 + else's page and cannot link a stylesheet. Colour comes from makeover at
10 + render time (platform default theme), spacing from makeover-geometry at
11 + build time (Pointer density, no touch block). Do not write a literal
12 + below: nothing would catch it drifting. */
13 + {{ theme_css|safe }}
14 + {{ geometry_css|safe }}
8 15 * { margin: 0; padding: 0; box-sizing: border-box; }
9 16 body {
10 17 font-family: Lato, -apple-system, sans-serif;
11 - background: #ddd6c9; color: #3d3530;
18 + background: var(--surface-page); color: var(--content);
12 19 display: flex; align-items: center;
13 - height: 100vh; padding: 10px 12px;
20 + height: 100vh; padding: var(--gap-group) var(--gap-section);
14 21 }
15 - .player { display: flex; align-items: center; gap: 12px; width: 100%; }
22 + .player { display: flex; align-items: center; gap: var(--gap-section); width: 100%; }
16 23 .cover { width: 80px; height: 80px; border-radius: 6px; object-fit: cover; flex-shrink: 0; }
17 - .placeholder { background: #efe9e0; }
24 + .placeholder { background: var(--surface-raised); }
18 25 .right { flex: 1; min-width: 0; }
19 - .top-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 4px; }
26 + .top-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--gap-bound); }
20 27 .title { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
21 - .price { font-size: 12px; font-family: 'IBM Plex Mono', monospace; margin-left: 8px; white-space: nowrap; }
22 - .creator { font-size: 11px; opacity: 0.6; margin-bottom: 8px; }
23 - .controls { display: flex; align-items: center; gap: 8px; }
28 + .price { font-size: 12px; font-family: 'IBM Plex Mono', monospace; margin-left: var(--step-base); white-space: nowrap; }
29 + .creator { font-size: 11px; opacity: 0.6; margin-bottom: var(--step-base); }
30 + .controls { display: flex; align-items: center; gap: var(--step-base); }
24 31 .play-btn {
25 32 width: 32px; height: 32px; border-radius: 50%;
26 - background: #6c5ce7; color: #fff; border: none;
33 + background: var(--action); color: var(--content-on-action); border: none;
27 34 font-size: 14px; cursor: pointer; display: flex;
28 35 align-items: center; justify-content: center; flex-shrink: 0;
29 36 }
30 - .play-btn:hover { background: #5a4bd6; }
37 + .play-btn:hover { background: var(--action-hover); }
31 38 .progress-bar {
32 - flex: 1; height: 4px; background: rgba(61,53,48,0.15);
39 + flex: 1; height: 4px; background: var(--surface-sunken);
33 40 border-radius: 2px; cursor: pointer; position: relative;
34 41 }
35 - .progress-fill { height: 100%; background: #6c5ce7; border-radius: 2px; width: 0%; transition: width 0.1s; }
42 + .progress-fill { height: 100%; background: var(--action); border-radius: 2px; width: 0%; transition: width 0.1s; }
36 43 .time { font-size: 10px; font-family: 'IBM Plex Mono', monospace; opacity: 0.6; white-space: nowrap; }
37 - .bottom-row { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }
44 + .bottom-row { display: flex; justify-content: space-between; align-items: center; margin-top: var(--step-base); }
38 45 .preview-label { font-size: 10px; opacity: 0.5; }
39 46 .buy-btn {
40 - background: #6c5ce7; color: #fff; border: none; border-radius: 4px;
41 - padding: 6px 12px; font-size: 11px; font-weight: 600;
47 + background: var(--action); color: var(--content-on-action); border: none; border-radius: 4px;
48 + padding: var(--gap-peer) var(--gap-section); font-size: 11px; font-weight: 600;
42 49 cursor: pointer; text-decoration: none; white-space: nowrap;
43 50 }
44 - .buy-btn:hover { background: #5a4bd6; }
51 + .buy-btn:hover { background: var(--action-hover); }
45 52 </style>
46 53 </head>
47 54 <body>
@@ -5,33 +5,40 @@
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 6 <title>{{ title }}: Makenotwork</title>
7 7 <style>
8 + /* The design system, inlined: an embed renders in an iframe on somebody
9 + else's page and cannot link a stylesheet. Colour comes from makeover at
10 + render time (platform default theme), spacing from makeover-geometry at
11 + build time (Pointer density, no touch block). Do not write a literal
12 + below: nothing would catch it drifting. */
13 + {{ theme_css|safe }}
14 + {{ geometry_css|safe }}
8 15 * { margin: 0; padding: 0; box-sizing: border-box; }
9 16 body {
10 17 font-family: Lato, -apple-system, sans-serif;
11 - background: #ddd6c9; color: #3d3530;
18 + background: var(--surface-page); color: var(--content);
12 19 display: flex; align-items: center; justify-content: center;
13 - min-height: 100vh; padding: 12px;
20 + min-height: 100vh; padding: var(--gap-section);
14 21 }
15 22 .card {
16 - background: #fff; border-radius: 8px; overflow: hidden;
17 - border: 1px solid rgba(61,53,48,0.1);
23 + background: var(--surface-well); border-radius: 8px; overflow: hidden;
24 + border: 1px solid var(--border);
18 25 max-width: 350px; width: 100%;
19 26 }
20 - .cover { width: 100%; height: 160px; background: #efe9e0; }
27 + .cover { width: 100%; height: 160px; background: var(--surface-raised); }
21 28 .cover img { width: 100%; height: 100%; object-fit: cover; }
22 - .body { padding: 14px; }
23 - .title { font-family: 'Young Serif', Georgia, serif; font-size: 15px; font-weight: bold; margin-bottom: 4px; }
24 - .creator { font-family: 'IBM Plex Mono', monospace; font-size: 11px; opacity: 0.6; margin-bottom: 6px; }
29 + .body { padding: var(--gap-section); }
30 + .title { font-family: 'Young Serif', Georgia, serif; font-size: 15px; font-weight: bold; margin-bottom: var(--gap-bound); }
31 + .creator { font-family: 'IBM Plex Mono', monospace; font-size: 11px; opacity: 0.6; margin-bottom: var(--gap-peer); }
25 32 .creator a { color: inherit; text-decoration: none; }
26 33 .creator a:hover { text-decoration: underline; }
27 - .meta { font-size: 11px; opacity: 0.6; margin-bottom: 8px; font-family: 'IBM Plex Mono', monospace; }
28 - .desc { font-size: 12px; opacity: 0.8; line-height: 1.4; margin-bottom: 12px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
34 + .meta { font-size: 11px; opacity: 0.6; margin-bottom: var(--step-base); font-family: 'IBM Plex Mono', monospace; }
35 + .desc { font-size: 12px; opacity: 0.8; line-height: 1.4; margin-bottom: var(--gap-section); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
29 36 .view-btn {
30 - display: inline-block; background: #6c5ce7; color: #fff; border-radius: 4px;
31 - padding: 8px 14px; font-size: 12px; font-weight: 600;
37 + display: inline-block; background: var(--action); color: var(--content-on-action); border-radius: 4px;
38 + padding: var(--step-base) var(--gap-section); font-size: 12px; font-weight: 600;
32 39 text-decoration: none; white-space: nowrap;
33 40 }
34 - .view-btn:hover { background: #5a4bd6; }
41 + .view-btn:hover { background: var(--action-hover); }
35 42 </style>
36 43 </head>
37 44 <body>
@@ -5,23 +5,30 @@
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 6 <title>Support {{ display_name }}: Makenotwork</title>
7 7 <style>
8 + /* The design system, inlined: an embed renders in an iframe on somebody
9 + else's page and cannot link a stylesheet. Colour comes from makeover at
10 + render time (platform default theme), spacing from makeover-geometry at
11 + build time (Pointer density, no touch block). Do not write a literal
12 + below: nothing would catch it drifting. */
13 + {{ theme_css|safe }}
14 + {{ geometry_css|safe }}
8 15 * { margin: 0; padding: 0; box-sizing: border-box; }
9 16 body {
10 17 font-family: Lato, -apple-system, sans-serif;
11 - background: #ddd6c9; color: #3d3530;
18 + background: var(--surface-page); color: var(--content);
12 19 display: flex; align-items: center;
13 - height: 100vh; padding: 8px 12px;
20 + height: 100vh; padding: var(--step-base) var(--gap-section);
14 21 }
15 - .tip { display: flex; align-items: center; gap: 10px; width: 100%; }
22 + .tip { display: flex; align-items: center; gap: var(--gap-group); width: 100%; }
16 23 .avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
17 - .placeholder { background: #efe9e0; }
24 + .placeholder { background: var(--surface-raised); }
18 25 .label { flex: 1; font-size: 13px; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
19 26 .support-btn {
20 - background: #6c5ce7; color: #fff; border: none; border-radius: 4px;
21 - padding: 8px 14px; font-size: 12px; font-weight: 600;
27 + background: var(--action); color: var(--content-on-action); border: none; border-radius: 4px;
28 + padding: var(--step-base) var(--gap-section); font-size: 12px; font-weight: 600;
22 29 cursor: pointer; text-decoration: none; white-space: nowrap;
23 30 }
24 - .support-btn:hover { background: #5a4bd6; }
31 + .support-btn:hover { background: var(--action-hover); }
25 32 </style>
26 33 </head>
27 34 <body>
@@ -128,6 +128,8 @@
128 128 purchase_url: ctx.purchase_url,
129 129 button_text: ctx.button_text,
130 130 cover_image_url: ctx.cover_image_url,
131 + theme_css: crate::templates::embed_theme_css(),
132 + geometry_css: crate::templates::EMBED_GEOMETRY_CSS,
131 133 }
132 134 .into_response();
133 135 set_embed_headers(&mut response);
@@ -165,6 +167,8 @@
165 167 profile_url,
166 168 description_excerpt: ctx.description_excerpt,
167 169 is_horizontal,
170 + theme_css: crate::templates::embed_theme_css(),
171 + geometry_css: crate::templates::EMBED_GEOMETRY_CSS,
168 172 }
169 173 .into_response();
170 174 set_embed_headers(&mut response);
@@ -200,6 +204,8 @@
200 204 creator_display_name: ctx.creator_display_name,
201 205 cover_image_url: ctx.cover_image_url,
202 206 preview_url,
207 + theme_css: crate::templates::embed_theme_css(),
208 + geometry_css: crate::templates::EMBED_GEOMETRY_CSS,
203 209 }
204 210 .into_response();
205 211 set_embed_headers(&mut response);
@@ -58,6 +58,8 @@
58 58 description_excerpt,
59 59 item_count,
60 60 category_label: category_label.to_string(),
61 + theme_css: crate::templates::embed_theme_css(),
62 + geometry_css: crate::templates::EMBED_GEOMETRY_CSS,
61 63 }
62 64 .into_response();
63 65 set_embed_headers(&mut response);
@@ -42,6 +42,8 @@
42 42 username: user.username.to_string(),
43 43 tip_url,
44 44 avatar_url: user.avatar_url,
45 + theme_css: crate::templates::embed_theme_css(),
46 + geometry_css: crate::templates::EMBED_GEOMETRY_CSS,
45 47 }
46 48 .into_response();
47 49 set_embed_headers(&mut response);