Skip to main content

max / goingson

The stylesheet styles the document the app serves, and nothing else styles.css was 9,830 lines written against index.html and the 84 scripts that built its panes, both deleted at the swap. 1,426 rules cut, leaving 785 lines and 237 live selectors. Cut against the build's own verdict rather than by reading, one rule at a time: a complex selector naming any class nothing can emit can never match, so the rule goes whole. That is why 992 dead selectors took 1,426 rules with them, which is the "1,021 is a floor, not a ceiling" the task predicted. check_stylesheet_reaches_markup measured 1,017, and it was wrong in both directions. Its scanner did not skip comments, so a prelude opening with the comment that documents it did not start with '@' and six media blocks were read as declaration blocks, hiding every class inside them: the whole compact shell. Comment prose reached it as selector text (build.rs in a sentence became a .rs class), and @font-face was read as holding rules, so url("QuasiMono.woff2") became a dead .woff2. Fixed, the count was 1,001: 34 false positives out, 19 genuinely dead compact-shell classes in. Also gone, on the same evidence and outside what the check can see: - 16 #id rules from index.html, none of the ids named anywhere in src/ - four @keyframes with no animation referencing them - --timeline-slot-h, read by the day-plan scripts that went with the SPA The seal goes with them. It sealed at 1,021 to refuse growth while the cut was pending; the list has to be empty now, so a rule written against markup nothing emits fails the build where it is added. The header is rewritten. It was a 285-line guide to composing app classes, and the app names none: every class the document can hold belongs to quasi-webview or makeover, which is what makes the check exact. The last eight dead selectors were the generated tables.css, so 7bf95b31 lands here too rather than waiting a commit. TABLES, TASK_COLUMNS, UPCOMING_COLUMNS, RECURRING_COLUMNS, table_css, table_columns_json, table_columns_js and the three files they wrote are gone, with the assets arm and the shell's .styled() line. A described table is display: table, laid out by makeover's stylesheet and aligned by position, so there is no grid-template-columns for an app to generate and no row class to key one on. The overlap census's cause 1 dies with it and does not reappear: task_list.rs's COLUMNS carries Width::Fill and Width::Fixed and no pixel lengths, so it never inherited the clipping shape. geometry.css stopped generating its .ui-mode-mobile touch override in the same pass. bootstrap-uimode.js set that class and went with the SPA, so the block styled a class nothing could put in the document. Closes daac5cc7 and 7bf95b31.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 22:52 UTC
Commit: 775e95a4bfae10376ef4bee089214964129163e6
Parent: d303935
6 files changed, +92 insertions, -833 deletions
M CONTRIBUTING.md +10 -7
@@ -229,10 +229,15 @@
229 229
230 230 ## CSS Workflow
231 231
232 - - **Edit:** `src-tauri/frontend/css/styles.css`, which is what the HTML loads. There is no
233 - build step: edit it and reload.
234 - - `geometry.css`, `layout.css` and `tables.css` are generated by `src-tauri/build.rs` from
235 - the makeover crates. Edit the descriptions in `build.rs`, not the output.
232 + - **Edit:** `src-tauri/frontend/css/styles.css`, which is the app's own remainder. There
233 + is no build step: edit it and reload.
234 + - `typography.css`, `geometry.css` and `layout.css` are generated by `src-tauri/build.rs`
235 + from the makeover crates. Edit the descriptions in `build.rs`, not the output.
236 + - **A new class is not an option.** The app describes screens and names no classes, so
237 + every class the document can hold comes from quasi-webview or makeover. `build.rs`
238 + checks every selector in `styles.css` against that vocabulary on each build and fails
239 + on one nothing can emit. If a screen needs something the vocabulary cannot say, file
240 + the gap against quasi rather than writing a class for it.
236 241 - Nothing is minified. The app reads its CSS from a bundled local folder, so there is no
237 242 transfer to shrink, and minified CSS during development means the webview inspector
238 243 points at one enormous line. See the wiki note `quasi-overview` for the measurement.
@@ -248,9 +253,7 @@
248 253 @media (min-width: 600px) { .foo { ... } } /* wide shell */
249 254 ```
250 255
251 - `src-tauri/build.rs` reads those numbers off `SizeClass` for the generated `tables.css`; the hand-written ones in `styles.css` are the same numbers typed out, so they change together. Other widths (1024px, 1400px) are ordinary tuning inside the wide shell, not a third shell.
252 -
253 - In JS, ask the same question the same way: `window.matchMedia('(max-width: 599px)').matches`, per call rather than cached, so a resized window is right at the next render.
256 + `src-tauri/build.rs` reads those numbers off `SizeClass` for the generated sheets; the hand-written ones in `styles.css` are the same numbers typed out, so they change together. Other widths (1024px, 1400px) are ordinary tuning inside the wide shell, not a third shell.
254 257
255 258 **Capability: what is pointing at it.**
256 259
@@ -44,7 +44,7 @@
44 44 | `cargo build` fails on macOS | Ensure Xcode CLT installed: `xcode-select --install` |
45 45 | Tauri build fails | Check `src-tauri/tauri.conf.json`, ensure frontend paths exist |
46 46 | Missing shared deps | Ensure `MNW/shared/` exists at correct relative path from workspace |
47 - | CSS not updating | Edit `styles.css` and reload; there is no CSS build step. If a `--gap-*` or layout class is stale, re-run `cargo build` so `build.rs` regenerates `geometry.css`/`layout.css`/`tables.css` |
47 + | CSS not updating | Edit `styles.css` and reload; there is no CSS build step. If a `--gap-*` or layout class is stale, re-run `cargo build` so `build.rs` regenerates `geometry.css`/`layout.css` |
48 48
49 49 ## Theme Loading Issues
50 50
M src-tauri/build.rs +21 -442
@@ -1,417 +1,7 @@
1 - use std::fmt::Write as _;
2 1 use std::fs;
3 2 use std::path::Path;
4 3
5 - use makeover_geometry::SizeClass;
6 - use makeover_layout::{Column, Priority, Width};
7 4 use makeover_webview::Emit;
8 - use makeover_webview::list::{Sizing, column_class, narrowing_css};
9 -
10 - /// The tasks table, left to right.
11 - ///
12 - /// Priority is what a column is worth when there is not room for all of them,
13 - /// and it is the whole reason this description exists. The stylesheet used to
14 - /// hide mobile columns with `nth-child(n+5)` against a seven-column table plus a
15 - /// separate `nth-child(3)`, so inserting a column anywhere left of the cut hid
16 - /// the wrong one and nothing said so.
17 - ///
18 - /// `sorted` is `None` on every column here and always will be. Which way the
19 - /// table is ordered is a runtime fact that changes when a header is pressed, so
20 - /// a build script cannot hold it; `tasks-filter.js` writes it onto the heading
21 - /// as `aria-sort`, which is the same field said in the webview's idiom.
22 - ///
23 - /// This serves the JS task list and nothing else, so it deletes at the flip
24 - /// along with `tasks.js` and the hand-written table in `index.html`. The
25 - /// described table lives in `src/quasi/task_list.rs` and is the one that
26 - /// survives. Until the flip a column added here has to be added there by hand;
27 - /// nothing checks that they agree and nothing is going to, because keeping the
28 - /// two in step is not work worth automating for a frontend being removed.
29 - const TASK_COLUMNS: &[Column<'static>] = &[
30 - // Without it the row does not identify itself.
31 - Column {
32 - width: Width::Fill,
33 - priority: Priority::Essential,
34 - sortable: true,
35 - ..Column::new("description")
36 - },
37 - Column {
38 - width: Width::Fixed,
39 - priority: Priority::Secondary,
40 - sortable: true,
41 - ..Column::new("project")
42 - },
43 - // A single letter, and the row already carries its priority in the left
44 - // border colour, so it is the first thing that can go.
45 - Column {
46 - width: Width::Fixed,
47 - priority: Priority::Optional,
48 - sortable: true,
49 - ..Column::new("priority")
50 - },
51 - Column {
52 - width: Width::Fixed,
53 - priority: Priority::Secondary,
54 - sortable: true,
55 - ..Column::new("due")
56 - },
57 - Column {
58 - width: Width::Fixed,
59 - priority: Priority::Optional,
60 - ..Column::new("recurrence")
61 - },
62 - Column {
63 - width: Width::Fixed,
64 - priority: Priority::Optional,
65 - ..Column::new("progress")
66 - },
67 - // Secondary, unlike every other table's actions column: on a narrow screen
68 - // the task row reflows into a card, and the kebab is the only affordance it
69 - // has left. The card orders it fourth and the touch rules make it always
70 - // visible, so dropping it here would contradict both.
71 - Column {
72 - width: Width::Fixed,
73 - priority: Priority::Secondary,
74 - ..Column::new("actions")
75 - },
76 - ];
77 -
78 - /// The upcoming-events table, left to right.
79 - ///
80 - /// Separate from [`RECURRING_COLUMNS`] because these are two tables and not
81 - /// one. They had been sharing a class and therefore a grid, which is what put
82 - /// the upcoming row's six cells into five tracks: the checkbox took the date's
83 - /// track, the title took the location's, and the location took the 40px one
84 - /// meant for the kebab. Only the recurring table ever fitted.
85 - const UPCOMING_COLUMNS: &[Column<'static>] = &[
86 - // Bulk selection. Goes first on a narrow screen, like the kebab does.
87 - Column {
88 - width: Width::Fixed,
89 - priority: Priority::Optional,
90 - ..Column::new("select")
91 - },
92 - Column {
93 - width: Width::Fixed,
94 - priority: Priority::Essential,
95 - ..Column::new("date")
96 - },
97 - Column {
98 - width: Width::Fixed,
99 - priority: Priority::Secondary,
100 - ..Column::new("time")
101 - },
102 - Column {
103 - width: Width::Fill,
104 - priority: Priority::Essential,
105 - ..Column::new("title")
106 - },
107 - Column {
108 - width: Width::Fixed,
109 - priority: Priority::Optional,
110 - ..Column::new("location")
111 - },
112 - Column {
113 - width: Width::Fixed,
114 - priority: Priority::Optional,
115 - ..Column::new("actions")
116 - },
117 - ];
118 -
119 - /// The recurring-events table, left to right.
120 - ///
121 - /// Its first column is the recurrence pattern where the upcoming table's is a
122 - /// date, and it has no bulk selection, which is the other half of why one
123 - /// description could never have served both.
124 - const RECURRING_COLUMNS: &[Column<'static>] = &[
125 - Column {
126 - width: Width::Fixed,
127 - priority: Priority::Essential,
128 - ..Column::new("pattern")
129 - },
130 - Column {
131 - width: Width::Fixed,
132 - priority: Priority::Secondary,
133 - ..Column::new("time")
134 - },
135 - Column {
136 - width: Width::Fill,
137 - priority: Priority::Essential,
138 - ..Column::new("title")
139 - },
140 - Column {
141 - width: Width::Fixed,
142 - priority: Priority::Optional,
143 - ..Column::new("location")
144 - },
145 - Column {
146 - width: Width::Fixed,
147 - priority: Priority::Optional,
148 - ..Column::new("actions")
149 - },
150 - ];
151 -
152 - /// The tables: `(name, header class, selector, columns)`.
153 - ///
154 - /// One list, read twice. [`table_css`] emits the rules and
155 - /// [`table_columns_json`] hands the same order to the frontend's tests, which
156 - /// is what lets them assert that a row's cells are the columns the description
157 - /// names.
158 - ///
159 - /// The two event selectors carry a modifier rather than a class of their own,
160 - /// so everything the two tables genuinely share -- padding, hover, the row
161 - /// border -- stays on `.event-row-virtual` and only the grid splits.
162 - const TABLES: &[(&str, &str, &str, &[Column<'static>])] = &[
163 - (
164 - "task",
165 - "task-header-row",
166 - ".task-header-row, .task-row",
167 - TASK_COLUMNS,
168 - ),
169 - (
170 - "upcoming",
171 - "event-header-upcoming",
172 - ".event-header-row.event-header-upcoming, .event-row-virtual.event-upcoming",
173 - UPCOMING_COLUMNS,
174 - ),
175 - (
176 - "recurring",
177 - "event-header-recurring",
178 - ".event-header-row.event-header-recurring, .event-row-virtual.event-recurring",
179 - RECURRING_COLUMNS,
180 - ),
181 - ];
182 -
183 - /// The column names, for a reader that is not a stylesheet.
184 - ///
185 - /// The CSS carries the names of the columns it *hides* and nothing else, so a
186 - /// cell whose `col-` class matches no column is invisible to it: the narrowing
187 - /// rules simply never match, and the cell stays on a narrow screen with nothing
188 - /// said. That is how the events table came to render six cells into five
189 - /// tracks. This file is the description in a form the frontend can check itself
190 - /// against.
191 - ///
192 - /// # Why the class is carried rather than spelled
193 - ///
194 - /// `classes` is the column's own class as [`column_class`] writes it, and it is
195 - /// here so that no reader has to derive `col-` + name for itself. The derivation
196 - /// is not the identity it looks like: `push_column_name` reduces a name to
197 - /// identifier characters, so a column named `Due date` is `col-Due-date` and a
198 - /// reader that concatenated would spell `col-Due date`, which the HTML parser
199 - /// reads as two classes and which the narrowing selector matches neither of.
200 - /// One writer, every reader.
201 - fn table_columns_json() -> String {
202 - let opts = Emit::default();
203 - let mut json = String::from("{\n");
204 - for (i, (table, header, _, columns)) in TABLES.iter().enumerate() {
205 - let names = columns
206 - .iter()
207 - .map(|c| format!("\"{}\"", c.name))
208 - .collect::<Vec<_>>()
209 - .join(", ");
210 - // Which of them offer a reorder, so the one place that says so is the
211 - // description. `check_sortable_headers` reads this same field out of
212 - // the const; this carries it to a reader that is not Rust.
213 - let sortable = columns
214 - .iter()
215 - .filter(|c| c.sortable)
216 - .map(|c| format!("\"{}\"", c.name))
217 - .collect::<Vec<_>>()
218 - .join(", ");
219 - let classes = columns
220 - .iter()
221 - .map(|c| format!("\"{}\": \"{}\"", c.name, column_class(c, &opts)))
222 - .collect::<Vec<_>>()
223 - .join(", ");
224 - let _ = writeln!(
225 - json,
226 - " \"{table}\": {{ \"header\": \"{header}\", \"columns\": [{names}], \
227 - \"sortable\": [{sortable}], \"classes\": {{{classes}}} }}{}",
228 - if i + 1 == TABLES.len() { "" } else { "," }
229 - );
230 - }
231 - json.push_str("}\n");
232 - json
233 - }
234 -
235 - /// The same description again, in a form the running frontend can read.
236 - ///
237 - /// [`table_columns_json`] is read by the node test runner off disk. The webview
238 - /// has no filesystem and fetching a JSON file under Tauri's CSP is a worse
239 - /// bargain than a script tag, so the runtime gets the identical bytes assigned
240 - /// to a global instead. One description, one writer, two readers -- rather than
241 - /// the third copy that lived in the row builders as hand-typed `col-` classes.
242 - ///
243 - /// This is data, not markup. The row builders stay in JS and keep emitting
244 - /// their own cells, which is what `makeover_webview::list`'s own header asks a
245 - /// webview app to do: take `narrowing_css` and `column_class`, and do not put an
246 - /// IPC round trip on a scroll path that runs synchronously at 60Hz.
247 - fn table_columns_js() -> String {
248 - format!(
249 - "// Generated by src-tauri/build.rs from the column descriptions. Do not edit.\n\
250 - //\n\
251 - // The runtime half of tables.columns.json, which the node tests read off\n\
252 - // disk. Both come out of one function, so a row cannot be built against a\n\
253 - // column list the stylesheet and the tests disagree with.\n\
254 - (function () {{\n\
255 - 'use strict';\n\
256 - const TABLES = {};\n\
257 - if (window.GoingsOn) {{\n\
258 - \x20 GoingsOn.tableColumns = TABLES;\n\
259 - }}\n\
260 - }})();\n",
261 - table_columns_json().trim_end()
262 - )
263 - }
264 -
265 - /// Generate the tables' column CSS.
266 - ///
267 - /// Both halves of narrowing come out of one call per breakpoint: the track list
268 - /// and the hiding. They used to be written apart and kept in step by hand,
269 - /// which they were not. The mobile rule declared four tracks while three cells
270 - /// survived, so the due date landed in the 40px track meant for the priority
271 - /// letter.
272 - fn table_css() -> String {
273 - let opts = Emit::default();
274 -
275 - let task_wide = Sizing {
276 - lengths: &[
277 - ("description", "200px"),
278 - ("project", "140px"),
279 - ("priority", "80px"),
280 - ("due", "110px"),
281 - ("recurrence", "90px"),
282 - ("progress", "100px"),
283 - ("actions", "90px"),
284 - ],
285 - fallback: "",
286 - };
287 - // The narrow pass carries its own lengths: the columns that survive are not
288 - // the same size in a compact window as in an expanded one.
289 - let task_narrow = Sizing {
290 - lengths: &[
291 - ("description", "0"),
292 - ("project", "80px"),
293 - ("due", "80px"),
294 - ("actions", "40px"),
295 - ],
296 - fallback: "",
297 - };
298 -
299 - let upcoming_wide = Sizing {
300 - lengths: &[
301 - ("select", "40px"),
302 - ("date", "100px"),
303 - ("time", "80px"),
304 - ("title", "0"),
305 - ("location", "150px"),
306 - ("actions", "40px"),
307 - ],
308 - fallback: "",
309 - };
310 - let upcoming_narrow = Sizing {
311 - lengths: &[("date", "90px"), ("time", "70px"), ("title", "0")],
312 - fallback: "",
313 - };
314 -
315 - let recurring_wide = Sizing {
316 - lengths: &[
317 - ("pattern", "140px"),
318 - ("time", "80px"),
319 - ("title", "0"),
320 - ("location", "150px"),
321 - ("actions", "40px"),
322 - ],
323 - fallback: "",
324 - };
325 - let recurring_narrow = Sizing {
326 - lengths: &[("pattern", "110px"), ("time", "70px"), ("title", "0")],
327 - fallback: "",
328 - };
329 -
330 - // Paper is narrower than a window and nothing is hidden on it, so every
331 - // column has to fit: the third set of lengths for the same seven columns.
332 - let task_print = Sizing {
333 - lengths: &[
334 - ("description", "0"),
335 - ("project", "100px"),
336 - ("priority", "40px"),
337 - ("due", "80px"),
338 - ("recurrence", "60px"),
339 - ("progress", "80px"),
340 - ("actions", "60px"),
341 - ],
342 - fallback: "",
343 - };
344 -
345 - let mut css = String::new();
346 -
347 - // Zipped rather than carried in TABLES: a length is a CSS answer and the
348 - // description deliberately holds none, which is the split Sizing exists for.
349 - for ((_, _, selector, columns), (wide, narrow)) in TABLES.iter().zip([
350 - (&task_wide, &task_narrow),
351 - (&upcoming_wide, &upcoming_narrow),
352 - (&recurring_wide, &recurring_narrow),
353 - ]) {
354 - css.push('\n');
355 - css.push_str(&narrowing_css(
356 - columns,
357 - selector,
358 - wide,
359 - Priority::Optional,
360 - &opts,
361 - ));
362 -
363 - // Width, and only width. A column comes out because there is no room
364 - // for it, which is a question about the viewport and not about what is
365 - // pointing at it: a desktop window dragged narrow drops the same
366 - // columns a phone does, and a tablet in landscape keeps them.
367 - let _ = write!(
368 - css,
369 - "\n@media {} {{\n",
370 - SizeClass::Compact.media_condition()
371 - );
372 - css.push_str(&narrowing_css(
373 - columns,
374 - selector,
375 - narrow,
376 - Priority::Secondary,
377 - &opts,
378 - ));
379 - css.push_str("}\n");
380 - }
381 -
382 - // The print pass keeps every column -- a printed table is read without a
383 - // scrollbar, so a column that is merely optional on screen is the one thing
384 - // paper is good at carrying -- and only restates the lengths. It had been a
385 - // seven-track list written by hand inside @media print, which is the same
386 - // fact the description already holds and the same shape that put the mobile
387 - // rule one track out.
388 - css.push_str("\n@media print {\n");
389 - css.push_str(&narrowing_css(
390 - TASK_COLUMNS,
391 - ".task-header-row, .task-row",
392 - &task_print,
393 - Priority::Optional,
394 - &opts,
395 - ));
396 - css.push_str("}\n");
397 -
398 - // Same cascade layer as layout.css and geometry.css. These rules are as
399 - // generated as those are, and makeover cannot put them there on our behalf
400 - // because the columns are this app's, so it never sees this file. Left
401 - // unlayered, tables.css would go on outranking every layered rule in
402 - // styles.css and the layer adoption would look broken in exactly the place
403 - // it was supposed to help.
404 - //
405 - // Banner outside the layer: a comment participates in no cascade, and a
406 - // reader opening the file should see what it is before seeing an at-rule.
407 - format!(
408 - "/* Generated by makeover-webview from the column descriptions in\n \
409 - build.rs. Do not edit. Columns narrow by priority, never by position:\n \
410 - inserting one changes what is emitted rather than changing which one\n \
411 - silently disappears. */\n{}",
412 - makeover_webview::in_css_layer(&css)
413 - )
414 - }
415 5
416 6 /// Class-and-property overlaps with the generated stylesheet that have been
417 7 /// read and kept.
@@ -500,13 +90,17 @@
500 90 /// something a person can act on rather than something they have to go and
501 91 /// check.
502 92 ///
503 - /// # Sealed, not fixed
93 + /// # No seal
504 94 ///
505 - /// The count is large and shrinking it is `daac5cc7`'s job, not this one's. So
506 - /// this seals at a high water and refuses only growth, the same shape every
507 - /// other drift check in this file has. The number in `DEAD_SELECTOR_HIGH_WATER`
508 - /// is a measurement; when the stylesheet is cut, the build says so and the seal
509 - /// comes down with it.
95 + /// This carried a `DEAD_SELECTOR_HIGH_WATER` of 1,021 when it was written,
96 + /// because the cut was `daac5cc7`'s job and not this one's. That cut landed on
97 + /// 2026-08-22 and took the count to zero, so the high water went with it: the
98 + /// list has to be empty, and a rule written against markup nothing emits fails
99 + /// the build where it is added.
100 + ///
101 + /// A failure here reads two ways and the message cannot tell them apart. Either
102 + /// the rule is dead, or quasi-webview stopped emitting a class it used to and
103 + /// this rule is the only thing that noticed.
510 104 fn check_stylesheet_reaches_markup(frontend: &Path) {
511 105 let opts = Emit::default();
512 106
@@ -529,27 +123,17 @@
529 123 dead.dedup();
530 124
531 125 assert!(
532 - dead.len() <= DEAD_SELECTOR_HIGH_WATER,
126 + dead.is_empty(),
533 127 "{} selectors in this app's stylesheets match no class anything can \
534 - emit, above the recorded {}. Either a rule was written for markup that \
535 - does not exist, or quasi-webview stopped emitting something:\n{}",
128 + emit. Either a rule was written for markup that does not exist, or \
129 + quasi-webview stopped emitting something:\n{}",
536 130 dead.len(),
537 - DEAD_SELECTOR_HIGH_WATER,
538 131 dead.iter()
539 132 .map(|one| format!(" {one}"))
540 133 .collect::<Vec<_>>()
541 134 .join("\n")
542 135 );
543 136
544 - if dead.len() < DEAD_SELECTOR_HIGH_WATER {
545 - println!(
546 - "cargo::warning=dead stylesheet selectors are down to {} from a \
547 - sealed {}; lower the seal in build.rs so they cannot grow back",
548 - dead.len(),
549 - DEAD_SELECTOR_HIGH_WATER
550 - );
551 - }
552 -
553 137 // Said whether or not anything is wrong. The two numbers together are the
554 138 // measurement `43a682b0` asked for, and a count that only appears on
555 139 // failure is a count nobody watches move.
@@ -565,19 +149,7 @@
565 149 /// carry custom properties rather than class rules, so they contribute almost
566 150 /// nothing here; they are read anyway because a generated sheet that grew a
567 151 /// class rule is exactly the drift worth catching.
568 - const APP_STYLESHEETS: &[&str] = &[
569 - "typography.css",
570 - "geometry.css",
571 - "layout.css",
572 - "tables.css",
573 - "styles.css",
574 - ];
575 -
576 - /// How many dead selectors this app is known to carry.
577 - ///
578 - /// A measurement, taken 2026-08-22 by this function. Not a target: `daac5cc7`
579 - /// is the task that cuts them, and this number comes down with it.
580 - const DEAD_SELECTOR_HIGH_WATER: usize = 1021;
152 + const APP_STYLESHEETS: &[&str] = &["typography.css", "geometry.css", "layout.css", "styles.css"];
581 153
582 154 /// Every class named in a selector in this CSS.
583 155 ///
@@ -597,15 +169,55 @@
597 169 /// So a block is classified when it opens: an at-rule block holds rules, and
598 170 /// anything else holds declarations. Classes count everywhere except inside a
599 171 /// declaration block.
172 + ///
173 + /// # Comments are skipped, and the reason is not tidiness
174 + ///
175 + /// A prelude routinely opens with the comment that documents it, and
176 + /// `/* ... */\n@media (max-width: 599px)` does not start with `@` however
177 + /// plainly it is an at-rule. Reading the raw prelude classified six of this
178 + /// sheet's media blocks as declaration blocks and skipped every class inside
Lines truncated
@@ -1,286 +1,63 @@
1 1 /*
2 - GoingsOn: Neobrute Theme
2 + GoingsOn: the app-local remainder.
3 3
4 - Build new UI by COMPOSING the existing vocabulary, in this order of preference:
4 + This file is what is left of a 9,830-line stylesheet after the swap
5 + (goingson@f4edefb) deleted `index.html` and the 84 scripts that built its
6 + panes. What went with them was every rule written against their markup:
7 + 1,426 rules, cut 2026-08-22 against the build's own measurement rather than
8 + by reading.
5 9
6 - 1. Use a UTILITY class for one-off adjustments (.mb-section, .mb-peer, .flex-1,
7 - .text-sm-secondary, .hidden)
8 - 2. Use a LAYOUT PRIMITIVE to position content (.row-flex + .row-flex-{peer,
9 - group,section}, .stack +
10 - .stack-{peer,group,section})
11 - 3. Use a COMPONENT PRIMITIVE for a UI element (.card, .button, .field,
12 - .modal, .badge,
13 - .toggle-switch)
14 - 4. Extend a primitive with a modifier (.button--primary, .button--sm,
15 - .field--compact,
16 - .subtask-item--linked)
17 - 5. ONLY THEN consider a new class | and add it to the right
18 - BAND below
10 + WHAT BELONGS HERE, and it is a short list now. The app describes screens; it
11 + does not name classes. `src/quasi/` names zero of them, so every class this
12 + document can contain comes from quasi-webview or from makeover, and every
13 + selector below is one of theirs. Three kinds of rule survive:
19 14
20 - A new class is a smell, not a goal. Before writing one:
21 - - grep this file for the visual shape you want; almost everything is here
22 - - check whether an existing primitive + modifier composes to your shape
23 - - page-scoped rules under a feature class (`.day-plan .foo`) are a last
24 - resort, not a first move
15 + - the reset and the token block, which are the app's own ground
16 + - box model and type on a vocabulary class, where the design system
17 + supplies depth and colour and declines to supply geometry
18 + - placement for the described shell's chrome, which quasi states as
19 + structure and refuses to state as position, because a terminal has
20 + neither
25 21
26 - WHEN UTILITY-STACKING IS WRONG: A row of mixed-intent elements (one fills,
27 - others size to content) belongs in a NAMED LAYOUT with an explicit
28 - `grid-template-columns`, not in `.row-flex` + utilities. The utility form
29 - encodes intent in browser flex-shrink math rather than in CSS that reads
30 - as intent, especially fragile when the row contains a native `<select>`,
31 - whose intrinsic-min-content sizing fights `flex: 1`. Smell test: if you
32 - find yourself reaching for `.field--compact`, `style="width: auto;"`,
33 - or `min-width: 0 !important` to make a row lay out right, the layout
34 - itself wants a name. See `.work-hours-row` (2026-05-24) for the canonical
35 - form, and `.email-filter-row` / `.contacts-filter-row`, which took the same
36 - naming to a declared run with a fallback on 2026-08-18.
22 + Depth, fill, edge, focus ring and disabled colour are NOT here. They come
23 + from css/layout.css, generated by makeover-webview, and a rule here that
24 + restates one is the bug `check_vocabulary` exists to catch. The one thing an
25 + app must restate is the disabled variant of any property it sets at rest:
26 + layers resolve before specificity, so a base rule in this file beats the
27 + generated `:disabled` in `@layer makeover` however specific that is. See
28 + §9 and §20, where both cases are worked.
37 29
38 - FILE STRUCTURE: search by BAND name (uppercase anchors) to navigate.
39 - Per-section titles use numbered CSS-comment headers ("3. Design System
40 - Variables"); numbered for stability,
41 - not because order matters. Bands group related sections; cascade order is
42 - load-bearing only inside the RESPONSIVE LAYERS band.
30 + ADDING A RULE. Ask first whether the design system already answers it, and
31 + second whether the answer belongs in the description rather than in CSS. A
32 + new class is not an option: nothing can emit one. If a screen needs
33 + something this vocabulary cannot say, the gap is filed against quasi, and
34 + the vocabulary grows through the cascade.
43 35
44 - BAND: FOUNDATIONS
45 - 1. Font Face
46 - 2. CSS Reset & Base
47 - 3. Design System Variables (Neobrute) | tokens (:root)
48 - 4. Utility Classes | shadow/hover/border,
49 - layout, form sizing,
50 - settings, subtask
36 + THE BUILD IS THE CHECK. `check_stylesheet_reaches_markup` in build.rs asks
37 + quasi-webview whether anything can emit each selector here, prints the live
38 + and dead counts on every build, and seals the dead count so it cannot grow.
39 + The set is closed, so a selector outside it is dead rather than merely
40 + unaccounted for. Sections keep their original numbers; the gaps are where a
41 + section used to be.
51 42
52 - BAND: APP SHELL & CHROME
53 - 5. Body & App Shell
54 - 6. Header
55 - 7. Tab Navigation
56 - 8. Main Content & Page Header
43 + LAYERS, and why they are not file position. Three, declared once below:
57 44
58 - BAND: COMPONENT PRIMITIVES & WIDGETS
59 - 9. Buttons | .button + variants/sizes
60 - 10. Quick Add Input
61 - 11. Cards
62 - 12. Tags & Badges
63 - 13. Data Table
64 - 14. Task Table & Rows
65 - 15. Due Date Badges
66 - 16. Events
67 - 17. Email List
68 - 18. Toast Notifications
69 - 19. Modals
70 - 20. Form Elements | .field + kind by
71 - element, +
72 - JS-rendered layout
73 - utilities tail
74 - 21. Footer
75 - 22. Empty & Error States
76 - 23. Filter Bar
45 + base the reset, the token block (1-3)
46 + components everything a component owns at rest (5-33)
47 + responsive every width and capability block (25+59)
77 48
78 - BAND: RESPONSIVE LAYER 1 (see RESPONSIVE STRATEGY)
79 - 24. Responsive - Large Screens & Tablet
80 - (25 merged into 59: one compact pass, see the COMPACT SHELL band)
49 + `makeover` is declared ahead of these by the renderer: the generated
50 + stylesheets are the design system and this file overrides them. Before the
51 + layers, `.ui-mode-mobile .foo` (0,2,0) beat `.foo` (0,1,0) and which rule
52 + won was decided by a prefix. Removing the prefix made file position start
53 + mattering, which is why the compact pass was once split in two. It is one
54 + pass now.
81 55
82 - BAND: A11Y, PAGINATION, FOCUS
83 - 26. Screen Reader & Accessibility
84 - 27. Pagination
85 - 28. Focus & Keyboard Accessibility
86 -
87 - BAND: EMAIL READER
88 - 29. Source Email Link
89 - 30. Email Reader Mode
90 -
91 - BAND: OVERLAYS
92 - 31. Dropdown Menus
93 - 32. Context Menus
94 - 33. Scrollbar Styling
95 - 34. Desktop App Styles (Loading, Skeleton, Spinner)
96 -
97 - BAND: FEATURE SCREENS (A)
98 - 35. Project Dashboard 41. Settings
99 - 36. Task Badges 42. Snooze Options
100 - 37. Progress Bar 43. Bulk Selection
101 - 38. Day Plan View 44. Time Blocking
102 - 39. Timeline 45. App Layout
103 - 40. Unscheduled Tasks 46. Saved Views Sidebar
104 - 47. Contacts
105 -
106 - BAND: WEEKLY REVIEW (V1 + V2 coexist)
107 - (48 deleted 2026-08-06: print styles, no users. 52 vacated 2026-05-24)
108 - 49. Weekly Review (V1, still live)
109 - 50. Weekly Review V2 - Grid Layout
110 - 51. Weekly Review Transitions & Keyboard Navigation
111 -
112 - BAND: FEATURE SCREENS (B)
113 - 52b. Monthly Review 55. Toggle Switch
114 - 53. Import Wizard 56. Project Milestones
115 - 54. Plugin Manager (57 intentionally unused)
116 -
117 - BAND: COMPACT SHELL + CAPABILITY (see TWO AXES)
118 - 58. Mobile Navigation (Nav Dot, Dial, Bottom Sheets)
119 - 25+59. Responsive - Compact (one pass, was two)
120 - 60. Touch Device Hover Disable
121 -
122 - BAND: FEATURE SCREENS (C)
123 - 61. Kanban Board 66. Timer Subview (Time tab pill)
124 - 62. Timer Widget 67. Task Overview
125 - 63. Focus Timer (Pomodoro) 68. Plan/Review Toggle Nudge Dot
126 - 64. Time Summary Panel 69. Finish & Review
127 - 65. Task Time Tracking UI
128 -
129 - BAND: SCOPED STYLESHEETS
130 - Compose window (compose.html standalone Tauri window)
131 -
132 - EXISTING VOCABULARY: grep before inventing a new class.
133 -
134 - UTILITIES
135 - Layout .flex-1, .row-flex + .row-flex-{bound,peer,group,section},
136 - .stack + .stack-{bound,peer,group,section},
137 - .mb-{peer,section}
138 - Text .text-sm-secondary, .text-xs-secondary, .text-center,
139 - .text-left, .text-muted, .text-accent-red
140 - Sizing .flex-1, .w-full
141 - State .hidden, .is-selected, .is-active
142 -
143 - LAYOUT PRIMITIVES
144 - .row-flex (display:flex; align-items:center) + gap modifier .row-flex-N
145 - .stack (vertical) + gap modifier .stack-N
146 - NOT .row: it has no rule here, and the generated layout.css claims the
147 - name for a LIST ROW (makeover-layout RowPart, its parts .row-primary /
148 - -secondary / -meta / -actions). Do not write class="row" for a flex row.
149 - DO write it on a list row that hides actions until hover: .row is the
150 - ancestor .row-actions reveals from. Worn today by the task row, the email
151 - item, the event row and the filled month goal.
152 - The three text parts are NOT scoped to that ancestor -- they are just the
153 - content, secondary and muted colours -- so wear .row-primary /
154 - -secondary / -meta on any list-row text whose colour is its rank, .row
155 - above it or not, and delete the declaration they replace.
156 -
157 - COMPONENT PRIMITIVES
158 - .button + .button--primary / --secondary / --danger / --sm
159 - .card + .card--list-item (a card is a control)
160 - .panel + .panel--shell / --muted (a panel is not; wear
161 - .raised with it for depth)
162 - .field + .field--ghost / --compact (kind rides on the element:
163 - input / select / textarea)
164 - .modal + .modal-overlay
165 - .badge + variants ([data-color], .status-*, --xs / --sm / --filled)
166 - .toggle-switch
167 - .avatar + .avatar--sm / --lg / --unknown
168 - .toast + variants
169 - .pagination-controls
170 - .filter-bar / .filter-select
171 - .dropdown-menu / .context-menu
172 - .empty-state
173 -
174 - CONTROL MODIFIERS
175 - Sizes: .button--sm
176 - Intents: .button--primary / --secondary / --danger
177 - Shapes: .field--compact / --ghost
178 -
179 - TWO AXES (do not casually merge)
180 -
181 - There is no "UI mode". There are two independent questions, and a rule
182 - answers one of them. A tablet in landscape is wide and touch; a desktop
183 - window dragged narrow is compact and pointer. One signal cannot name
184 - four combinations, and trying to is what this section used to describe.
185 -
186 - WIDTH -- how much room there is. Boundaries are makeover-geometry's
187 - `SizeClass`, quoted from Material 3's window size classes:
188 -
189 - @media (max-width: 599px) Compact
190 - @media (min-width: 600px) and (max-width: 839px) Medium
191 - @media (min-width: 840px) Expanded
192 -
193 - Only Compact and its complement are load-bearing here, so most rules
194 - read `(max-width: 599px)` for the compact shell or `(min-width: 600px)`
195 - for the wide one. src-tauri/build.rs takes those numbers from
196 - `SizeClass` directly for the generated tables.css; the hand-written
197 - ones below are the same numbers typed out, so change them there and
198 - here together. Other widths (1024px, 1400px) are ordinary responsive
199 - tuning inside the wide shell, not a third shell.
200 -
201 - CAPABILITY -- what is pointing at it.
202 -
203 - @media (hover: none), (pointer: coarse)
204 -
205 - Same condition the generated geometry.css keys touch density on. Use
206 - it for hover suppression, touch-target sizing, and gestures that need
207 - a pointer to perform. Never for visibility or layout that is really
208 - about room.
209 -
210 - TWO SHELLS, and that is the decision rather than unfinished work. The
211 - wide shell is §§6, 7 and 8; the compact shell is §25+59, one pass. It
212 - used to be two, split only to manage cascade order, and `@layer
213 - responsive` removed the reason. The two shells are two designs, not one
214 - design badly factored, so they do not collapse into one: the compact
215 - shell hides the header, tab strip and pill nav and
216 - puts up a bottom tab bar instead. The boundary between them is 600px and
217 - stays there. Medium (600-839px) gets the wide shell.
218 -
219 - §25+59 Compact | every `(max-width: 599px)` rule. One place;
220 - add new compact overrides here. It sits in
221 - `@layer responsive`, so it beats any component
222 - rule regardless of where either sits in this
223 - file. The old "add it early or late depending
224 - on whether you need to win" instruction is
225 - gone with the split that forced it.
226 - §60 Touch capability | the capability axis. Not a shell.
227 -
228 - `.ui-mode-mobile` appears nowhere in this file, and should not. It still
229 - exists as the explicit user override -- `?ui=mobile` or
230 - `goingson.uiMode` in localStorage -- but the only thing that reads it is
231 - the generated geometry.css, which layers it last so a deliberate choice
232 - beats detection. Nothing sniffs a user agent any more. Do not reach for
233 - the class to mean "small".
234 -
235 - FEATURE GATES are neither axis, and there is one: `.no-card-drag`, set
236 - in js/bootstrap-uimode.js, hiding the List / Board toggle where kanban
237 - cards cannot be dragged. It is a class rather than a media query only
238 - because js/task-board.js needs the same answer. If you add another, name
239 - it for the feature and say next to it why neither axis fits.
240 -
241 - WEEKLY REVIEW V1 + V2 COEXISTENCE
242 -
243 - §49 (V1: .weekly-review-content, .weekly-review-header, ...) and
244 - §50 (V2: .review-grid, .review-card, ...) are BOTH live. weekly-review.js
245 - and mobile.js still render V1 classes; the V2 grid is used by newer
246 - review surfaces. Do not delete §49 until those JS render paths migrate.
247 -
248 - */
249 -
250 - /* ============================================================================
251 - CASCADE LAYERS
252 - ============================================================================
253 - @layer base, components, responsive;
254 -
255 - Earlier in the list = lower priority. A layer beats specificity outright, so
256 - a responsive rule now wins over a component rule wherever it sits in this
257 - file, and inserting a component rule can no longer silently kill one.
258 -
259 - That is the whole point. Before this, `.ui-mode-mobile .foo` (0,2,0) beat
260 - `.foo` (0,1,0) and which rule won was decided by a prefix; removing the
261 - prefix made file position start mattering, and four rules had to be re-homed
262 - or were found already dead. Sections 25 and 59 exist only because of it.
263 -
264 - base the reset, the token block, typography (1-3)
265 - components everything a component owns at rest, plus the
266 - utilities (4-23, 26-58, 61-70)
267 - responsive every width and capability block (24-25, 59-60)
268 -
269 - `makeover` is declared ahead of these in index.html: the generated
270 - stylesheets are the design system and this file overrides them.
271 -
272 - THERE IS NO PRINT LAYER, and there should not be one. §48 held the only
273 - @media print block until 2026-08-06, when it was deleted: nothing in the app
274 - triggers a print, and the block had been quietly losing to the component
275 - rules in §§49-70 for an unknown stretch without anyone noticing. If a print
276 - surface is ever wanted, it starts from what that surface needs, not from
277 - reviving this.
278 -
279 - UTILITIES are in `components` deliberately. They do not currently beat later
280 - component rules either -- `.mb-section` at the top of the file loses to any
281 - component rule setting margin-bottom below it -- so putting them in a lower
282 - layer would have changed behaviour, where leaving them here does not.
283 - `.hidden` is `!important` and is unaffected by any of this.
56 + THERE IS NO PRINT LAYER, and there should not be one. The only `@media
57 + print` block was deleted 2026-08-06: nothing in the app triggers a print,
58 + and the block had been quietly losing to the component rules for an unknown
59 + stretch. If a print surface is ever wanted, it starts from what that surface
60 + needs.
284 61 ============================================================================ */
285 62 @layer base, components, responsive;
286 63
@@ -298,24 +75,22 @@
298 75
299 76 /* 3. Design System Variables (Platinum-informed) */
300 77 :root {
301 - /* Day-plan timeline slot height (1 slot = 15 min). Mobile bumps this for fat-finger touch.
302 - JS reads it via getComputedStyle so the height: in .timeline-slot stays in sync with
303 - positioning math in day-planning-render.js / day-planning.js. */
304 - --timeline-slot-h: 12px;
305 -
306 78 /* --- PLATINUM-INFORMED DESIGN SYSTEM */
307 79 /* Mac OS 8 Platinum, informed rather than copied: two-tone bevels, square
308 80 corners, a neutral ramp, instant state changes, native window chrome.
309 81 Grew out of the neobrutalist system it replaces, which is why the token
310 82 shapes (border width, radius scale, offset shadows) carry over intact. */
311 83
312 - /* --- INTENT LAYER (themeable)
313 - Resolved intent tokens, applied at runtime by js/themes.js (setProperty
314 - on :root). Defaults below are the "goingson" titular theme for first
315 - paint. makeover resolves every theme and themes.js applies the whole
316 - set at runtime, so this block mirrors only the intents this stylesheet
317 - actually reads. Adding a rule that reads a new intent means adding its
318 - default here too, or it has no value until the first paint is over. */
84 + /* --- INTENT LAYER
85 + Resolved intent tokens for the "goingson" titular theme. These were the
86 + first-paint defaults under the SPA, which had js/themes.js apply the
87 + chosen theme over them at runtime. That script went with the swap and
88 + nothing applies a theme now, so this block is not a default: it is the
89 + theme the app renders. Settings > Appearance still writes a choice, and
90 + nothing reads it (filed 2026-08-22).
91 +
92 + Only the intents this stylesheet reads are here. A rule that reads a new
93 + one needs its value added here too, or the declaration is invalid. */
319 94 --surface-page: #AEB6DC;
320 95 --surface-raised: #D9DDF4;
321 96 --surface-sunken: #BAC2E6;
@@ -325,7 +100,7 @@
325 100 surface-raised, away from resolved content, so it goes lighter on a light
326 101 theme and darker on a dark one. This default is the resolved goingson
327 102 value: an intent read by a rule needs one here too, or the whole
328 - declaration is invalid until themes.js runs rather than degrading. */
103 + declaration is invalid rather than degrading. */
329 104 --surface-well: #F0F4FF;
330 105 /* The two edges of a bevel. makeover derives both from the theme's raised
331 106 surface, so they arrive with the rest of the intent set. */
@@ -442,40 +217,6 @@
442 217 }
443 218
444 219 @layer components {
445 - /* 4. Utility Classes (Shadow, Hover, Border) */
446 - /* --- LAYOUT UTILITIES */
447 - /* Compose: .row-flex + .row-flex-{bound,peer,group,section} for
448 - align-items:center rows. .flex-1 to grow a child. */
449 - .flex-1 { flex: 1; }
450 - .text-accent-red { color: var(--danger); }
451 - .mb-section { margin-bottom: var(--gap-section); }
452 - .mb-peer { margin-bottom: var(--gap-peer); }
453 -
454 - /* --- FORM SIZING UTILITIES */
455 - .field--compact { width: auto; min-width: 120px; }
456 - .settings-heading { margin-bottom: var(--gap-section); font-family: var(--font-heading); }
457 - .settings-desc { font-size: var(--font-size-base); color: var(--content-secondary); margin-bottom: var(--gap-section); }
458 -
459 - /* --- SUBTASK UTILITIES */
460 - /* .subtask-item is the base row; --linked is a modifier for subtasks that
461 - reference another task (raised bg + accent border). Use both classes
462 - together: <div class="subtask-item subtask-item--linked">. */
463 - .subtask-item {
464 - display: flex;
465 - align-items: center;
466 - gap: var(--gap-bound);
467 - padding: var(--gap-peer);
468 - background: var(--surface-overlay);
469 - border-radius: var(--radius-sm);
470 - margin-bottom: var(--gap-peer);
471 - }
472 - .subtask-item--linked {
473 - background: var(--surface-sunken);
474 - border-left: var(--border-width) solid var(--action);
475 - }
476 - .subtask-checkbox { cursor: pointer; width: 18px; height: 18px; }
477 - .subtask-checkbox-disabled { cursor: not-allowed; width: 18px; height: 18px; opacity: 0.5; }
478 - .subtask-text-done { text-decoration: line-through; opacity: 0.6; }
479 220
480 221 /* 5. Body & App Shell */
481 222 body {
@@ -496,13 +237,7 @@
496 237 and anything explicitly marked .selectable, stays selectable. */
497 238 .button,
498 239 .tab,
499 - .tab-label,
500 - .tab-navigation,
501 - .mobile-tab,
502 - .mobile-tab-bar,
503 - .mobile-tab-create,
504 240 .badge,
505 - .toggle-switch,
506 241 label,
507 242 button {
508 243 -webkit-user-select: none;
@@ -516,53 +251,6 @@
516 251 user-select: text;
517 252 }
518 253
519 - /* 6. Header (wide shell only) WIDTH
520 - The top app header is part of the wide shell. The compact shell hides it
521 - wholesale and uses the bottom tab bar instead, so these rules are scoped
522 - to a viewport that is not compact and cannot leak into it.
523 -
524 - The header is the tab strip, not a band of its own. It sits on the page
525 - ground in --surface-sunken, carries the line that the pane hangs from
526 - (border-bottom), and shares the pane's width so the two read as one
527 - object. §7 has the tab semantic; §8 has the pane. */
528 - @media (min-width: 600px) {
529 - .app-header {
530 - width: 100%;
531 - max-width: var(--width-container);
532 - margin: 0 auto;
533 - background: var(--surface-sunken);
534 - border-bottom: var(--border-width) solid var(--border);
535 - padding: var(--gap-peer) var(--gap-peer) 0;
536 - display: flex;
537 - align-items: flex-end;
538 - gap: var(--gap-section);
539 - }
540 -
541 - .header-content {
542 - display: flex;
543 - align-items: center;
544 - gap: var(--gap-bound);
545 - }
546 -
547 - /* Utility controls ride the strip. Lifted off the line by the same step
548 - the tabs stand on, so they do not read as sitting in the pane. */
549 - .header-actions {
550 - display: flex;
551 - align-items: center;
552 - gap: var(--gap-peer);
553 - padding-bottom: var(--gap-peer);
554 - /* The tabs own the left edge; everything else is pushed to the far
555 - end. Not space-between: .header-content is empty in the wide shell
556 - and would take the left slot, floating the tabs off the pane's
557 - edge. */
558 - margin-left: auto;
559 - }
560 - }
561 -
562 - .mobile-view-title {
563 - display: none;
564 - }
565 -
566 254 /* 7. Tab Navigation (wide shell only) WIDTH
567 255 The top tab strip belongs to the wide shell; the compact shell uses the
568 256 bottom tab bar. Tabs run from the left edge, which is the pane's left edge.
@@ -575,11 +263,6 @@
575 263 chosen tab is a raised chip, the shape Balanced Breakfast already renders.
576 264 Nothing here may cancel a token the selector emits. */
577 265 @media (min-width: 600px) {
578 - .tab-navigation {
579 - display: flex;
580 - align-items: flex-end;
581 - gap: var(--gap-bound);
582 - }
583 266
584 267 .tab {
585 268 display: flex;
@@ -598,126 +281,6 @@
598 281 .tab.chosen {
599 282 color: var(--content);
600 283 }
601 -
602 - .tab-label {
603 - font-weight: 600;
Lines truncated
@@ -27,7 +27,6 @@
27 27 "/static/typography.css" => (CSS, include_bytes!("../../frontend/css/typography.css")),
28 28 "/static/geometry.css" => (CSS, include_bytes!("../../frontend/css/geometry.css")),
29 29 "/static/layout.css" => (CSS, include_bytes!("../../frontend/css/layout.css")),
30 - "/static/tables.css" => (CSS, include_bytes!("../../frontend/css/tables.css")),
31 30 "/static/styles.css" => (CSS, include_bytes!("../../frontend/css/styles.css")),
32 31
33 32 // htmx is vendored under `frontend/vendor/`, byte for byte the copy
@@ -89,7 +88,6 @@
89 88 "/static/typography.css",
90 89 "/static/geometry.css",
91 90 "/static/layout.css",
92 - "/static/tables.css",
93 91 "/static/styles.css",
94 92 "/static/htmx.min.js",
95 93 "/static/quasi-selection.js",
@@ -329,7 +329,6 @@
329 329 .styled("/static/typography.css")
330 330 .styled("/static/geometry.css")
331 331 .styled("/static/layout.css")
332 - .styled("/static/tables.css")
333 332 .styled("/static/styles.css")
334 333 // Not vendored, so asking for it would be one 404 per document.
335 334 .without_hyperscript()