Skip to main content

max / goingson

Read-only surfaces become panels instead of cards with the control cancelled Sixteen elements wore .card and then re-asserted the resting fill and bevel on :hover and :active to undo the control half: .card--static at 14 call sites, .card--muted at 2, .card--shell at 1. A card answers a click and the generated rule gives it hover, press, focus and disabled to say so, so cancelling half of it was the app arguing with the primitive. .panel is the card's box without the control, worn with the generated .raised for fill and bevel. .panel--muted takes no .raised: it is set back by colour and has no edge, which is what the old .card--muted was really saying underneath the bevel it switched off. This needed makeover-webview 0.17, where .raised stopped carrying the interactive set and became a surface that is merely an object. Before that there was nothing to opt into, which is why three variants each grew their own cancel instead. Repins makeover-build 0.9.0, makeover-webview 0.17, makeover-layout 0.8.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-06 17:07 UTC
Signed with PGP, not checked
Commit: 2a0213ee21eeb1f6bb32c32da89c68944e5ec072
Parent: 2251d2e
10 files changed, +82 insertions, -62 deletions
@@ -46,8 +46,15 @@
46 46
47 47 ### Card: `.card`
48 48 Sub-parts: `.card-header`, `.card-title`, `.card-description`, `.card-meta`, `.card-badge`.
49 + Variant: `.card--list-item` (compact, for dense lists).
49 50 Container: `.cards-grid`.
50 51 Used by: projects-render, contacts-render, dashboard tiles.
52 + **A card is a control.** It answers a click, and the generated rule gives it hover, press, focus and disabled to say so. If the thing does not answer a click, it is a panel.
53 +
54 + ### Panel: `.panel`
55 + The card's box without the control: same border, radius and padding, no cursor and no states. Variants: `.panel--shell` (no padding, flex column, for list wrappers), `.panel--muted` (set back by colour, no edge).
56 + Wear `.raised` alongside it for fill and bevel: `<div class="raised panel review-card">`. `.panel--muted` takes no `.raised` — it is set back rather than raised.
57 + Replaced `.card--static | --shell | --muted` on 2026-08-06, which were cards with the control half cancelled. Never cancel a state a generated rule emits; take a class that does not emit it.
51 58
52 59 ### Form field: `.form-group`
53 60 Sub-parts: `.form-label`, `.field`, `.form-actions`, `.form-row`.
@@ -252,6 +252,19 @@
252 252 - Hover: lifts up (-2px), shadow grows, bg shifts to `--bg-secondary`
253 253 - Touch devices: no hover transform
254 254
255 + **A card answers a click.** Read-only surfaces are panels, not cards with the
256 + hover switched off:
257 +
258 + ```html
259 + <div class="raised panel review-card">...</div>
260 + <div class="raised panel panel--shell">...</div>
261 + <div class="panel panel--muted">...</div>
262 + ```
263 +
264 + `.panel` is the card's box (border, radius, padding) with no cursor and no
265 + states; `.raised` alongside it supplies fill and bevel. `.panel--muted` is set
266 + back by colour and wears no `.raised`.
267 +
255 268 ### Badges
256 269
257 270 There is no `.tag`. It was a second name for the same thing and collapsed into
@@ -529,7 +542,7 @@
529 542 |---------|-------|---------|
530 543 | `.block` | Component root | `.card`, `.modal`, `.button` |
531 544 | `.block-element` | Child element | `.card-header`, `.card-title` |
532 - | `.block--variant` | Variant | `.button--primary`, `.card--shell` |
545 + | `.block--variant` | Variant | `.button--primary`, `.panel--shell` |
533 546 | `.block.state` | State class | `.tab.chosen`, `.item.selected` |
534 547 | `.utility` | Single purpose | `.shadow-lg`, `.hover-lift` |
535 548 | `[data-attr]` | Dynamic variants | `[data-color="red"]` |
@@ -16,12 +16,12 @@
16 16 [build-dependencies]
17 17 tauri-build = { workspace = true }
18 18 # Materialises all three generated files: themes/, geometry.css, layout.css.
19 - makeover-build = "0.7.0"
19 + makeover-build = "0.9.0"
20 20 # The table CSS is generated here too: the columns are this app's, so the
21 21 # shared helper cannot know them, but the tracks and the narrowing rules come
22 22 # from the description rather than from hand-written nth-child cuts.
23 - makeover-webview = "0.13"
24 - makeover-layout = "0.7"
23 + makeover-webview = "0.17"
24 + makeover-layout = "0.8"
25 25 # Width. Direct rather than through makeover-webview, because the narrow table
26 26 # pass keys off SizeClass::Compact and a boundary reached transitively is a
27 27 # boundary nobody pinned.
@@ -97,8 +97,8 @@
97 97 # The forms emitter and the description it renders. Runtime deps, not build:
98 98 # form markup depends on the value, the error and the options at the moment a
99 99 # modal opens, so it cannot be materialised the way the stylesheet is.
100 - makeover-webview = "0.13"
101 - makeover-layout = "0.7"
100 + makeover-webview = "0.17"
101 + makeover-layout = "0.8"
102 102 # Browser opening
103 103 open = { workspace = true }
104 104
@@ -514,7 +514,7 @@
514 514 <button class="button button--sm button--danger" data-act="bulk.deleteEmails">Delete</button>
515 515 <button class="button button--sm bulk-select-all" data-act="bulk.selectAllEmails">Select All</button>
516 516 </div>
517 - <div class="card card--shell email-list" id="email-list-wrapper" role="list">
517 + <div class="raised panel panel--shell email-list" id="email-list-wrapper" role="list">
518 518 <div class="email-list-container well" id="email-list" aria-live="polite">
519 519 <div class="skeleton-shimmer" aria-label="Loading emails">
520 520 <div class="skeleton-row"><div class="skeleton-avatar"></div><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div>
@@ -160,7 +160,9 @@
160 160
161 161 COMPONENT PRIMITIVES
162 162 .button + .button--primary / --secondary / --danger / --sm
163 - .card + .card--list-item / --shell / --muted (compact / container / info-panel)
163 + .card + .card--list-item (a card is a control)
164 + .panel + .panel--shell / --muted (a panel is not; wear
165 + .raised with it for depth)
164 166 .field + .field--ghost / --compact (kind rides on the element:
165 167 input / select / textarea)
166 168 .modal + .modal-overlay
@@ -864,9 +866,10 @@
864 866
865 867 /* Depth comes from the generated layout.css, same as .button: a card and a
866 868 button are one composition with two names, which is why this file used to
867 - hold two hand-written copies of it. The variants below still override the
868 - generated :hover and :active -- equal specificity, and this file loads
869 - after layout.css, so they win. */
869 + hold two hand-written copies of it. A .card is a control -- it answers a
870 + click, and the generated rule gives it hover, press, focus and disabled to
871 + say so. Anything that does not answer a click is a .panel below, not a card
872 + with the control half cancelled. */
870 873 .card {
871 874 border: var(--border-width) solid var(--border);
872 875 border-radius: var(--radius-md);
@@ -881,46 +884,43 @@
881 884 margin-bottom: var(--gap-peer);
882 885 }
883 886
884 - /* Container shell, keeps card chrome (border/radius/shadow) but strips
885 - padding/cursor/hover and arranges children as a flex column. For list
887 + /* The card's box without the control. Same border, radius and padding; no
888 + cursor and no states, because a panel is a surface you read rather than a
889 + thing you press. Wear it with the generated `.raised` for fill and bevel:
890 +
891 + <div class="raised panel review-card">
892 +
893 + Until 2026-08-06 these were `.card card--static`, `.card card--shell` and
894 + `.card card--muted`: sixteen elements wearing a control's class and then
895 + re-asserting the resting fill and bevel on :hover and :active to undo it.
896 + makeover-webview 0.17 is what made this expressible -- `.raised` used to
897 + carry the interactive set too, so there was no raised surface to opt into
898 + (makeover-layout task 32d66494). */
899 + .panel {
900 + border: var(--border-width) solid var(--border);
901 + border-radius: var(--radius-md);
902 + padding: var(--gap-group);
903 + }
904 +
905 + /* Container shell: a panel that holds a scrolling list rather than content of
906 + its own, so it drops the padding and arranges children as a column. For list
886 907 wrappers like .email-list. */
887 - .card--shell {
908 + .panel--shell {
888 909 padding: 0;
889 - cursor: default;
890 910 display: flex;
891 911 flex-direction: column;
892 912 overflow: hidden;
893 913 }
894 - .card--shell:hover,
895 - .card--shell:active {
896 - background-color: var(--surface-raised);
897 - box-shadow: var(--bevel-raised);
898 - }
899 914
900 - /* Muted info card, bg-secondary, no border/shadow/cursor/hover. For
901 - small read-only info panels (contact info, summary stats). */
902 - .card--muted {
915 + /* Muted info panel for small read-only blocks (contact info, summary stats).
916 + Set back by colour alone -- no edge, so it wears no `.raised` and takes no
917 + bevel. Depth::Sunken in the description's terms rather than a raised object
918 + with its edge switched off, which is what the old `.card--muted` was. */
919 + .panel--muted {
903 920 background: var(--surface-overlay);
904 921 border: none;
905 - box-shadow: none;
906 - cursor: default;
907 - }
908 - .card--muted:hover,
909 - .card--muted:active {
910 - background: var(--surface-overlay);
911 - box-shadow: none;
912 - }
913 -
914 - /* Static card, keeps full card chrome (border/shadow/bg) but strips
915 - cursor and hover animation. For read-only display cards (review
916 - surfaces, monthly summary panels). */
917 - .card--static {
918 - cursor: default;
919 - }
920 - .card--static:hover,
921 - .card--static:active {
922 - background-color: var(--surface-raised);
923 - box-shadow: var(--bevel-raised);
922 + padding: var(--gap-group);
923 + border-radius: var(--radius-md);
924 924 }
925 925
926 926 .card-header {
@@ -1579,7 +1579,7 @@
1579 1579 }
1580 1580
1581 1581 /* 17. Email List */
1582 - /* Base chrome from .card.card--shell; only the flex sizing for the
1582 + /* Base chrome from .raised.panel.panel--shell; only the flex sizing for the
1583 1583 list panel remains here. */
1584 1584 .email-list {
1585 1585 flex: 1;
@@ -4788,7 +4788,7 @@
4788 4788 }
4789 4789
4790 4790
4791 - /* Base chrome from .card.card--muted; only feature-specific spacing
4791 + /* Base chrome from .panel.panel--muted; only feature-specific spacing
4792 4792 (gap, margin, tighter padding) remains here. */
4793 4793 .contact-info-section {
4794 4794 display: flex;
@@ -5823,7 +5823,7 @@
5823 5823 min-width: 80px;
5824 5824 }
5825 5825
5826 - /* Focus section background, applied on top of the .card.card--static.review-card
5826 + /* Focus section background, applied on top of the .raised.panel.review-card
5827 5827 composition for the focus scope card in the V2 grid. */
5828 5828 .focus-section {
5829 5829 background: linear-gradient(135deg, var(--surface-raised) 0%, color-mix(in srgb, var(--warning) 15%, var(--surface-raised)) 100%);
@@ -5865,7 +5865,7 @@
5865 5865 }
5866 5866
5867 5867 /* Review Cards, marker class for review-specific scoping. Callsites compose
5868 - `.card .card--static .review-card`; chrome inherits from .card. Only the
5868 + `.raised .panel .review-card`; chrome inherits from .panel. Only the
5869 5869 padding bump (+0.25rem) and the review-scoped descendants live here. */
5870 5870 .review-card {
5871 5871 padding: var(--gap-group);
@@ -122,7 +122,7 @@
122 122 infoItems.push(`<span class="text-secondary">${esc(s.platform)}:</span> ${esc(s.handle)}`);
123 123 }
124 124 if (infoItems.length > 0) {
125 - infoHtml = `<div class="card card--muted contact-info-section">${infoItems.map(i => `<div class="contact-info-item">${i}</div>`).join('')}</div>`;
125 + infoHtml = `<div class="panel panel--muted contact-info-section">${infoItems.map(i => `<div class="contact-info-item">${i}</div>`).join('')}</div>`;
126 126 }
127 127
128 128 // Activity timeline, Rust already merged, sorted (newest-first), capped,
@@ -124,7 +124,7 @@
124 124 </div>
125 125 </div>
126 126
127 - ${info ? `<div class="card card--muted contact-info-section">${info}</div>` : ''}
127 + ${info ? `<div class="panel panel--muted contact-info-section">${info}</div>` : ''}
128 128 ${tagPills ? `<div class="contact-detail-tags">${tagPills}</div>` : ''}
129 129 ${contact.notes ? `<div class="contact-notes"><strong>Notes:</strong><p>${esc(contact.notes)}</p></div>` : ''}
130 130
@@ -96,7 +96,7 @@
96 96 `).join('');
97 97
98 98 return `
99 - <div class="card card--static review-card month-accomplished-card">
99 + <div class="raised panel review-card month-accomplished-card">
100 100 <div class="card-header">
101 101 <span class="card-title">Accomplished</span>
102 102 <span class="card-badge card-badge--success">
@@ -117,7 +117,7 @@
117 117 * @returns {string} HTML string
118 118 */
119 119 function renderStats(r) {
120 - let html = '<div class="card card--static review-card month-stats-card">';
120 + let html = '<div class="raised panel review-card month-stats-card">';
121 121 html += '<h3 class="review-card-title">Month in Numbers</h3>';
122 122 html += '<div class="month-stats-grid">';
123 123 html += renderStatItem('Tasks Completed', r.tasksCompletedCount, 'completed');
@@ -157,7 +157,7 @@
157 157 function renderProjectPulse(r) {
158 158 if (!r.projectPulse || r.projectPulse.length === 0) return '';
159 159
160 - let html = '<div class="card card--static review-card month-pulse-card">';
160 + let html = '<div class="raised panel review-card month-pulse-card">';
161 161 html += '<h3 class="review-card-title">Project Health</h3>';
162 162 html += '<div class="stack stack-peer">';
163 163
@@ -183,7 +183,7 @@
183 183 * @returns {string} HTML string
184 184 */
185 185 function renderGoals(r) {
186 - let html = '<div class="card card--static review-card month-goals-card scope-card full-width">';
186 + let html = '<div class="raised panel review-card month-goals-card scope-card full-width">';
187 187 html += '<div class="card-header">';
188 188 html += '<span class="card-title">Monthly Goals</span>';
189 189 html += '<span class="card-badge card-badge--warning">Set up to 3 goals</span>';
@@ -254,7 +254,7 @@
254 254 function renderPatterns(r) {
255 255 if (!r.patterns || r.patterns.length === 0) return '';
256 256
257 - let html = '<div class="card card--static review-card month-patterns-card">';
257 + let html = '<div class="raised panel review-card month-patterns-card">';
258 258 html += '<h3 class="review-card-title">Patterns</h3>';
259 259 html += '<ul class="month-patterns-list">';
260 260 for (const p of r.patterns) {
@@ -74,7 +74,7 @@
74 74 `).join('');
75 75
76 76 return `
77 - <div class="card card--static review-card reflection-card">
77 + <div class="raised panel review-card reflection-card">
78 78 <div class="reflection-card-header">
79 79 <h3 class="review-card-title">Reflection</h3>
80 80 <span class="reflection-autosave" id="${idPrefix}-reflection-autosave" role="status" aria-live="polite"></span>
@@ -34,7 +34,7 @@
34 34 const days = r.timelineDays || [];
35 35
36 36 return `
37 - <div class="card card--static review-card week-timeline">
37 + <div class="raised panel review-card week-timeline">
38 38 <div class="card-header">
39 39 <span class="card-title">
40 40 Week at a Glance
@@ -111,7 +111,7 @@
111 111 if (daysWithEvents.length === 0) return '';
112 112
113 113 return `
114 - <div class="card card--static review-card week-timeline-events">
114 + <div class="raised panel review-card week-timeline-events">
115 115 <div class="card-header">
116 116 <span class="card-title">
117 117 Week's Events
@@ -138,7 +138,7 @@
138 138 const eventCount = r.eventsOccurredCount || 0;
139 139
140 140 return `
141 - <div class="card card--static review-card">
141 + <div class="raised panel review-card">
142 142 <div class="card-header">
143 143 <span class="card-title">
144 144 Accomplished
@@ -174,7 +174,7 @@
174 174 const carriedOverCount = r.carriedOverCount || 0;
175 175
176 176 return `
177 - <div class="card card--static review-card">
177 + <div class="raised panel review-card">
178 178 <div class="card-header">
179 179 <span class="card-title">
180 180 Needs Attention
@@ -214,7 +214,7 @@
214 214 const count = r.tasksDueNextWeekCount || 0;
215 215
216 216 return `
217 - <div class="card card--static review-card">
217 + <div class="raised panel review-card">
218 218 <div class="card-header">
219 219 <span class="card-title">
220 220 Due This Week
@@ -283,7 +283,7 @@
283 283 }
284 284
285 285 return `
286 - <div class="card card--static review-card scope-card focus-section full-width">
286 + <div class="raised panel review-card scope-card focus-section full-width">
287 287 <div class="card-header">
288 288 <span class="card-title">
289 289 This Week's Focus
@@ -340,7 +340,7 @@
340 340 }
341 341
342 342 return `
343 - <div class="card card--static review-card">
343 + <div class="raised panel review-card">
344 344 <div class="card-header">
345 345 <span class="card-title">
346 346 Projects Health
@@ -406,7 +406,7 @@
406 406 const vacationDays = r.vacationDays || [];
407 407
408 408 return `
409 - <div class="card card--static review-card vacation-card">
409 + <div class="raised panel review-card vacation-card">
410 410 <div class="card-header">
411 411 <span class="card-title">Days Off</span>
412 412 </div>