Skip to main content

max / makenotwork

Convert the server to the Platinum bevel vocabulary Depth was three hand-rolled tokens over two literal greys, #bbb and #ddd, picked against parchment and then applied to all 31 themes a creator can select. On nord they were a light smear on a dark panel. The three tokens now alias the bevel pair that makeover derives from each theme's own raised surface, so no literal survives and all 19 consumers re-theme without being opened. Phases 1, 3, 4 and 5 of wiki mnw-platinum, in one commit because they do not separate cleanly: --shadow-edge had six direct consumers beyond the three tokens, and those six are the depth-lift hover rule that phase 5 deletes, so retargeting the tokens and rewriting the state vocabulary are one change. 1. makeover 2.3.0 -> 2.4.1, and the :root default block regenerated from dump_intents. 2.4.1 carries the re-ramped makenotwork palette: the old one sat 2.8 L* between page and raised, which no bevel can read, and is now 6.7. That block is a first-paint default for every intent a rule reads, and non-creator pages render from it with no injection at all, so it is regenerated rather than merely extended. 3. build.rs emits static/geometry.css and static/layout.css through makeover-build, linked ahead of style.css. Spacing and composition now come from the crates the same way colour already did. 4. --shadow-raised/-card/-inset point at --bevel-raised/--bevel-inset. 5. Press inverts the bevel, containers square (--radius-md to 0, controls keep 4px), and the charter's "State vocabulary" is rewritten. Hover is a fill shift everywhere, which costs the charter's depth-lift signal for controls that commit a write. Spent deliberately: a bevel is an edge rather than a distance and has no growth axis, and keeping the cue would mean a local override on top of the layout.css GO and BB take unmodified. The two solid-fill buttons shift their own fill through color-mix rather than taking --hover-surface, which on white-on-dark would have put white text on parchment. Also chases the parchment where it is written outside style.css and could not otherwise follow: five embed templates (self-contained iframes on third-party sites), the PWA splash colour, and media-picker.js, which was reaching for --background-color, --border-color, --text-color, --light-background and --surface-muted. None of those exist, so that modal had been rendering the old parchment from a hex fallback and no border at all. The two standalone checkout heads take the new stylesheets explicitly. They build their own <head> rather than including _head_assets.html, and a box-shadow naming an undefined var drops the whole declaration instead of degrading.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-31 19:13 UTC
Signed with PGP, not checked
Commit: 22c1aa04c1dae6be5e0a85dad18a0463f5371ab4
Parent: e796dd2
17 files changed, +185 insertions, -77 deletions
M .gitignore +5
@@ -8,6 +8,11 @@
8 8 multithreaded/static/dist/
9 9 multithreaded/frontend/node_modules/
10 10
11 + # Generated stylesheets (makeover-build emits these from makeover-geometry and
12 + # makeover-webview; the crates are the source, a checked-in copy would drift)
13 + server/static/geometry.css
14 + server/static/layout.css
15 +
11 16 # Environment files (contain secrets)
12 17 server/.env
13 18 server/.env.local
@@ -4396,6 +4396,7 @@
4396 4396 "lightningcss",
4397 4397 "log",
4398 4398 "makeover",
4399 + "makeover-build",
4399 4400 "memmap2",
4400 4401 "metrics",
4401 4402 "metrics-exporter-prometheus",
@@ -4450,15 +4451,47 @@
4450 4451
4451 4452 [[package]]
4452 4453 name = "makeover"
4453 - version = "2.3.0"
4454 + version = "2.4.1"
4454 4455 source = "registry+https://github.com/rust-lang/crates.io-index"
4455 - checksum = "e35cc903581eea6df09a3a102f101d85a6b941ea18431fcd0a70738762271fdf"
4456 + checksum = "b0a796c931419e54dd71a8aaa7d62230730e958f3101d37d4c1d3ab7b541b38e"
4456 4457 dependencies = [
4457 4458 "include_dir",
4458 4459 "serde",
4459 4460 "toml 1.1.3+spec-1.1.0",
4460 4461 ]
4461 4462
4463 + [[package]]
4464 + name = "makeover-build"
4465 + version = "0.2.0"
4466 + source = "registry+https://github.com/rust-lang/crates.io-index"
4467 + checksum = "2de96b65f668a690dc86768338af38779692c23ccc116f4df5746f4700b15d9a"
4468 + dependencies = [
4469 + "makeover",
4470 + "makeover-geometry",
4471 + "makeover-webview",
4472 + ]
4473 +
4474 + [[package]]
4475 + name = "makeover-geometry"
4476 + version = "0.2.0"
4477 + source = "registry+https://github.com/rust-lang/crates.io-index"
4478 + checksum = "fee41544ab308cc2b66da57ec381d19ca763e85627410b0e27361539e3836873"
4479 +
4480 + [[package]]
4481 + name = "makeover-layout"
4482 + version = "0.6.0"
4483 + source = "registry+https://github.com/rust-lang/crates.io-index"
4484 + checksum = "ad745601b5220b2244d73a5da3f3854aefe4cdc955dd45c6d3aa5781ad645f5a"
4485 +
4486 + [[package]]
4487 + name = "makeover-webview"
4488 + version = "0.9.0"
4489 + source = "registry+https://github.com/rust-lang/crates.io-index"
4490 + checksum = "d4ae8b0857cfa95b5d698e22d5df17eb19bae978839d568ae5de02344892962b"
4491 + dependencies = [
4492 + "makeover-layout",
4493 + ]
4494 +
4462 4495 [[package]]
4463 4496 name = "maplit"
4464 4497 version = "1.0.2"
@@ -125,7 +125,7 @@
125 125 tagtree = { path = "../shared/tagtree" }
126 126
127 127 # Shared theme palette + the bundled theme set (Tier 0 creator theming).
128 - makeover = "2.3.0"
128 + makeover = "2.4.1"
129 129
130 130 # Git source browser
131 131 git2 = { version = "0.21", features = ["vendored-libgit2"] }
@@ -168,6 +168,12 @@
168 168 name = "mnw-admin"
169 169 path = "src/bin/mnw-admin.rs"
170 170
171 + [build-dependencies]
172 + # Emits static/geometry.css (makeover-geometry) and static/layout.css
173 + # (makeover-webview). The same generator GO and BB run; only the output paths
174 + # differ, since the server serves its stylesheets rather than bundling them.
175 + makeover-build = "0.2.0"
176 +
171 177 [dev-dependencies]
172 178 tower = { version = "0.5.3", features = ["util"] }
173 179 http-body-util = "0.1"
@@ -21,6 +21,20 @@
21 21 // Compile the TypeScript frontend to static/dist/ (best-effort, see fn).
22 22 build_frontend();
23 23
24 + // The two generated stylesheets. Spacing comes from makeover-geometry and
25 + // composition from makeover-webview, the same way colour already comes
26 + // from makeover through theming.rs. Written into static/ rather than a
27 + // bundle directory because the server serves its stylesheets; both are
28 + // gitignored, since the crates are the source and a checked-in copy would
29 + // drift from the pin.
30 + //
31 + // No explicit touch selector: density hangs off (hover: none),
32 + // (pointer: coarse) alone, because the server has no mode class to key it
33 + // on. GO passes `.ui-mode-mobile` because it has one.
34 + makeover_build::geometry_css("static/geometry.css", None);
35 + makeover_build::layout_css("static/layout.css", &makeover_build::Emit::default());
36 + println!("cargo::rerun-if-changed=build.rs");
37 +
24 38 // --- Static asset fingerprinting ---
25 39 // Hash the content of key static files to produce a version suffix.
26 40 // When any watched file changes, URLs in templates get a new ?v= param,
@@ -44,6 +58,14 @@
44 58 // the TypeScript changes. Read-only: the inputs under frontend/src are the
45 59 // watched trigger (in build_frontend); watching the outputs would loop.
46 60 hash_dir_js(Path::new("static/dist"), &mut hasher);
61 + // Same treatment for the two generated stylesheets: read-only, so a bump
62 + // of makeover-geometry or makeover-webview busts `?v=` without the build
63 + // script watching a file it writes itself.
64 + for path in ["static/geometry.css", "static/layout.css"] {
65 + if let Ok(content) = fs::read(path) {
66 + content.hash(&mut hasher);
67 + }
68 + }
47 69 let static_hash = format!("{:016x}", hasher.finish());
48 70 let version = &static_hash[..8];
49 71
@@ -52,6 +74,8 @@
52 74 let partial = format!(
53 75 r#" <link rel="preload" href="/static/fonts/Lato-Regular.woff2" as="font" type="font/woff2" crossorigin>
54 76 <link rel="preload" href="/static/fonts/ysrf.woff2" as="font" type="font/woff2" crossorigin>
77 + <link rel="stylesheet" href="/static/geometry.css?v={version}">
78 + <link rel="stylesheet" href="/static/layout.css?v={version}">
55 79 <link rel="stylesheet" href="/static/style.css?v={version}">
56 80 <link rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
57 81 <script src="/static/htmx.min.js"></script>
@@ -13,8 +13,8 @@
13 13 | Color | `--background`, `--detail`, `--highlight`, `--light-background`, surface family, `--text-muted`, `--border`, semantic (`--success/-bg`, `--warning/-bg/-border`, `--danger/-bg`, `--error/-bg`), `--stripe`, health (`--health-ok/-warn/-error/-unknown`), diff (`--diff-add/-bg`, `--diff-del/-bg`), `--focus-ring`, `--highlight-faint`, `--overlay` | `style.css:40-95` |
14 14 | Type | `--font-heading` (Young Serif), `--font-mono` (IBM Plex Mono), `--font-body` (Lato) | `style.css:42-44` |
15 15 | Spacing | `--space-1`...`--space-6` mapping to `0.25 / 0.5 / 0.75 / 1 / 1.5 / 2 rem` | `style.css:96-103` |
16 - | Radius | `--radius-sm` (2px), `--radius-md` (4px), `--radius-round` (50%) | `style.css:105-108` |
17 - | Shadow | Hard-offset (everyday affordance): `--shadow-raised` (buttons, tabs), `--shadow-card` (cards, panels), `--shadow-inset` (inputs, recessed). Edge colors `--shadow-edge` / `--shadow-edge-soft`. Blurred (true elevation, floats over page): `--shadow-1` (subtle), `--shadow-2` (raised), `--shadow-3` (modal/overlay). | `style.css:208-222` |
16 + | Radius | `--radius-sm` (4px, controls: buttons, inputs, chips, badges), `--radius-md` (0, containers: cards, panels, dropdowns), `--radius-round` (50%) | `style.css:216-222` |
17 + | Shadow | Everyday depth: `--shadow-raised` (buttons, tabs), `--shadow-card` (cards, panels), `--shadow-inset` (inputs, recessed). All three alias the bevel pair `--bevel-raised` / `--bevel-inset` from `layout.css`, which makeover derives from each theme's own raised surface. Blurred (true elevation, floats over page): `--shadow-1` (subtle), `--shadow-2` (raised), `--shadow-3` (modal/overlay). | `style.css:224-243` |
18 18
19 19 Pure `#000` and `#fff` are forbidden outside the token table. Bootstrap-derived yellows (`#fff3cd`, `#ffc107`) are forbidden. Use `--warning-bg` / `--warning-border`.
20 20
@@ -112,10 +112,10 @@
112 112
113 113 Exactly one spelling for each interaction state, applied to every interactive primitive:
114 114
115 - - **Hover**: three options, picked by component type; do not mix on the same component:
116 - - Surface lift: shift background one step (`--background` -> `--light-background` -> `--surface-muted`). For inline lists, cards-as-links, table rows, and navigation controls (tabs, pagination, breadcrumbs), anything that moves the camera without committing.
117 - - Depth lift: shadow extends from `var(--shadow-raised)` to `3px 3px var(--shadow-edge)`. Reserved for controls that **cause a state change**, DB write, payment, file upload, delete, save. Pair with `:active { box-shadow: none; transform: translate(2px, 2px); }` so the press feels tactile. Default carriers: solid-fill buttons (`.btn-primary` / `-secondary` / `-danger`, bare `button`, `input[type="submit"]`, `form button`). The shadow is an opt-in signal that "this commits something," so a `.btn-primary` used purely for navigation (e.g. "Go to checkout" link) is the exception, not the rule. Those keep depth because they lead to a commit, but pure view-switching uses of these classes should override to `box-shadow: none`.
118 - - Opacity fade to `0.6`: reserved for `.btn--link` (semantically a link, no surface).
115 + - **Hover**: fill shift to `var(--hover-surface)`, on every interactive primitive. One spelling, no component-type branch. The exception is `.btn--link`, which is semantically a link and has no surface to shift: it fades to opacity `0.6`.
116 + - **Press**: `:active` inverts the bevel, `box-shadow: var(--shadow-inset)`. Nothing moves; there is no `transform: translate(2px, 2px)` and no `box-shadow: none`.
117 +
118 + Hover used to have three options picked by component type, and the second of them was a depth lift: the shadow grew from `var(--shadow-raised)` to `3px 3px var(--shadow-edge)`, reserved for controls that commit a write, so that pressing "Pay" looked different from pressing "Next". That signal is gone rather than moved, and it was spent deliberately. A bevel is an edge and not a distance, so it has no growth axis to extend along; keeping a commit-versus-navigate cue would have meant a local override on top of the `layout.css` every other make-family app takes unmodified. Decided 2026-07-31 (wiki `mnw-platinum`). A control that commits a write is distinguished by its label and its placement, the same way it is in GoingsOn and Balanced Breakfast.
119 119 - **Focus**: `:focus-visible` shows the `--focus-ring` violet outline. Custom interactive containers (`.type-card`, `.pricing-card`, sort headers) must opt in by adding `:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }`.
120 120 - **Selected / active**: `.is-selected` modifier applies `background: var(--highlight-faint)` plus the focus-ring border. Migrate `.tab.active`, `.filter-item.active`, `.view-btn.active`, `.badge.active`, and the `:checked + .type-card-inner` recipe onto this single class.
121 121 - **Disabled**: `:disabled` and `[aria-disabled="true"]` show `opacity: 0.5` and `cursor: not-allowed`.
@@ -116,6 +116,11 @@
116 116 "/sso/login",
117 117 "/sso/callback",
118 118 "/static/style.css",
119 + // Generated by build.rs. style.css reads the bevel pair out of
120 + // layout.css, so gating either would strip depth from the login
121 + // page rather than merely slowing it.
122 + "/static/geometry.css",
123 + "/static/layout.css",
119 124 "/static/images/favicon.ico",
120 125 "/rustdoc/index.html",
121 126 "/favicon.ico",
@@ -143,10 +143,15 @@
143 143 fn default_css_carries_intent_tokens() {
144 144 let css = theme_css(None);
145 145 // Intent vocabulary the page's brand aliases derive from.
146 - assert!(css.contains("--surface-page: #ede8e1;"));
146 + // The two surface values are the re-ramped parchment (makeover 2.4.1):
147 + // the original sat 2.8 L* between page and raised, which no bevel can
148 + // read. Pinning them here is what makes the style.css :root default and
149 + // the injected block provably the same theme rather than two drifting
150 + // copies of it.
151 + assert!(css.contains("--surface-page: #ddd6c9;"));
147 152 assert!(css.contains("--content: #3d3530;"));
148 153 assert!(css.contains("--action: #6c5ce7;"));
149 - assert!(css.contains("--border: #d0cbb8;"));
154 + assert!(css.contains("--border: #b3ab97;"));
150 155 // Derived states + the scrim are present too.
151 156 assert!(css.contains("--action-hover: "));
152 157 assert!(css.contains("--overlay: rgba("));
@@ -4,7 +4,7 @@
4 4 "description": "Sell your work directly. 0% platform fee, only Stripe's ~3% processing.",
5 5 "start_url": "/",
6 6 "display": "minimal-ui",
7 - "background_color": "#ede8e1",
7 + "background_color": "#ddd6c9",
8 8 "theme_color": "#3d3530",
9 9 "icons": [
10 10 {
@@ -12,19 +12,19 @@
12 12 modal.id = 'media-picker-modal';
13 13 modal.style.cssText = 'display:none; position:fixed; inset:0; z-index:1000; background:rgba(0,0,0,0.5); align-items:center; justify-content:center;';
14 14 modal.innerHTML =
15 - '<div style="background:var(--background-color,#ede8e1); border:1px solid var(--border-color); border-radius:6px; width:90%; max-width:700px; max-height:80vh; display:flex; flex-direction:column; overflow:hidden;">' +
16 - '<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.25rem; border-bottom:1px solid var(--border-color);">' +
15 + '<div style="background:var(--surface-page); border:1px solid var(--border); border-radius:6px; width:90%; max-width:700px; max-height:80vh; display:flex; flex-direction:column; overflow:hidden;">' +
16 + '<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.25rem; border-bottom:1px solid var(--border);">' +
17 17 '<h3 style="margin:0;">Insert from Media Library</h3>' +
18 - '<button id="media-picker-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer; padding:0 0.25rem; color:var(--text-color);">&times;</button>' +
18 + '<button id="media-picker-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer; padding:0 0.25rem; color:var(--content);">&times;</button>' +
19 19 '</div>' +
20 - '<div style="padding:0.75rem 1.25rem; border-bottom:1px solid var(--border-color); display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap;">' +
20 + '<div style="padding:0.75rem 1.25rem; border-bottom:1px solid var(--border); display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap;">' +
21 21 '<input type="text" id="media-picker-search" placeholder="Filter by name..." style="flex:1; min-width:140px; padding:0.35rem 0.5rem; font-size:0.85rem;">' +
22 22 '<select id="media-picker-folder" style="padding:0.35rem; font-size:0.85rem;"><option value="">All folders</option></select>' +
23 23 '</div>' +
24 24 '<div id="media-picker-grid" style="flex:1; overflow-y:auto; padding:1rem 1.25rem; display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:0.75rem;">' +
25 25 '<p class="form-hint">Loading...</p>' +
26 26 '</div>' +
27 - '<div style="padding:0.75rem 1.25rem; border-top:1px solid var(--border-color); font-size:0.85rem; opacity:0.7;">Click an image to insert its Markdown reference at the cursor.</div>' +
27 + '<div style="padding:0.75rem 1.25rem; border-top:1px solid var(--border); font-size:0.85rem; opacity:0.7;">Click an image to insert its Markdown reference at the cursor.</div>' +
28 28 '</div>';
29 29 document.body.appendChild(modal);
30 30
@@ -88,7 +88,7 @@
88 88 card.className = 'mp-card';
89 89 card.dataset.name = name;
90 90 card.dataset.folder = f.folder || '';
91 - card.style.cssText = 'cursor:pointer; border:1px solid var(--border-color); padding:0.5rem; background:var(--light-background); transition:outline 0.1s;';
91 + card.style.cssText = 'cursor:pointer; border:1px solid var(--border); padding:0.5rem; background:var(--surface-raised); transition:outline 0.1s;';
92 92 (function(r) {
93 93 card.addEventListener('click', function() { window._mediaPickerSelect(r); });
94 94 })(ref);
@@ -97,7 +97,7 @@
97 97
98 98 var thumb = document.createElement('div');
99 99 if (isImage) {
100 - thumb.style.cssText = 'width:100%; height:80px; overflow:hidden; display:flex; align-items:center; justify-content:center; background:var(--surface-muted); margin-bottom:0.4rem;';
100 + thumb.style.cssText = 'width:100%; height:80px; overflow:hidden; display:flex; align-items:center; justify-content:center; background:var(--surface-sunken); margin-bottom:0.4rem;';
101 101 var img = document.createElement('img');
102 102 img.src = f.cdn_url || '';
103 103 img.alt = name;
@@ -105,7 +105,7 @@
105 105 img.loading = 'lazy';
106 106 thumb.appendChild(img);
107 107 } else {
108 - thumb.style.cssText = 'width:100%; height:80px; display:flex; align-items:center; justify-content:center; background:var(--surface-muted); margin-bottom:0.4rem; font-size:1.5rem; opacity:0.4;';
108 + thumb.style.cssText = 'width:100%; height:80px; display:flex; align-items:center; justify-content:center; background:var(--surface-sunken); margin-bottom:0.4rem; font-size:1.5rem; opacity:0.4;';
109 109 thumb.textContent = '\u{1F4C4}';
110 110 }
111 111 card.appendChild(thumb);
@@ -147,10 +147,20 @@
147 147 pages inject a <style> that supersedes this block with the chosen theme.
148 148 Generated. Keep in sync with `cargo run --example dump_intents makenotwork`.
149 149 -------------------------------------------------------------------- */
150 - --surface-page: #ede8e1;
151 - --surface-raised: #f5f0eb;
152 - --surface-sunken: #ddd7c5;
153 - --surface-overlay: #f4f0eb;
150 + --surface-page: #ddd6c9;
151 + --surface-raised: #efe9e0;
152 + --surface-sunken: #cdc5b5;
153 + --surface-overlay: #efe9e0;
154 + /* The content surface cut into a raised one, so a list reads as content in a
155 + container rather than as bands on a panel. Derived by makeover from
156 + surface-raised, away from resolved content, so it goes lighter on a light
157 + theme and darker on a dark one. */
158 + --surface-well: #fffef5;
159 + /* The two edges of a bevel. makeover derives both from the theme's raised
160 + surface, so they arrive with the rest of the intent set through
161 + intent_css_vars() and every creator theme gains them automatically. */
162 + --bevel-light: #fffef5;
163 + --bevel-dark: #b5afa6;
154 164 --content: #3d3530;
155 165 --content-secondary: #5c554f;
156 166 --content-muted: #8a8480;
@@ -161,17 +171,17 @@
161 171 --success: #27ae60;
162 172 --warning: #f59e0b;
163 173 --info: #17a2b8;
164 - --border: #d0cbb8;
165 - --border-strong: #c0bba8;
174 + --border: #b3ab97;
175 + --border-strong: #a39c88;
166 176 --focus-ring: #6c5ce7;
167 - --hover-surface: #ddd7c5;
177 + --hover-surface: #cdc5b5;
168 178 --category-one: #c0392b;
169 179 --category-two: #27ae60;
170 180 --category-three: #6c5ce7;
171 181 --category-four: #f59e0b;
172 182 --category-five: #a29bfe;
173 183 --category-six: #17a2b8;
174 - --overlay: rgba(3, 2, 0, 0.5);
184 + --overlay: rgba(3, 1, 0, 0.5);
175 185
176 186 /* --------------------------------------------------------------------
177 187 APP-LOCAL CONSTANTS, not theme colors, not derivable from intents.
@@ -203,22 +213,31 @@
203 213 --space-7: 3rem;
204 214 --space-8: 4rem;
205 215
206 - /* Radius scale */
207 - --radius-sm: 2px;
208 - --radius-md: 4px;
216 + /* Radius scale. Containers are square and controls keep a corner: the
217 + rounding survives only where a thing is meant to be pressed, so the
218 + corner reads as an affordance rather than as decoration. --radius-md is
219 + kept at 0 rather than deleted so the container call sites stay named. */
220 + --radius-sm: 4px; /* controls: buttons, inputs, chips, badges */
221 + --radius-md: 0; /* containers: cards, panels, dropdowns */
209 222 --radius-round: 50%;
210 223
211 224 /* Shadow scale.
212 225 --shadow-1/-2/-3 are blurred elevation (true floating: popovers,
213 - dropdowns, modals). --shadow-raised/-card/-inset are hard-offset
214 - "sticker on paper" depth for everyday affordance, buttons feel
215 - pressable, cards feel layered, inputs feel recessed. Pick by role:
216 - blurred = floats over page, hard-offset = is part of page. */
217 - --shadow-edge: #bbb; /* heavier, pressable (button, tab) */
218 - --shadow-edge-soft: #ddd; /* lighter, surface (card, input) */
219 - --shadow-raised: 2px 2px var(--shadow-edge);
220 - --shadow-card: 2px 2px var(--shadow-edge-soft);
221 - --shadow-inset: inset 2px 2px var(--shadow-edge-soft);
226 + dropdowns, modals). --shadow-raised/-card/-inset are everyday depth,
227 + buttons feel pressable, cards feel layered, inputs feel recessed. Pick
228 + by role: blurred = floats over page, bevel = is part of page.
229 +
230 + The three used to be hard offsets over two literal greys, #bbb and #ddd,
231 + picked against parchment and then applied to all 31 themes a creator can
232 + select; on nord they were a light smear on a dark panel. They now alias
233 + the bevel pair from layout.css, which makeover derives from each theme's
234 + own raised surface, so no literal survives and every consumer re-themes
235 + without being opened. --shadow-raised and --shadow-card collapse onto
236 + one value: a bevel is an edge rather than a distance, so there is no
237 + heavier and lighter version of it to pick between. */
238 + --shadow-raised: var(--bevel-raised);
239 + --shadow-card: var(--bevel-raised);
240 + --shadow-inset: var(--bevel-inset);
222 241 --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.06);
223 242 --shadow-2: 0 2px 8px rgba(0, 0, 0, 0.10);
224 243 --shadow-3: 0 4px 12px rgba(0, 0, 0, 0.15);
@@ -373,16 +392,15 @@
373 392 font-family: inherit;
374 393 cursor: pointer;
375 394 box-shadow: var(--shadow-raised);
376 - transition: box-shadow 0.1s ease, transform 0.05s ease;
395 + transition: box-shadow 0.1s ease, background 0.2s ease;
377 396 }
378 397
379 398 button:hover {
380 - box-shadow: 3px 3px var(--shadow-edge);
399 + background: var(--hover-surface);
381 400 }
382 401
383 402 button:active {
384 - box-shadow: none;
385 - transform: translate(2px, 2px);
403 + box-shadow: var(--shadow-inset);
386 404 }
387 405
388 406 .btn-primary {
@@ -394,19 +412,22 @@
394 412 font-size: 1rem;
395 413 cursor: pointer;
396 414 box-shadow: var(--shadow-raised);
397 - transition: box-shadow 0.1s ease, transform 0.05s ease;
415 + transition: box-shadow 0.1s ease, background 0.2s ease;
398 416 display: inline-block;
399 417 text-decoration: none;
400 418 }
401 419
420 + /* A solid-fill button shifts its own fill rather than taking --hover-surface.
421 + The charter's hover rule is "one step lighter than the resting fill", and on
422 + a neutral control that step is --hover-surface; on this one it is not, since
423 + the label is --primary-light and a parchment fill under it is unreadable. */
402 424 .btn-primary:hover {
403 - box-shadow: 3px 3px var(--shadow-edge);
425 + background: color-mix(in oklch, var(--primary-dark) 85%, var(--primary-light));
404 426 text-decoration: none;
405 427 }
406 428
407 429 .btn-primary:active {
408 - box-shadow: none;
409 - transform: translate(2px, 2px);
430 + box-shadow: var(--shadow-inset);
410 431 }
411 432
412 433 .btn-secondary {
@@ -418,19 +439,18 @@
418 439 font-size: 1rem;
419 440 cursor: pointer;
420 441 box-shadow: var(--shadow-raised);
421 - transition: box-shadow 0.1s ease, transform 0.05s ease;
442 + transition: box-shadow 0.1s ease, background 0.2s ease;
422 443 display: inline-block;
423 444 text-decoration: none;
424 445 }
425 446
426 447 .btn-secondary:hover {
427 - box-shadow: 3px 3px var(--shadow-edge);
448 + background: var(--hover-surface);
428 449 text-decoration: none;
429 450 }
430 451
431 452 .btn-secondary:active {
432 - box-shadow: none;
433 - transform: translate(2px, 2px);
453 + box-shadow: var(--shadow-inset);
434 454 }
435 455
436 456 .btn-danger {
@@ -442,19 +462,19 @@
442 462 font-size: 1rem;
443 463 cursor: pointer;
444 464 box-shadow: var(--shadow-raised);
445 - transition: box-shadow 0.1s ease, transform 0.05s ease;
465 + transition: box-shadow 0.1s ease, background 0.2s ease;
446 466 display: inline-block;
447 467 text-decoration: none;
448 468 }
449 469
470 + /* Solid fill, so the same exception as .btn-primary applies. */
450 471 .btn-danger:hover {
451 - box-shadow: 3px 3px var(--shadow-edge);
472 + background: color-mix(in oklch, var(--danger) 85%, var(--primary-light));
452 473 text-decoration: none;
453 474 }
454 475
455 476 .btn-danger:active {
456 - box-shadow: none;
457 - transform: translate(2px, 2px);
477 + box-shadow: var(--shadow-inset);
458 478 }
459 479
460 480 /* Disabled state for all button variants (charter rule).
@@ -626,19 +646,17 @@
626 646 border: 1px solid var(--content);
627 647 cursor: pointer;
628 648 box-shadow: var(--shadow-raised);
629 - transition: box-shadow 0.1s ease, transform 0.05s ease, background 0.2s ease;
649 + transition: box-shadow 0.1s ease, background 0.2s ease;
630 650 }
631 651
632 652 input[type="submit"]:hover,
633 653 form button:hover {
634 - background: var(--surface-page);
635 - box-shadow: 3px 3px var(--shadow-edge);
654 + background: var(--hover-surface);
636 655 }
637 656
638 657 input[type="submit"]:active,
639 658 form button:active {
640 - box-shadow: none;
641 - transform: translate(2px, 2px);
659 + box-shadow: var(--shadow-inset);
642 660 }
643 661
644 662 .form-group {
@@ -1528,7 +1546,7 @@
1528 1546 width: 100px;
1529 1547 padding: 8px;
1530 1548 border: 1px solid var(--border);
1531 - border-radius: var(--radius-md);
1549 + border-radius: var(--radius-sm);
1532 1550 font-size: 1rem;
1533 1551 }
1534 1552
@@ -10118,7 +10136,7 @@
10118 10136 background: var(--action);
10119 10137 color: var(--primary-light);
10120 10138 border: none;
10121 - border-radius: var(--radius-md);
10139 + border-radius: var(--radius-sm);
10122 10140 cursor: pointer;
10123 10141 white-space: nowrap;
10124 10142 }
@@ -11382,18 +11400,19 @@
11382 11400 color: var(--content);
11383 11401 background: var(--surface-raised);
11384 11402 border: 1px solid var(--content);
11385 - border-radius: 4px;
11386 - box-shadow: 2px 2px var(--shadow-edge);
11403 + border-radius: var(--radius-sm);
11404 + box-shadow: var(--shadow-raised);
11387 11405 cursor: pointer;
11388 11406 }
11389 11407
11390 11408 .carousel-nav:hover {
11391 - background: var(--surface-page);
11409 + background: var(--hover-surface);
11392 11410 }
11393 11411
11412 + /* Keeps the translateY that centres it vertically; the press is the bevel,
11413 + so nothing is added to the transform. */
11394 11414 .carousel-nav:active {
11395 - transform: translateY(-50%) translate(2px, 2px);
11396 - box-shadow: none;
11415 + box-shadow: var(--shadow-inset);
11397 11416 }
11398 11417
11399 11418 .carousel-nav:focus-visible {
@@ -8,7 +8,7 @@
8 8 * { margin: 0; padding: 0; box-sizing: border-box; }
9 9 body {
10 10 font-family: Lato, -apple-system, sans-serif;
11 - background: #ede8e1; color: #3d3530;
11 + background: #ddd6c9; color: #3d3530;
12 12 display: flex; align-items: center;
13 13 height: 100vh; padding: 8px 12px;
14 14 }