Skip to main content

max / goingson

The stylesheet check holds ids, keyframes and custom properties too The class half is exact because quasi-webview's vocabulary is closed. Three other ways a rule can be dead were unchecked, and cutting styles.css found all three by hand: 16 id rules from the deleted index.html, four @keyframes whose animation went with the SPA, and a --timeline-slot-h the day-plan scripts read. Nothing stopped them coming back. Ids are not a closed set and cannot be: the renderer writes them from names the app supplies, so there is no vocabulary to ask. The tree answers instead. An id in a selector that appears in no .rs file under src/ cannot be emitted, because the name would have to be written down for the renderer to be handed it. Substring, so a name inside a longer one reads as live: this can miss a dead id and cannot condemn a live one. Keyframes and custom properties are wholly inside the cascade, and the cascade is the unit. Asked per file, --bevel-light looks unused in styles.css and --gap-peer looks undeclared in layout.css, and both readings are wrong. So uses are collected from every sheet. Declarations are condemned only in styles.css: the other three are makeover's output, and an unused declaration there is that generator's business. Found nine more dead custom properties on the first run, in two rounds because the first round freed the second: --content-on-action, --overlay, --line-height-relaxed, --width-modal, --width-sidebar and the lg/xl brutal shadows, then the two shadow offsets those were the only readers of. The four scanners this needed are one. Class position, id position, keyframes name, property declaration and declaration value all need the same thing first, which is whether the cursor is in selector position, in a declaration or in a comment. That distinction is where the undercount bug was, and it is written once now. Each of the three fails on an injected case and passes on a live one, checked by probe rather than assumed: the sheet carries no id and no keyframe today, so both would otherwise have shipped unexercised. The counts are printed on every build for the same reason the class count is. Closes 8db02723.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 23:33 UTC
Commit: b7b2f00267924009485e6246c2a451ecf9e76412
Parent: 775e95a
2 files changed, +308 insertions, -87 deletions
M src-tauri/build.rs +303 -72
@@ -58,7 +58,6 @@
58 58 /// An entry that stops colliding fails the build, the same as above.
59 59 const REVIEWED_ELEMENT_OVERLAPS: &[(&str, &str, &str)] = &[];
60 60
61 - /// Every file that can carry a class name.
62 61 /// Which of this app's stylesheet rules match something the app can emit.
63 62 ///
64 63 /// goingson `43a682b0`, and the restoration of `check_vocabulary_use`.
@@ -101,18 +100,15 @@
101 100 /// A failure here reads two ways and the message cannot tell them apart. Either
102 101 /// the rule is dead, or quasi-webview stopped emitting a class it used to and
103 102 /// this rule is the only thing that noticed.
104 - fn check_stylesheet_reaches_markup(frontend: &Path) {
103 + fn check_stylesheet_reaches_markup(frontend: &Path, described: &Path) {
105 104 let opts = Emit::default();
105 + let sheets = read_stylesheets(frontend);
106 106
107 107 let mut dead: Vec<String> = Vec::new();
108 108 let mut styled = 0usize;
109 - for sheet in APP_STYLESHEETS {
110 - let path = frontend.join("css").join(sheet);
111 - println!("cargo::rerun-if-changed={}", path.display());
112 - let css =
113 - fs::read_to_string(&path).unwrap_or_else(|error| panic!("read css/{sheet}: {error}"));
114 - for class in class_selectors(&css) {
115 - if quasi_webview::vocabulary::covers(&class, &opts) {
109 + for (sheet, scan) in &sheets {
110 + for class in &scan.classes {
111 + if quasi_webview::vocabulary::covers(class, &opts) {
116 112 styled += 1;
117 113 } else {
118 114 dead.push(format!("{sheet}: .{class}"));
@@ -128,21 +124,182 @@
128 124 emit. Either a rule was written for markup that does not exist, or \
129 125 quasi-webview stopped emitting something:\n{}",
130 126 dead.len(),
131 - dead.iter()
132 - .map(|one| format!(" {one}"))
133 - .collect::<Vec<_>>()
134 - .join("\n")
127 + listed(&dead)
135 128 );
136 129
137 130 // Said whether or not anything is wrong. The two numbers together are the
138 131 // measurement `43a682b0` asked for, and a count that only appears on
139 132 // failure is a count nobody watches move.
133 + let ids = check_ids_are_addressed(&sheets, described);
134 + let (keyframes, properties) = check_nothing_declares_what_nothing_reads(&sheets);
140 135 println!(
141 - "cargo::warning=stylesheet reaches markup: {styled} live selectors, {} dead",
142 - dead.len()
136 + "cargo::warning=stylesheet reaches markup: {styled} classes, {ids} ids, \
137 + {keyframes} keyframes and {properties} custom properties, all reachable"
143 138 );
144 139 }
145 140
141 + /// Every id a rule styles is one the app can put in the document.
142 + ///
143 + /// The class half above is exact because the vocabulary is closed. Ids are not
144 + /// closed and cannot be: `quasi-webview` writes them from names the app
145 + /// supplies, for a chrome place, a frame or a field, so there is no set to ask.
146 + ///
147 + /// What there is instead is the tree. An id in a selector that appears in no
148 + /// source file under `src/` cannot be emitted, because the name would have to
149 + /// be written down somewhere for the renderer to be handed it. Coarser than the
150 + /// class question and exact enough for the failure it is here to catch: after
151 + /// the swap this sheet still carried 16 id rules from the deleted `index.html`
152 + /// (`#tasks-view`, `#task-kanban-board`, `#timeline-slots` and the rest), and
153 + /// the class check could not see one of them.
154 + ///
155 + /// A substring match, deliberately. `#task-pagination` is dead if the string
156 + /// `task-pagination` is nowhere in the tree, and a name that appears inside a
157 + /// longer one is treated as live. That direction is the safe one: this can miss
158 + /// a dead id and cannot condemn a live one.
159 + fn check_ids_are_addressed(sheets: &[(String, Scan)], described: &Path) -> usize {
160 + let source = read_tree(described);
161 +
162 + let mut dead: Vec<String> = Vec::new();
163 + let mut addressed = 0usize;
164 + for (sheet, scan) in sheets {
165 + for id in &scan.ids {
166 + if source.contains(id.as_str()) {
167 + addressed += 1;
168 + } else {
169 + dead.push(format!("{sheet}: #{id}"));
170 + }
171 + }
172 + }
173 + dead.sort();
174 + dead.dedup();
175 +
176 + assert!(
177 + dead.is_empty(),
178 + "{} id selectors name an id no file under src/ mentions, so nothing \
179 + can put one in the document:\n{}",
180 + dead.len(),
181 + listed(&dead)
182 + );
183 + addressed
184 + }
185 +
186 + /// Nothing in the sheets declares a name the sheets never read.
187 + ///
188 + /// The two remaining ways a rule can be dead without naming a class, and the
189 + /// two the swap left behind: four `@keyframes` whose `animation` went with the
190 + /// SPA, and a `--timeline-slot-h` the day-plan scripts read.
191 + ///
192 + /// # Why the whole set is read at once
193 + ///
194 + /// A generated sheet declares custom properties this one reads and reads ones
195 + /// this one declares. Asked per file, `--bevel-light` looks unused in
196 + /// `styles.css` and `--gap-peer` looks undeclared in `layout.css`, and both
197 + /// readings are wrong. The question is only meaningful across the cascade.
198 + ///
199 + /// # Only what this app writes is condemned
200 + ///
201 + /// A declaration in `typography.css`, `geometry.css` or `layout.css` is
202 + /// makeover's, and an unused one there is that generator's business rather than
203 + /// something this repo can fix by editing the output. So the check reads every
204 + /// sheet for uses and only `styles.css` for declarations. The generated sheets
205 + /// have their own drift check in `check_vocabulary`.
206 + ///
207 + /// # Where a use can hide
208 + ///
209 + /// A value, and nothing else. Nothing under `frontend/js/` sets a style
210 + /// property, and `quasi-webview` writes custom properties into inline styles
211 + /// (`--track-at`, `--region-share`) rather than reading ones a sheet declares.
212 + /// So a name absent from every declaration value in the cascade is read by
213 + /// nothing.
214 + fn check_nothing_declares_what_nothing_reads(sheets: &[(String, Scan)]) -> (usize, usize) {
215 + let values: String = sheets
216 + .iter()
217 + .map(|(_, scan)| scan.values.as_str())
218 + .collect();
219 +
220 + let mut dead: Vec<String> = Vec::new();
221 + let (mut keyframes, mut properties) = (0usize, 0usize);
222 + for (sheet, scan) in sheets {
223 + for name in &scan.keyframes {
224 + // The name stands bare in `animation: pulse 1s linear`, so this
225 + // asks the value text rather than trying to parse a shorthand
226 + // whose name can sit at any position in the list.
227 + if values.contains(name.as_str()) {
228 + keyframes += 1;
229 + } else {
230 + dead.push(format!("{sheet}: @keyframes {name}"));
231 + }
232 + }
233 + if sheet != HAND_WRITTEN_STYLESHEET {
234 + continue;
235 + }
236 + for property in &scan.properties {
237 + if values.contains(&format!("var({property}")) {
238 + properties += 1;
239 + } else {
240 + dead.push(format!("{sheet}: {property}"));
241 + }
242 + }
243 + }
244 + dead.sort();
245 + dead.dedup();
246 +
247 + assert!(
248 + dead.is_empty(),
249 + "{} names are declared in this app's stylesheets and read by none of \
250 + them:\n{}",
251 + dead.len(),
252 + listed(&dead)
253 + );
254 + (keyframes, properties)
255 + }
256 +
257 + /// Every stylesheet, scanned once, in cascade order.
258 + fn read_stylesheets(frontend: &Path) -> Vec<(String, Scan)> {
259 + APP_STYLESHEETS
260 + .iter()
261 + .map(|sheet| {
262 + let path = frontend.join("css").join(sheet);
263 + println!("cargo::rerun-if-changed={}", path.display());
264 + let css = fs::read_to_string(&path)
265 + .unwrap_or_else(|error| panic!("read css/{sheet}: {error}"));
266 + ((*sheet).to_owned(), scan(&css))
267 + })
268 + .collect()
269 + }
270 +
271 + /// Every source file under a directory, run together.
272 + ///
273 + /// One string rather than a list, because the only question asked of it is
274 + /// whether a name appears at all.
275 + fn read_tree(root: &Path) -> String {
276 + println!("cargo::rerun-if-changed={}", root.display());
277 + let mut all = String::new();
278 + let mut stack = vec![root.to_path_buf()];
279 + while let Some(dir) = stack.pop() {
280 + let entries =
281 + fs::read_dir(&dir).unwrap_or_else(|error| panic!("read {}: {error}", dir.display()));
282 + for entry in entries {
283 + let path = entry.expect("read dir entry").path();
284 + if path.is_dir() {
285 + stack.push(path);
286 + } else if path.extension().is_some_and(|it| it == "rs") {
287 + all.push_str(&fs::read_to_string(&path).expect("read source file"));
288 + all.push('\n');
289 + }
290 + }
291 + }
292 + all
293 + }
294 +
295 + /// A failure list, one per line and indented.
296 + fn listed(dead: &[String]) -> String {
297 + dead.iter()
298 + .map(|one| format!(" {one}"))
299 + .collect::<Vec<_>>()
300 + .join("\n")
301 + }
302 +
146 303 /// The stylesheets this app hand-writes or generates, in cascade order.
147 304 ///
148 305 /// `typography.css` and `geometry.css` are generated by makeover-build and
@@ -151,24 +308,43 @@
151 308 /// class rule is exactly the drift worth catching.
152 309 const APP_STYLESHEETS: &[&str] = &["typography.css", "geometry.css", "layout.css", "styles.css"];
153 310
154 - /// Every class named in a selector in this CSS.
311 + /// The one stylesheet this app writes by hand.
155 312 ///
156 - /// Deliberately crude, and correct for what it is asked. It finds `.name` in
157 - /// selector position and stops a name at the first character CSS does not allow
158 - /// in one. It does not parse.
313 + /// The others are makeover's output. The distinction matters only to
314 + /// [`check_nothing_declares_what_nothing_reads`], which condemns a declaration
315 + /// in this file and reports none in a generated one.
316 + const HAND_WRITTEN_STYLESHEET: &str = "styles.css";
317 +
318 + /// What one stylesheet names, split by where the name sits.
159 319 ///
160 - /// # Selector position is not "depth zero"
320 + /// One walk, because the four questions asked of a sheet all need the same
321 + /// thing first: whether the byte under the cursor is in selector position, in
322 + /// a declaration, or in a comment. Asking them separately meant four scanners
323 + /// with four copies of that distinction, and the copy is where the bug was.
324 + #[derive(Default)]
325 + struct Scan {
326 + /// Class names in selector position.
327 + classes: Vec<String>,
328 + /// Id names in selector position. A `#RRGGBB` colour is a declaration
329 + /// value and never reaches here, which is the whole reason this is read
330 + /// off the walk rather than off a regex over the file.
331 + ids: Vec<String>,
332 + /// Names given to an `@keyframes` block.
333 + keyframes: Vec<String>,
334 + /// Custom properties this sheet declares, as `--name`.
335 + properties: Vec<String>,
336 + /// Every declaration value in the sheet, run together. Read by substring
337 + /// for `var(--name)` and for an animation name, which is coarse and is the
338 + /// safe direction: a name that appears anywhere in a value is treated as
339 + /// used, so this check can miss a dead one and cannot report a live one.
340 + values: String,
341 + }
342 +
343 + /// Read one stylesheet.
161 344 ///
162 - /// The first version of this tracked brace depth and read classes only at zero,
163 - /// which is wrong in the direction that matters: `@media`, `@supports` and
164 - /// `@layer` open a block whose contents are more rules, so every responsive
165 - /// rule in the sheet sat at depth one and was skipped. It reported 96 dead
166 - /// selectors against a 9,830-line stylesheet, which is a believable-looking
167 - /// number and a wrong one.
168 - ///
169 - /// So a block is classified when it opens: an at-rule block holds rules, and
170 - /// anything else holds declarations. Classes count everywhere except inside a
171 - /// declaration block.
345 + /// Deliberately crude, and correct for what it is asked. It does not parse: it
346 + /// tracks what opened the innermost brace, and reads names in the position
347 + /// that block puts them in.
172 348 ///
173 349 /// # Comments are skipped, and the reason is not tidiness
174 350 ///
@@ -176,31 +352,30 @@
176 352 /// `/* ... */\n@media (max-width: 599px)` does not start with `@` however
177 353 /// plainly it is an at-rule. Reading the raw prelude classified six of this
178 354 /// sheet's media blocks as declaration blocks and skipped every class inside
179 - /// them, so the count this function reported was an undercount and the rules
180 - /// it missed were the compact-shell ones. Comment text also reaches the
181 - /// scanner as selector text: `build.rs` in a sentence became a `.rs` class.
355 + /// them, so the count this reported was an undercount and the rules it missed
356 + /// were the compact-shell ones. Comment text also reaches the scanner as
357 + /// selector text: `build.rs` in a sentence became a `.rs` class.
182 358 ///
183 359 /// Not every at-rule holds rules, either. `@font-face` and `@keyframes` open a
184 360 /// block of declarations, and reading them as rules is how `url("x.woff2")`
185 361 /// was reported as a dead `.woff2` selector.
186 - fn class_selectors(css: &str) -> Vec<String> {
362 + fn scan(css: &str) -> Scan {
187 363 /// What the innermost open brace was opened by.
188 364 enum Block {
189 - /// `@media`, `@supports`, `@layer` -- more rules inside.
365 + /// `@media`, `@supports`, `@layer`. More rules inside.
190 366 Rules,
191 - /// A selector, `@font-face` or `@keyframes`. Declarations inside,
192 - /// and no classes to find.
367 + /// A selector, `@font-face` or `@keyframes`. Declarations inside, and
368 + /// no selector names to find.
193 369 Declarations,
194 370 }
195 371
196 372 /// The at-rules whose block holds more rules rather than declarations.
197 373 const CONDITIONAL: &[&str] = &["media", "supports", "layer", "container", "scope"];
198 374
199 - /// Whether this prelude opens a block of rules.
375 + /// A prelude with its leading whitespace and comments read past.
200 376 ///
201 - /// Reads past any run of whitespace and comments first: the prelude of a
202 - /// documented at-rule begins with its documentation.
203 - fn opens_rules(prelude: &str) -> bool {
377 + /// The prelude of a documented at-rule begins with its documentation.
378 + fn head(prelude: &str) -> &str {
204 379 let mut head = prelude.trim_start();
205 380 while let Some(rest) = head.strip_prefix("/*") {
206 381 head = match rest.find("*/") {
@@ -208,26 +383,53 @@
208 383 None => "",
209 384 };
210 385 }
211 - let Some(rest) = head.strip_prefix('@') else {
212 - return false;
213 - };
214 - let name: String = rest
215 - .chars()
216 - .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
217 - .collect();
218 - CONDITIONAL.contains(&name.to_ascii_lowercase().as_str())
386 + head
219 387 }
220 388
221 - let mut found = Vec::new();
389 + /// The at-rule this prelude names, lowercased, if it names one.
390 + fn at_rule(prelude: &str) -> Option<String> {
391 + Some(
392 + head(prelude)
393 + .strip_prefix('@')?
394 + .chars()
395 + .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
396 + .collect::<String>()
397 + .to_ascii_lowercase(),
398 + )
399 + }
400 +
401 + /// The name an `@keyframes` prelude gives its block.
402 + fn keyframes_name(prelude: &str) -> Option<String> {
403 + let rest = head(prelude).strip_prefix("@keyframes")?.trim_start();
404 + let name: String = rest
405 + .chars()
406 + .take_while(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
407 + .collect();
408 + (!name.is_empty()).then_some(name)
409 + }
410 +
411 + /// The identifier starting at `at`, as CSS spells one.
412 + fn ident(bytes: &[u8], css: &str, at: usize) -> Option<(String, usize)> {
413 + let mut end = at;
414 + while end < bytes.len()
415 + && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'-' || bytes[end] == b'_')
416 + {
417 + end += 1;
418 + }
419 + // `.5rem` in selector position is a number, not a name.
420 + (end > at && !bytes[at].is_ascii_digit()).then(|| (css[at..end].to_owned(), end))
421 + }
422 +
423 + let mut out = Scan::default();
222 424 let mut stack: Vec<Block> = Vec::new();
223 425 let bytes = css.as_bytes();
224 426 // Where the current prelude began, so an opening brace can say what kind of
225 - // block it is: at-rule preludes start with `@`.
427 + // block it is.
226 428 let mut prelude = 0usize;
227 429 let mut i = 0;
228 430 while i < bytes.len() {
229 431 // Byte-wise, because `i` walks bytes and a slice taken mid-character
230 - // panics: this sheet's comments contain '▶'.
432 + // panics: this app's stylesheet comments contain '▶'.
231 433 if bytes[i] == b'/' && bytes.get(i + 1) == Some(&b'*') {
232 434 let mut end = i + 2;
233 435 while end + 1 < bytes.len() && !(bytes[end] == b'*' && bytes[end + 1] == b'/') {
@@ -236,42 +438,68 @@
236 438 i = (end + 2).min(bytes.len());
237 439 continue;
238 440 }
441 + let in_declarations = matches!(stack.last(), Some(Block::Declarations));
239 442 match bytes[i] {
240 443 b'{' => {
241 - stack.push(if opens_rules(&css[prelude..i]) {
444 + if let Some(name) = keyframes_name(&css[prelude..i]) {
445 + out.keyframes.push(name);
446 + }
447 + let rules = at_rule(&css[prelude..i])
448 + .is_some_and(|name| CONDITIONAL.contains(&name.as_str()));
449 + stack.push(if rules {
242 450 Block::Rules
243 451 } else {
244 452 Block::Declarations
245 453 });
246 454 prelude = i + 1;
247 455 }
248 - b'}' => {
249 - stack.pop();
456 + b'}' | b';' => {
457 + // One declaration, prelude to terminator. Kept whole rather
458 + // than split at the colon, because `var()` and an animation
459 + // name both sit on the value side. The closing brace counts as
460 + // a terminator too: the last declaration in a block is
461 + // routinely written without a semicolon.
462 + if in_declarations {
463 + out.values.push_str(&css[prelude..i]);
464 + out.values.push('\n');
465 + }
466 + if bytes[i] == b'}' {
467 + stack.pop();
468 + }
250 469 prelude = i + 1;
251 470 }
252 - b';' => prelude = i + 1,
253 - b'.' if !matches!(stack.last(), Some(Block::Declarations)) => {
254 - let start = i + 1;
255 - let mut end = start;
256 - while end < bytes.len()
257 - && (bytes[end].is_ascii_alphanumeric()
258 - || bytes[end] == b'-'
259 - || bytes[end] == b'_')
260 - {
261 - end += 1;
471 + b'.' if !in_declarations => {
472 + if let Some((name, end)) = ident(bytes, css, i + 1) {
473 + out.classes.push(name);
474 + i = end;
475 + continue;
262 476 }
263 - // `.5rem` in selector position is a number, not a class.
264 - if end > start && !bytes[start].is_ascii_digit() {
265 - found.push(css[start..end].to_owned());
477 + }
478 + b'#' if !in_declarations => {
479 + if let Some((name, end)) = ident(bytes, css, i + 1) {
480 + out.ids.push(name);
481 + i = end;
482 + continue;
483 + }
484 + }
485 + b'-' if in_declarations
486 + && bytes.get(i + 1) == Some(&b'-')
487 + && css[prelude..i].trim().is_empty() =>
488 + {
489 + // A custom property, and only where a property name can stand:
490 + // at the start of a declaration. `var(--x)` sits in a value and
491 + // is a use rather than a declaration.
492 + if let Some((name, end)) = ident(bytes, css, i + 2) {
493 + out.properties.push(format!("--{name}"));
494 + i = end;
495 + continue;
266 496 }
267 - i = end;
268 - continue;
269 497 }
270 498 _ => {}
271 499 }
272 500 i += 1;
273 501 }
274 - found
502 + out
275 503 }
276 504
277 505 // `markup_files` stood here until 2026-08-22. It gathered the files this app's
@@ -403,7 +631,10 @@
403 631 // The question `check_vocabulary_use` used to answer, asked of the thing
404 632 // that now answers it. See `check_stylesheet_reaches_markup`, and goingson
405 633 // `43a682b0` for the hole this closes.
406 - check_stylesheet_reaches_markup(&frontend);
634 + check_stylesheet_reaches_markup(
635 + &frontend,
636 + &Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
637 + );
407 638 // `check_touch_density` is dropped, on the instruction the check itself
408 639 // gives for this case: it keeps every copy of `TOUCH_DENSITY` equal to
409 640 // `makeover_geometry::Density::Touch`, and after the swap this frontend
@@ -109,7 +109,6 @@
109 109 --content: #000000;
110 110 --content-secondary: #2D2D2D;
111 111 --content-muted: #6B6B6B;
112 - --content-on-action: #000000;
113 112 --action: #6196FF;
114 113 --danger: #DC3545;
115 114 --success: #5CB85C;
@@ -124,7 +123,6 @@
124 123 --category-four: #F7D154;
125 124 --category-five: #7B68EE;
126 125 --category-six: #17A2B8;
127 - --overlay: rgba(1, 0, 16, 0.5);
128 126
129 127 /* --- APP-LOCAL (not theme intents)
130 128 GoingsOn's component CSS references the intent tokens above directly;
@@ -148,18 +146,13 @@
148 146 the spacing is makeover-geometry's. Nothing app-local is left of it.
149 147 The .raised and .well classes arrive in the same file. */
150 148
151 - /* Drop-shadow offset scale. Only floating surfaces cast one now: raised
152 - objects are read by their bevel, so the xs step and the bare 4px offset
153 - lost their last consumers in the Platinum conversion. */
149 + /* One drop shadow, and one offset under it. Only floating surfaces cast
150 + one: a raised object is read by its bevel, which is what took the xs
151 + step and the bare 4px offset in the Platinum conversion and the lg and
152 + xl steps with the swap. Composed with --bevel-raised, always: the bevel
153 + says lit object, the shadow says above the page. */
154 154 --shadow-offset-md: 3px;
155 - --shadow-offset-lg: 6px;
156 - --shadow-offset-xl: 8px;
157 -
158 - /* Hard drop shadows, floats only. Always composed with --bevel-raised:
159 - the bevel says lit object, the shadow says above the page. */
160 155 --shadow-brutal-md: var(--shadow-offset-md) var(--shadow-offset-md) 0 var(--border);
161 - --shadow-brutal-lg: var(--shadow-offset-lg) var(--shadow-offset-lg) 0 var(--border);
162 - --shadow-brutal-xl: var(--shadow-offset-xl) var(--shadow-offset-xl) 0 var(--border);
163 156
164 157 /* Border radius scale */
165 158 --radius-xs: 0; /* Scrollbars, tiny elements */
@@ -171,8 +164,6 @@
171 164
172 165 /* Layout widths */
173 166 --width-container: 1400px;
174 - --width-modal: 560px;
175 - --width-sidebar: 280px;
176 167
177 168 /* Spacing: see css/geometry.css. --gap-bound, --gap-peer, --gap-group,
178 169 --gap-section, --gap-pane, --gap-page, over a --step-* scale. */
@@ -206,7 +197,6 @@
206 197
207 198 /* Line height scale */
208 199 --line-height-normal: 1.5;
209 - --line-height-relaxed: 1.75;
210 200
211 201 }
212 202