Skip to main content

max / goingson

The tab strip and the toolbar that shares its row are one run Task b39ee4a2, ruling wiki layout-room-and-fallback (Max 2026-08-18). goingson is the proving ground, adopted before the vocabulary is published. styles.css:702 pinned every .page-header over the pill strip with position: absolute, top 0, right 0, z-index 1. The comment above it stated the intent correctly -- buttons float right on the same row -- and the implementation took the toolbar out of flow, so it contributed no width to the row it shared, nothing could collide with it, and therefore nothing prevented the collision. Rule 1 of the ruling is that every described member is in flow. The twelve subview toolbars move out of their subview and into a .run.run-menu beside the pill strip, which is where the stylesheet was pretending they already were. Each carries data-for and navigation.js switches them with the subview, since they no longer follow .subview.hidden. The two that were already in normal flow -- project-dashboard and day-plan -- stay where they are, and now keep their page titles with no override, because the title rule is scoped to .subview-head rather than to the whole tab group. The subview-scoped selectors follow the markup: #tasks-view .page-header and its fifteen siblings become .page-header[data-for="tasks"]. Two rules go with the absolute one: the mobile "undo absolute positioning" override, which now undoes nothing, and #project-dashboard-view .page-title, which now overrides nothing. Verified against the census harness at 1200, 913, 840, 700, 600, 560, 400 and 320. 1200, 913 and 840 unchanged and side by side; 700 and 600 wrap instead of putting the search field on top of "Contacts"; below 560 the app's own mobile rules take over as before. No overlap at any width, and nothing leaves the viewport. CORRECTION to the census. The "? clipped off the header edge at 913" symptom is a harness artifact, not a bug: the button measures x=0 w=0, because .shortcut-hint-btn is display: none under the touch media query and headless firefox reports a coarse pointer. The app-header row measured right=903 in a 913 viewport, so it was never out of room. An earlier version of this commit made it a run on that evidence and it came back out. The dead-vocabulary seal goes 27 to 30. goingson writes two of the five generated run classes; a group declares exactly one fallback, so the other three being unwritten is what a correct description looks like.
Author: Max Johnson <me@maxj.phd> · 2026-08-18 19:40 UTC
Signed with PGP, not checked
Commit: 5fa511a9fe08e92f85c4a8bde7b564c4401adaa9
Parent: eef7e7a
6 files changed, +231 insertions, -195 deletions
M Cargo.lock +31 -33
@@ -1387,7 +1387,7 @@
1387 1387 "libc",
1388 1388 "option-ext",
1389 1389 "redox_users",
1390 - "windows-sys 0.60.2",
1390 + "windows-sys 0.61.2",
1391 1391 ]
1392 1392
1393 1393 [[package]]
@@ -1628,7 +1628,7 @@
1628 1628 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
1629 1629 dependencies = [
1630 1630 "libc",
1631 - "windows-sys 0.60.2",
1631 + "windows-sys 0.61.2",
1632 1632 ]
1633 1633
1634 1634 [[package]]
@@ -3444,8 +3444,6 @@
3444 3444 [[package]]
3445 3445 name = "makeover-build"
3446 3446 version = "0.43.0"
3447 - source = "registry+https://github.com/rust-lang/crates.io-index"
3448 - checksum = "71a7188f7131771586f9b8136b939f0a0117cc7cc3c118a44f08f4bf0f021ef8"
3449 3447 dependencies = [
3450 3448 "makeover",
3451 3449 "makeover-geometry",
@@ -3481,7 +3479,7 @@
3481 3479
3482 3480 [[package]]
3483 3481 name = "makeover-webview"
3484 - version = "0.48.0"
3482 + version = "0.49.0"
3485 3483 dependencies = [
3486 3484 "makeover-geometry",
3487 3485 "makeover-layout",
@@ -3630,7 +3628,7 @@
3630 3628 "png 0.18.1",
3631 3629 "serde",
3632 3630 "thiserror 2.0.20",
3633 - "windows-sys 0.60.2",
3631 + "windows-sys 0.61.2",
3634 3632 ]
3635 3633
3636 3634 [[package]]
@@ -3763,7 +3761,7 @@
3763 3761 source = "registry+https://github.com/rust-lang/crates.io-index"
3764 3762 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
3765 3763 dependencies = [
3766 - "windows-sys 0.60.2",
3764 + "windows-sys 0.61.2",
3767 3765 ]
3768 3766
3769 3767 [[package]]
@@ -4697,7 +4695,7 @@
4697 4695
4698 4696 [[package]]
4699 4697 name = "quasi-http"
4700 - version = "0.28.0"
4698 + version = "0.28.1"
4701 4699 dependencies = [
4702 4700 "form_urlencoded",
4703 4701 "http",
@@ -4706,14 +4704,14 @@
4706 4704
4707 4705 [[package]]
4708 4706 name = "quasi-router"
4709 - version = "0.28.0"
4707 + version = "0.28.1"
4710 4708 dependencies = [
4711 4709 "makeover-layout",
4712 4710 ]
4713 4711
4714 4712 [[package]]
4715 4713 name = "quasi-tauri"
4716 - version = "0.28.0"
4714 + version = "0.28.1"
4717 4715 dependencies = [
4718 4716 "http",
4719 4717 "quasi-http",
@@ -4739,7 +4737,7 @@
4739 4737
4740 4738 [[package]]
4741 4739 name = "quasi-webview"
4742 - version = "0.28.0"
4740 + version = "0.28.1"
4743 4741 dependencies = [
4744 4742 "docengine",
4745 4743 "makeover-layout",
@@ -5178,7 +5176,7 @@
5178 5176 "errno",
5179 5177 "libc",
5180 5178 "linux-raw-sys",
5181 - "windows-sys 0.60.2",
5179 + "windows-sys 0.61.2",
5182 5180 ]
5183 5181
5184 5182 [[package]]
@@ -5235,7 +5233,7 @@
5235 5233 "security-framework",
5236 5234 "security-framework-sys",
5237 5235 "webpki-root-certs",
5238 - "windows-sys 0.60.2",
5236 + "windows-sys 0.61.2",
5239 5237 ]
5240 5238
5241 5239 [[package]]
@@ -5775,7 +5773,7 @@
5775 5773 checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
5776 5774 dependencies = [
5777 5775 "libc",
5778 - "windows-sys 0.60.2",
5776 + "windows-sys 0.61.2",
5779 5777 ]
5780 5778
5781 5779 [[package]]
@@ -6550,7 +6548,7 @@
6550 6548 "getrandom 0.4.3",
6551 6549 "once_cell",
6552 6550 "rustix",
6553 - "windows-sys 0.60.2",
6551 + "windows-sys 0.61.2",
6554 6552 ]
6555 6553
6556 6554 [[package]]
@@ -6572,7 +6570,7 @@
6572 6570 "parking_lot",
6573 6571 "rustix",
6574 6572 "signal-hook",
6575 - "windows-sys 0.60.2",
6573 + "windows-sys 0.61.2",
6576 6574 ]
6577 6575
6578 6576 [[package]]
@@ -7054,7 +7052,7 @@
7054 7052 "png 0.18.1",
7055 7053 "serde",
7056 7054 "thiserror 2.0.20",
7057 - "windows-sys 0.60.2",
7055 + "windows-sys 0.61.2",
7058 7056 ]
7059 7057
7060 7058 [[package]]
@@ -7089,7 +7087,7 @@
7089 7087 dependencies = [
7090 7088 "memoffset",
7091 7089 "tempfile",
7092 - "windows-sys 0.60.2",
7090 + "windows-sys 0.61.2",
7093 7091 ]
7094 7092
7095 7093 [[package]]
@@ -7619,7 +7617,7 @@
7619 7617 source = "registry+https://github.com/rust-lang/crates.io-index"
7620 7618 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
7621 7619 dependencies = [
7622 - "windows-sys 0.60.2",
7620 + "windows-sys 0.61.2",
7623 7621 ]
7624 7622
7625 7623 [[package]]
@@ -8490,19 +8488,7 @@
8490 8488 ]
8491 8489
8492 8490 [[patch.unused]]
8493 - name = "quasi-axum"
8494 - version = "0.28.0"
8495 -
8496 - [[patch.unused]]
8497 - name = "quasi-basics"
8498 - version = "0.28.0"
8499 -
8500 - [[patch.unused]]
8501 - name = "quasi-immediate"
8502 - version = "0.28.0"
8503 -
8504 - [[patch.unused]]
8505 - name = "quasi-store"
8491 + name = "ops-status"
8506 8492 version = "0.1.0"
8507 8493
8508 8494 [[patch.unused]]
@@ -8510,5 +8496,17 @@
8510 8496 version = "0.28.0"
8511 8497
8512 8498 [[patch.unused]]
8513 - name = "ops-status"
8499 + name = "quasi-axum"
8500 + version = "0.28.1"
8501 +
8502 + [[patch.unused]]
8503 + name = "quasi-basics"
8504 + version = "0.28.1"
8505 +
8506 + [[patch.unused]]
8507 + name = "quasi-immediate"
8508 + version = "0.28.1"
8509 +
8510 + [[patch.unused]]
8511 + name = "quasi-store"
8514 8512 version = "0.1.0"
@@ -28,7 +28,7 @@
28 28 # The table CSS is generated here too: the columns are this app's, so the
29 29 # shared helper cannot know them, but the tracks and the narrowing rules come
30 30 # from the description rather than from hand-written nth-child cuts.
31 - makeover-webview = "0.48.0"
31 + makeover-webview = "0.49.0"
32 32 makeover-layout = "0.29.0"
33 33 # Width. Direct rather than through makeover-webview, because the narrow table
34 34 # pass keys off SizeClass::Compact and a boundary reached transitively is a
@@ -124,7 +124,7 @@
124 124 # The forms emitter and the description it renders. Runtime deps, not build:
125 125 # form markup depends on the value, the error and the options at the moment a
126 126 # modal opens, so it cannot be materialised the way the stylesheet is.
127 - makeover-webview = "0.48.0"
127 + makeover-webview = "0.49.0"
128 128 makeover-layout = "0.29.0"
129 129 # Browser opening
130 130 open = { workspace = true }
@@ -581,10 +581,24 @@
581 581 /// `js/problems.js` rather than described. It stops being dead when the
582 582 /// problems list ports, not before.
583 583 ///
584 + /// # 27 to 30, 2026-08-18: three of the four `run-*` classes
585 + ///
586 + /// `makeover-layout` 0.29.0 named `Fallback` and `makeover-webview` 0.49.0
587 + /// emits a rule per member, so the denominator went 55 to 60 and this app uses
588 + /// two of the five: `.run` and `.run-menu`, on the tab strip and the toolbar
589 + /// that shares its row. `.run-wrap`, `.run-stack` and `.run-shed` are the other
590 + /// three answers a group could have given and land here.
591 + ///
592 + /// This is the rise the rule below asks to be checked first, and it passes the
593 + /// check: a group declares exactly one fallback, so four of the five classes
594 + /// being unwritten is what a correct description looks like rather than markup
595 + /// that stopped using them. It tightens only if a second run somewhere in the
596 + /// app declares a different one, which is not something to go looking for.
597 + ///
584 598 /// The one direction this number should move is down, so if it ever needs
585 599 /// raising for a class goingson's own markup *should* be writing, that is the
586 600 /// defect this exists to catch and the answer is the markup, not the seal.
587 - const DEAD_VOCABULARY_HIGH_WATER: usize = 27;
601 + const DEAD_VOCABULARY_HIGH_WATER: usize = 30;
588 602
589 603 /// Every file that can carry a class name.
590 604 fn markup_files(frontend: &Path) -> Vec<PathBuf> {
@@ -100,26 +100,71 @@
100 100 <main id="main-content" class="main-content">
101 101 <!-- Work Tab Group -->
102 102 <div id="work-view" class="view tab-group">
103 - <div class="pill-nav" role="tablist" aria-label="Work views">
104 - <button class="pill active" data-subview="tasks">Tasks</button>
105 - <button class="pill" data-subview="projects">Projects</button>
106 - <button class="pill" data-subview="problems" title="Candidates from wam and audit runs, ranked by painhours">Problems <span id="problems-count" class="pill-count hidden"></span></button>
107 - <button class="pill" data-subview="task-graph" title="The blocking graph: what waits on what">Graph</button>
103 + <!-- The strip and the toolbar that shares its row: one flex run,
104 + every member in flow. See makeover-layout's Fallback. -->
105 + <div class="run run-menu">
106 + <div class="pill-nav" role="tablist" aria-label="Work views">
107 + <button class="pill active" data-subview="tasks">Tasks</button>
108 + <button class="pill" data-subview="projects">Projects</button>
109 + <button class="pill" data-subview="problems" title="Candidates from wam and audit runs, ranked by painhours">Problems <span id="problems-count" class="pill-count hidden"></span></button>
110 + <button class="pill" data-subview="task-graph" title="The blocking graph: what waits on what">Graph</button>
111 + </div>
112 + <div class="page-header subview-head" data-for="tasks">
113 + <h2 class="page-title">Tasks</h2>
114 + <div class="row-flex row-flex-section">
115 + <div class="view-toggle" id="task-view-toggle">
116 + <button class="view-toggle-btn active" data-mode="list" data-act="tasks.setViewMode" data-a1="list">List</button>
117 + <button class="view-toggle-btn" data-mode="board" data-act="tasks.setViewMode" data-a1="board">Board</button>
118 + </div>
119 + <button class="button button--secondary mobile-hide" data-act="keyboard.openQuickAddModal" title="Quick add (q)">Quick Add</button>
120 + <button class="button button--primary" data-act="tasks.openNew" title="New task (n)">+ New Task <kbd class="kbd-hint">n</kbd></button>
121 + </div>
122 + </div>
123 + <div class="page-header subview-head hidden" data-for="projects">
124 + <h2 class="page-title">Projects</h2>
125 + <div class="row-flex row-flex-peer">
126 + <span id="projects-filter-slot"></span>
127 + <button class="button button--primary" data-act="projects.openNew" title="New project (n)">+ New Project <kbd class="kbd-hint">n</kbd></button>
128 + </div>
129 + </div>
130 + <div class="page-header subview-head hidden" data-for="problems">
131 + <h2 class="page-title">Problems</h2>
132 + <div class="row-flex row-flex-peer">
133 + <select id="problems-source-filter" class="field field--compact" aria-label="Filter by source" data-change="problems.setSourceFilter">
134 + <option value="">All sources</option>
135 + </select>
136 + <select id="problems-status-filter" class="field field--compact" aria-label="Filter by status" data-change="problems.setStatusFilter">
137 + <option value="Open" selected>Open</option>
138 + <option value="Promoted">Promoted</option>
139 + <option value="Dismissed">Dismissed</option>
140 + <option value="Resolved">Resolved</option>
141 + <option value="all">All</option>
142 + </select>
143 + <button class="button button--secondary" id="problems-refresh" data-act="problems.pull" title="Pull the latest from every configured source">Refresh</button>
144 + <button class="button button--secondary" data-act="problems.configure" title="Configure the wam endpoint">Sources</button>
145 + </div>
146 + </div>
147 + <div class="page-header subview-head hidden" data-for="task-graph">
148 + <h2 class="page-title">Graph</h2>
149 + <div class="row-flex row-flex-peer">
150 + <select id="task-graph-project" class="field field--compact" aria-label="Scope the graph to a project" data-change="taskGraph.setProject">
151 + <option value="">All projects</option>
152 + </select>
153 + <button class="button button--secondary" data-act="taskGraph.load" title="Reload the graph">Refresh</button>
154 + </div>
155 + </div>
156 + <div class="page-header subview-head hidden" data-for="task-overview">
157 + <div class="row-flex row-flex-section">
158 + <button class="button button--secondary" data-act="taskOverview.close">&larr; Back</button>
159 + <h2 class="page-title" id="task-overview-title">Task Overview</h2>
160 + </div>
161 + <div id="task-overview-actions"></div>
162 + </div>
108 163 </div>
109 164
110 165 <!-- Tasks Sub-View -->
111 166 <div id="tasks-view" class="subview">
112 - <div class="page-header">
113 - <h2 class="page-title">Tasks</h2>
114 - <div class="row-flex row-flex-section">
115 - <div class="view-toggle" id="task-view-toggle">
116 - <button class="view-toggle-btn active" data-mode="list" data-act="tasks.setViewMode" data-a1="list">List</button>
117 - <button class="view-toggle-btn" data-mode="board" data-act="tasks.setViewMode" data-a1="board">Board</button>
118 - </div>
119 - <button class="button button--secondary mobile-hide" data-act="keyboard.openQuickAddModal" title="Quick add (q)">Quick Add</button>
120 - <button class="button button--primary" data-act="tasks.openNew" title="New task (n)">+ New Task <kbd class="kbd-hint">n</kbd></button>
121 - </div>
122 - </div>
167 +
123 168 <div id="task-bulk-actions" class="bulk-actions-bar hidden" role="toolbar" aria-label="Bulk task actions">
124 169 <span id="task-bulk-count" class="bulk-count">0 selected</span>
125 170 <button class="button button--sm" data-act="bulk.completeTasks">Complete</button>
@@ -204,13 +249,7 @@
204 249
205 250 <!-- Projects Sub-View -->
206 251 <div id="projects-view" class="subview hidden" role="tabpanel" aria-labelledby="projects-tab">
207 - <div class="page-header">
208 - <h2 class="page-title">Projects</h2>
209 - <div class="row-flex row-flex-peer">
210 - <span id="projects-filter-slot"></span>
211 - <button class="button button--primary" data-act="projects.openNew" title="New project (n)">+ New Project <kbd class="kbd-hint">n</kbd></button>
212 - </div>
213 - </div>
252 +
214 253 <div class="cards-grid" id="projects-grid">
215 254 <div class="skeleton-shimmer" aria-label="Loading projects">
216 255 <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>
@@ -222,23 +261,7 @@
222 261
223 262 <!-- Problems Sub-View -->
224 263 <div id="problems-view" class="subview hidden" role="tabpanel" aria-labelledby="problems-tab">
225 - <div class="page-header">
226 - <h2 class="page-title">Problems</h2>
227 - <div class="row-flex row-flex-peer">
228 - <select id="problems-source-filter" class="field field--compact" aria-label="Filter by source" data-change="problems.setSourceFilter">
229 - <option value="">All sources</option>
230 - </select>
231 - <select id="problems-status-filter" class="field field--compact" aria-label="Filter by status" data-change="problems.setStatusFilter">
232 - <option value="Open" selected>Open</option>
233 - <option value="Promoted">Promoted</option>
234 - <option value="Dismissed">Dismissed</option>
235 - <option value="Resolved">Resolved</option>
236 - <option value="all">All</option>
237 - </select>
238 - <button class="button button--secondary" id="problems-refresh" data-act="problems.pull" title="Pull the latest from every configured source">Refresh</button>
239 - <button class="button button--secondary" data-act="problems.configure" title="Configure the wam endpoint">Sources</button>
240 - </div>
241 - </div>
264 +
242 265 <p class="view-hint">Candidates, not work. Ranked by painhours, so anything left untriaged climbs on its own. Promote the ones worth doing.</p>
243 266 <div id="problems-list" class="stack stack-peer">
244 267 <div class="skeleton-shimmer" aria-label="Loading problems">
@@ -249,15 +272,7 @@
249 272 </div>
250 273
251 274 <div id="task-graph-view" class="subview hidden" role="tabpanel" aria-labelledby="task-graph-tab">
252 - <div class="page-header">
253 - <h2 class="page-title">Graph</h2>
254 - <div class="row-flex row-flex-peer">
255 - <select id="task-graph-project" class="field field--compact" aria-label="Scope the graph to a project" data-change="taskGraph.setProject">
256 - <option value="">All projects</option>
257 - </select>
258 - <button class="button button--secondary" data-act="taskGraph.load" title="Reload the graph">Refresh</button>
259 - </div>
260 - </div>
275 +
261 276 <p class="view-hint">What waits on what. An arrow runs from a blocker to the task it holds up, and each column is one more completion away from being startable. Tasks with no dependencies either way are not drawn.</p>
262 277 <div id="task-graph-legend" class="task-graph-legend" aria-hidden="true"></div>
263 278 <div id="task-graph-canvas" class="task-graph-canvas">
@@ -319,13 +334,7 @@
319 334 </div>
320 335 <!-- Task Overview Sub-View -->
321 336 <div id="task-overview-view" class="subview hidden">
322 - <div class="page-header">
323 - <div class="row-flex row-flex-section">
324 - <button class="button button--secondary" data-act="taskOverview.close">&larr; Back</button>
325 - <h2 class="page-title" id="task-overview-title">Task Overview</h2>
326 - </div>
327 - <div id="task-overview-actions"></div>
328 - </div>
337 +
329 338 <div id="task-overview-content">
330 339 <div class="skeleton-shimmer" aria-label="Loading task overview">
331 340 <div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div>
@@ -338,12 +347,35 @@
338 347
339 348 <!-- Time Tab Group -->
340 349 <div id="time-view" class="view tab-group hidden">
341 - <div class="pill-nav" role="tablist" aria-label="Time views">
342 - <button class="pill active" data-subview="day-plan">Day</button>
343 - <button class="pill" data-subview="weekly-review">Week</button>
344 - <button class="pill" data-subview="monthly-review">Month</button>
345 - <button class="pill" data-subview="timer" title="Pomodoro-style focus sessions">Timer</button>
346 - <button class="pill" data-subview="events">Events</button>
350 + <!-- The strip and the toolbar that shares its row: one flex run,
351 + every member in flow. See makeover-layout's Fallback. -->
352 + <div class="run run-menu">
353 + <div class="pill-nav" role="tablist" aria-label="Time views">
354 + <button class="pill active" data-subview="day-plan">Day</button>
355 + <button class="pill" data-subview="weekly-review">Week</button>
356 + <button class="pill" data-subview="monthly-review">Month</button>
357 + <button class="pill" data-subview="timer" title="Pomodoro-style focus sessions">Timer</button>
358 + <button class="pill" data-subview="events">Events</button>
359 + </div>
360 + <div class="page-header subview-head hidden" data-for="weekly-review">
361 + <h2 class="page-title">Weekly Review</h2>
362 + </div>
363 + <div class="page-header subview-head hidden" data-for="monthly-review">
364 + <div class="monthly-review-nav">
365 + <button class="button button--secondary" data-act="monthlyReview.previousMonth" title="Previous month">&larr;</button>
366 + <button class="button button--secondary" data-act="monthlyReview.goToCurrentMonth">This Month</button>
367 + <button class="button button--secondary" data-act="monthlyReview.nextMonth" title="Next month">&rarr;</button>
368 + <span id="monthly-review-month-display" class="monthly-review-month-display"></span>
369 + </div>
370 + <span id="month-review-status-badge" class="review-status hidden"></span>
371 + </div>
372 + <div class="page-header subview-head hidden" data-for="timer">
373 + <h2 class="page-title">Timer</h2>
374 + </div>
375 + <div class="page-header subview-head hidden" data-for="events">
376 + <h2 class="page-title">Events</h2>
377 + <button class="button button--primary" data-act="events.openNew" title="New event (n)">+ New Event <kbd class="kbd-hint">n</kbd></button>
378 + </div>
347 379 </div>
348 380
349 381 <!-- Day Plan Sub-View -->
@@ -395,9 +427,7 @@
395 427
396 428 <!-- Weekly Review Sub-View -->
397 429 <div id="weekly-review-view" class="subview hidden" role="tabpanel" aria-labelledby="weekly-review-tab">
398 - <div class="page-header">
399 - <h2 class="page-title">Weekly Review</h2>
400 - </div>
430 +
401 431 <div id="weekly-review-content" class="weekly-review-content">
402 432 <div class="skeleton-shimmer" aria-label="Loading weekly review">
403 433 <div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div>
@@ -409,15 +439,7 @@
409 439
410 440 <!-- Monthly Review Sub-View -->
411 441 <div id="monthly-review-view" class="subview hidden" role="tabpanel" aria-labelledby="monthly-review-tab">
412 - <div class="page-header">
413 - <div class="monthly-review-nav">
414 - <button class="button button--secondary" data-act="monthlyReview.previousMonth" title="Previous month">&larr;</button>
415 - <button class="button button--secondary" data-act="monthlyReview.goToCurrentMonth">This Month</button>
416 - <button class="button button--secondary" data-act="monthlyReview.nextMonth" title="Next month">&rarr;</button>
417 - <span id="monthly-review-month-display" class="monthly-review-month-display"></span>
418 - </div>
419 - <span id="month-review-status-badge" class="review-status hidden"></span>
420 - </div>
442 +
421 443 <div id="monthly-review-content" class="monthly-review-content">
422 444 <div class="skeleton-shimmer" aria-label="Loading monthly review">
423 445 <div class="skeleton-row"><div class="skeleton-lines"><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div></div>
@@ -428,9 +450,7 @@
428 450
429 451 <!-- Timer Sub-View -->
430 452 <div id="timer-view" class="subview hidden" role="tabpanel">
431 - <div class="page-header">
432 - <h2 class="page-title">Timer</h2>
433 - </div>
453 +
434 454 <div id="timer-subview-content">
435 455 <!-- Rendered by time-tracking.js -->
436 456 </div>
@@ -438,10 +458,7 @@
438 458
439 459 <!-- Events Sub-View (accessible via keyboard/URL, not shown in pill nav) -->
440 460 <div id="events-view" class="subview hidden" role="tabpanel" aria-labelledby="events-tab">
441 - <div class="page-header">
442 - <h2 class="page-title">Events</h2>
443 - <button class="button button--primary" data-act="events.openNew" title="New event (n)">+ New Event <kbd class="kbd-hint">n</kbd></button>
444 - </div>
461 +
445 462 <div class="raised filter-bar" id="events-filter-bar">
446 463 <label class="filter-checkbox" title="Include events hidden until a later date">
447 464 <input type="checkbox" id="filter-events-snoozed" data-change="events.onFilterChange">
@@ -511,20 +528,42 @@
511 528
512 529 <!-- Messages Tab Group -->
513 530 <div id="messages-view" class="view tab-group hidden">
514 - <div class="pill-nav" role="tablist" aria-label="Messages views">
515 - <button class="pill active" data-subview="emails">Email</button>
516 - <button class="pill" data-subview="contacts">Contacts</button>
531 + <!-- The strip and the toolbar that shares its row: one flex run,
532 + every member in flow. See makeover-layout's Fallback. -->
533 + <div class="run run-menu">
534 + <div class="pill-nav" role="tablist" aria-label="Messages views">
535 + <button class="pill active" data-subview="emails">Email</button>
536 + <button class="pill" data-subview="contacts">Contacts</button>
537 + </div>
538 + <div class="page-header subview-head" data-for="emails">
539 + <h2 class="page-title">Emails</h2>
540 + <div class="row-flex row-flex-peer">
541 + <button class="button button--secondary mobile-hide" data-act="emails.openDrafts">Drafts</button>
542 + <button class="button button--primary" data-act="emails.openCompose" title="Compose email (n)">+ Compose</button>
543 + </div>
544 + </div>
545 + <div class="page-header subview-head hidden" data-for="contacts">
546 + <h2 class="page-title">Contacts</h2>
547 + <div class="contacts-filter-row">
548 + <input type="text" class="field" id="contacts-search" placeholder="Search contacts..." data-input="contacts.filterBySearch">
549 + <select class="filter-select" id="contacts-tag-filter" data-change="contacts.filterByTag">
550 + <option value="">All Tags</option>
551 + </select>
552 + <button class="button button--primary" data-act="contacts.openNew" title="New contact (n)">+ New Contact <kbd class="kbd-hint">n</kbd></button>
553 + </div>
554 + </div>
555 + <div class="page-header subview-head hidden" data-for="contact-dashboard">
556 + <div class="row-flex row-flex-section">
557 + <button class="button button--secondary" data-act="contactDashboard.close">&larr; Back</button>
558 + <h2 class="page-title" id="contact-dashboard-title">Contact</h2>
559 + </div>
560 + <div id="contact-dashboard-actions"></div>
561 + </div>
517 562 </div>
518 563
519 564 <!-- Emails Sub-View -->
520 565 <div id="emails-view" class="subview" role="tabpanel" aria-labelledby="emails-tab">
521 - <div class="page-header">
522 - <h2 class="page-title">Emails</h2>
523 - <div class="row-flex row-flex-peer">
524 - <button class="button button--secondary mobile-hide" data-act="emails.openDrafts">Drafts</button>
525 - <button class="button button--primary" data-act="emails.openCompose" title="Compose email (n)">+ Compose</button>
526 - </div>
527 - </div>
566 +
528 567 <div class="email-filter-row mb-peer">
529 568 <input type="text" class="field" id="email-search" placeholder="Search emails..." data-input="emails.search">
530 569 <select class="field" id="email-folder-filter" data-change="emails.filterByFolder">
@@ -564,16 +603,7 @@
564 603
565 604 <!-- Contacts Sub-View -->
566 605 <div id="contacts-view" class="subview hidden" role="tabpanel" aria-labelledby="contacts-tab">
567 - <div class="page-header">
568 - <h2 class="page-title">Contacts</h2>
569 - <div class="contacts-filter-row">
570 - <input type="text" class="field" id="contacts-search" placeholder="Search contacts..." data-input="contacts.filterBySearch">
571 - <select class="filter-select" id="contacts-tag-filter" data-change="contacts.filterByTag">
572 - <option value="">All Tags</option>
573 - </select>
574 - <button class="button button--primary" data-act="contacts.openNew" title="New contact (n)">+ New Contact <kbd class="kbd-hint">n</kbd></button>
575 - </div>
576 - </div>
606 +
577 607 <div id="contacts-bulk-bar" class="bulk-actions-bar hidden">
578 608 <span class="bulk-count">0 selected</span>
579 609 <button class="button button--sm" data-act="contacts.selectAll">Select All</button>
@@ -591,13 +621,7 @@
591 621 </div>
592 622 <!-- Contact Dashboard Sub-View -->
593 623 <div id="contact-dashboard-view" class="subview hidden">
594 - <div class="page-header">
595 - <div class="row-flex row-flex-section">
596 - <button class="button button--secondary" data-act="contactDashboard.close">&larr; Back</button>
597 - <h2 class="page-title" id="contact-dashboard-title">Contact</h2>
598 - </div>
599 - <div id="contact-dashboard-actions"></div>
600 - </div>
624 +
601 625 <div id="contact-dashboard-content">
602 626 <div class="skeleton-shimmer" aria-label="Loading contact">
603 627 <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>
@@ -695,35 +695,30 @@
695 695
696 696 /* Right-aligned cluster of header controls (e.g. a filter toggle + primary action). */
697 697
698 - /* --- Merged pill + page-header toolbar --- */
699 - /* Tab groups use pills as view labels; page-header buttons float right on the same row */
700 - .tab-group {
701 - position: relative;
698 + /* --- The tab strip and the toolbar that shares its row --- */
699 + /* One described run: the strip leads and a subview's toolbar follows it, both
700 + in flow, and .run's min-content floor is what stops either being squeezed
701 + over the other. This was `position: absolute; top: 0; right: 0; z-index: 1`
702 + on the toolbar until 2026-08-18, which said the same intent by taking the
703 + toolbar out of flow -- so it contributed no width to the row it shared,
704 + nothing could collide with it, and therefore nothing prevented the collision.
705 + At 913 the help control clipped off the edge, at 700 the search field sat on
706 + "Contacts", at 560 the new-contact button left the viewport entirely. See
707 + makeover-layout's Fallback and wiki `layout-room-and-fallback`. */
708 + /* A toolbar that moved into the run. Selected by its own class rather than by
709 + `.run >`, so nothing here declares a property the generated .run already sets
710 + and the drift check has no arm to collapse. */
711 + .subview-head {
712 + margin-bottom: 0;
702 713 }
703 714
704 - .tab-group > .subview > .page-header {
705 - position: absolute;
706 - top: 0;
707 - right: 0;
708 - margin: 0;
709 - z-index: 1;
710 - }
711 -
712 - .tab-group .page-header .page-title {
715 + /* The pills are the label, so a toolbar sharing their row does not repeat it.
716 + This is why the two subviews whose header stays in flow -- project-dashboard
717 + and day-plan -- keep their titles with no override undoing anything. */
718 + .subview-head .page-title {
713 719 display: none;
714 720 }
715 721
716 - /* Special subviews that need their page-header in normal flow */
717 - #project-dashboard-view > .page-header,
718 - #day-plan-view > .page-header {
719 - position: static;
720 - margin-bottom: var(--gap-section);
721 - }
722 -
723 - #project-dashboard-view .page-title {
724 - display: block;
725 - }
726 -
727 722 /* 9. Buttons
728 723
729 724 Depth lives in the generated layout.css now: `.button`, `.button:hover` and
@@ -5221,7 +5216,7 @@
5221 5216 }
5222 5217
5223 5218 /* The pill row is chrome, not content: it sizes to itself in every group. */
5224 - .tab-group > .pill-nav {
5219 + .tab-group .pill-nav {
5225 5220 flex-shrink: 0;
5226 5221 }
5227 5222
@@ -5233,7 +5228,7 @@
5233 5228 min-height: 0;
5234 5229 }
5235 5230
5236 - #tasks-view .page-header,
5231 + .page-header[data-for="tasks"],
5237 5232 #tasks-view .bulk-actions-bar,
5238 5233 #tasks-view .mobile-sort-bar,
5239 5234 #tasks-view .filter-bar,
@@ -5257,7 +5252,7 @@
5257 5252 min-height: 0;
5258 5253 }
5259 5254
5260 - #events-view .page-header,
5255 + .page-header[data-for="events"],
5261 5256 #events-view .filter-bar,
5262 5257 #events-view .bulk-actions-bar {
5263 5258 flex-shrink: 0;
@@ -5327,7 +5322,7 @@
5327 5322 width: auto;
5328 5323 }
5329 5324
5330 - #emails-view .page-header,
5325 + .page-header[data-for="emails"],
5331 5326 #emails-view .email-filter-row,
5332 5327 #emails-view .bulk-actions-bar,
5333 5328 #email-pagination {
@@ -5343,7 +5338,7 @@
5343 5338 min-height: 0;
5344 5339 }
5345 5340
5346 - #contacts-view .page-header,
5341 + .page-header[data-for="contacts"],
5347 5342 #contacts-view .bulk-actions-bar {
5348 5343 flex-shrink: 0;
5349 5344 }
@@ -7367,9 +7362,9 @@
7367 7362 the row predictably (no orphans, no ragged wrapping). Search inputs
7368 7363 always span the full grid row. */
7369 7364 #emails-view > div.row-flex.mb-peer,
7370 - #tasks-view .page-header > .row-flex,
7365 + .page-header[data-for="tasks"] > .row-flex,
7371 7366 #tasks-view .mobile-sort-bar,
7372 - #contacts-view .page-header > .row-flex {
7367 + .page-header[data-for="contacts"] > .row-flex {
7373 7368 display: grid;
7374 7369 grid-auto-flow: row;
7375 7370 grid-template-columns: repeat(2, 1fr);
@@ -7380,7 +7375,7 @@
7380 7375 /* Tasks page-header: Quick Add + New Task are hidden on mobile (see
7381 7376 .mobile-hide / .button--primary rules), so only the view-toggle remains
7382 7377 and takes the full row. */
7383 - #tasks-view .page-header > .row-flex {
7378 + .page-header[data-for="tasks"] > .row-flex {
7384 7379 grid-template-columns: 1fr;
7385 7380 }
7386 7381
@@ -7445,26 +7440,21 @@
7445 7440 /* Inside the new grid cells, undo the segmented-button styling (no
7446 7441 right border, square corners) inherited from the desktop page-header
7447 7442 rule, grid already gives equal sizing. */
7448 - #tasks-view .page-header > .row-flex .button,
7449 - #contacts-view .page-header > .row-flex .button {
7443 + .page-header[data-for="tasks"] > .row-flex .button,
7444 + .page-header[data-for="contacts"] > .row-flex .button {
7450 7445 border-radius: var(--radius-sm);
7451 7446 border-right-width: var(--border-width);
7452 7447 }
7453 7448
7454 7449 /* View-toggle inside the tasks grid should fill its cell. */
7455 - #tasks-view .page-header .view-toggle {
7450 + .page-header[data-for="tasks"] .view-toggle {
7456 7451 display: flex;
7457 7452 width: 100%;
7458 7453 }
7459 - #tasks-view .page-header .view-toggle .view-toggle-btn {
7454 + .page-header[data-for="tasks"] .view-toggle .view-toggle-btn {
7460 7455 flex: 1 1 0;
7461 7456 }
7462 7457
7463 - /* Undo absolute positioning on mobile, page-header buttons are hidden anyway */
7464 - .tab-group > .subview > .page-header {
7465 - position: static;
7466 - }
7467 -
7468 7458 .page-header {
7469 7459 flex-wrap: wrap;
7470 7460 }
@@ -7481,14 +7471,14 @@
7481 7471 button text has more room without crowding the row. Stretch each segment
7482 7472 to share row width so the block reads as one unified control. */
7483 7473 .page-header > div,
7484 - #emails-view .page-header > div,
7485 - #contacts-view .page-header > div {
7474 + .page-header[data-for="emails"] > div,
7475 + .page-header[data-for="contacts"] > div {
7486 7476 gap: 0 !important;
7487 7477 flex-wrap: wrap;
7488 7478 }
7489 7479 .page-header > div .button,
7490 - #emails-view .page-header > div .button,
7491 - #contacts-view .page-header > div .button,
7480 + .page-header[data-for="emails"] > div .button,
7481 + .page-header[data-for="contacts"] > div .button,
7492 7482 .bulk-actions-bar .button {
7493 7483 flex: 1 1 0;
7494 7484 min-width: 0;
@@ -7499,8 +7489,8 @@
7499 7489 white-space: nowrap;
7500 7490 }
7501 7491 .page-header > div .button:first-child,
7502 - #emails-view .page-header > div .button:first-child,
7503 - #contacts-view .page-header > div .button:first-child,
7492 + .page-header[data-for="emails"] > div .button:first-child,
7493 + .page-header[data-for="contacts"] > div .button:first-child,
7504 7494 .bulk-actions-bar .button:first-of-type {
7505 7495 border-top-left-radius: var(--radius-sm);
7506 7496 border-bottom-left-radius: var(--radius-sm);
@@ -7511,10 +7501,10 @@
7511 7501 immediately before a hidden primary. */
7512 7502 .page-header > div .button:last-child,
7513 7503 .page-header > div .button:has(+ .button--primary),
7514 - #emails-view .page-header > div .button:last-child,
7515 - #emails-view .page-header > div .button:has(+ .button--primary),
7516 - #contacts-view .page-header > div .button:last-child,
7517 - #contacts-view .page-header > div .button:has(+ .button--primary) {
7504 + .page-header[data-for="emails"] > div .button:last-child,
7505 + .page-header[data-for="emails"] > div .button:has(+ .button--primary),
7506 + .page-header[data-for="contacts"] > div .button:last-child,
7507 + .page-header[data-for="contacts"] > div .button:has(+ .button--primary) {
7518 7508 border-top-right-radius: var(--radius-sm);
7519 7509 border-bottom-right-radius: var(--radius-sm);
7520 7510 border-right-width: var(--border-width);
@@ -160,6 +160,16 @@
160 160 groupEl.querySelectorAll('.pill-nav .pill').forEach(p => p.classList.remove('active'));
161 161 const activePill = groupEl.querySelector(`.pill-nav [data-subview="${view}"]`);
162 162 if (activePill) activePill.classList.add('active');
163 +
164 + // The sub-view's toolbar is a member of the pill row rather than a
165 + // child of the sub-view, so it does not follow .subview.hidden and
166 + // is switched here instead. It sits there because the two occupy
167 + // one row: it used to be pulled up out of the sub-view with
168 + // position: absolute, which took it out of flow and let it land on
169 + // top of the pills. See makeover-layout's Fallback.
170 + groupEl.querySelectorAll('.subview-head').forEach(h => h.classList.add('hidden'));
171 + const activeHead = groupEl.querySelector(`.subview-head[data-for="${view}"]`);
172 + if (activeHead) activeHead.classList.remove('hidden');
163 173 }
164 174 }
165 175