makeover-webview 0.118.0

The webview renderer for makeover-layout. Emits CSS, and is the one renderer that needs no palette: var() is the late binding, so resolution stays with the browser. With the build feature, writes a consumer's generated frontend files and checks the hand-written ones against them.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
//! The table rules: how every described table lays out, narrows and drops
//! columns, written once against the classes its cells carry.
//!
//! Split out of the crate root because it is one subject with its own
//! constants and the longest rule set in the sheet.

use crate::list::{self, cell_part_class};
use crate::{Emit, class, gated, hover_condition};
use makeover_geometry::{Density, Gap};
use makeover_layout::CellPart;
use std::fmt::Write as _;

/// One declared `ch` of a column's floor, in `rem`.
///
/// A figure at a table's text size, 0.6em of seven eighths of the base, is
/// 0.525rem, and a floor that sized a date to exactly its ten figures ended it
/// in an ellipsis with the rest of the row to spare. Nine sixteenths keeps it
/// whole. `rem` rather than the CSS `ch` because a container condition resolves
/// a font-relative unit against a different font than the cell's.
pub(crate) const CH_REM: f32 = 0.5625;

/// How much faster a fill column takes a row's slack than a content or fixed
/// column does.
const FILL_GROW: u8 = 4;

/// The size a table's rows are set in.
///
/// Named once because two rules say it: the table's own, and the sizer's, which
/// has to draw its copies in the rows' type from inside a heading that is set
/// smaller. Two literals would let a column be measured in one size and drawn
/// in another.
const TABLE_TYPE: &str = "var(--text-note)";

/// The frame a table sits in, and how it narrows.
///
/// # Nothing travels with the table
///
/// A table a description produced knows its columns at render time, so any rule
/// written per table would have to travel with the markup: a `<style>` element
/// beside it, which needs `style-src 'unsafe-inline'` and so blocks the MNW
/// server's standing plan to drop it, or the head, which a table swapped in by
/// htmx arrives without. So every rule here is written once, against classes a
/// cell carries.
///
/// # Flex rows, and priority as a shrink factor
///
/// Each row is a flex row and each column a flex item starting at its declared
/// floor, the `min-N` rung a host puts on its cells. The columns
/// line up across rows because every row's cells share one basis, grow and
/// shrink per column. When the table is short of room the browser takes the
/// shortfall in proportion to each column's shrink factor, and
/// [`Priority`](makeover_layout::Priority) is
/// that factor: optional columns give way first, secondary ones next, and an
/// essential column of a kind that cannot be cut short does not give way at all.
///
/// So a table narrows at the width its floors add up to, with no breakpoint and
/// no count of positions. That is the number `makeover-tui` counts in cells and
/// `makeover-immediate` budgets in points, which is what makes a description
/// drop a column at one declared width in every host.
///
/// A droppable column under its floor hides its contents, through a container
/// condition on the cell, so a column reads whole or blank and never as a
/// fragment. The condition cannot read a custom property, which is why the floor
/// is a class from a fixed ladder and the edges are written in per density.
pub(crate) fn table_rules(opts: &Emit) -> String {
    let table = class("table", opts);
    let head = class("table-head", opts);
    let row = class("table-row", opts);
    let heading = class("table-heading", opts);
    let cell = class("cell", opts);
    let mut css = String::new();

    // The table model, wiki `table-model`: specimen C with D's header strip, as
    // Max picked it off rendered specimens. The table paints its own raised
    // ground inside a hairline frame, so the row tones hold whatever it sits
    // on. Text is the note size, 14px at the default base.
    //
    // A table clips sideways rather than scrolling, so its head can stick to
    // the page's scroller: a scroll container of its own would be the nearest
    // scroller, and the head would stick inside a box that never scrolls.
    // `clip` makes no scroll container. Only a table holding code scrolls; see
    // below.
    let bw = opts.border_width;
    let cell_in = class("cell-in", opts);
    let _ = writeln!(
        css,
        ".{table} {{\n    border: {bw} solid var(--row-rule);\n    \
         background: var(--surface-raised);\n    font-size: {TABLE_TYPE};\n    \
         overflow-x: clip;\n}}"
    );
    // Flex rows, so priority can be a shrink factor and the browser narrows the
    // table itself: see "A table narrows itself" below. Every row's cells share
    // one basis, grow and shrink per column, which is what lines the columns up
    // across rows without a table layout doing it.
    //
    // Every value a stacked table changes is written as a pair read off the
    // `--table-stacked` / `--table-grid` toggles: see "Out of room" below.
    let _ = writeln!(
        css,
        ".{table} .{head},\n.{table} .{row} {{\n    display: flex;\n    \
         min-inline-size: var(--table-stacked, max-content) var(--table-grid, 0);\n}}"
    );
    // A minimum rather than a height, since a row grows to fit its tallest
    // cell: a cell holding more than a line grows the row.
    let _ = writeln!(
        css,
        ".{table} .{row} {{\n    min-height: var(--row-block);\n    \
         flex-wrap: var(--table-stacked, nowrap) var(--table-grid, wrap);\n    \
         padding-block: var(--table-stacked, 0) var(--table-grid, var(--gap-bound));\n}}"
    );
    // The head strip stays in view while a long table scrolls under it,
    // `--table-head-offset` below the top of the scroller, which a host sets to
    // the height of a header that sticks above it. Above the rows, since every
    // cell is a size container and so paints as its own layer.
    //
    // Stacked, the head strip goes and each value carries its heading instead.
    // Out of sight and not out of the tree: the head is still the table's
    // column headers to a screen reader, so it is clipped rather than removed.
    let _ = writeln!(
        css,
        ".{table} .{head} {{\n    \
         position: var(--table-stacked, sticky) var(--table-grid, absolute);\n    \
         inset-block-start: var(--table-head-offset, 0);\n    \
         z-index: 1;\n    \
         inline-size: var(--table-stacked, auto) var(--table-grid, 1px);\n    \
         block-size: var(--table-stacked, auto) var(--table-grid, 1px);\n    \
         overflow: var(--table-stacked, visible) var(--table-grid, hidden);\n    \
         clip-path: var(--table-stacked, none) var(--table-grid, inset(50%));\n}}"
    );

    // One column, heading or cell: a flex item that starts at its floor
    // (`.min-N` below), grows if it fills, and gives room up by what it is
    // worth. `min-width: 0` lets it reach nothing, and it clips, so the padding
    // on `.cell-in` never lingers as a gap once the column has closed.
    //
    // A size container, so a droppable column can ask whether it is under its
    // own floor and show nothing rather than a fragment. Type goes on
    // `.cell-in`, so a heading and the cells under it are one box apart from
    // what they hold.
    //
    // A declared `ch` is drawn as `CH_REM`, in `rem`, in the basis and in the
    // container condition both. The CSS `ch` would be read off the cell by the
    // basis and off another font by the condition, so a column would hide at
    // one width while being sized at another: measured, three columns went
    // blank at 520px with room for all of them.
    let _ = writeln!(
        css,
        ".{table} .{heading},\n.{table} .{cell} {{\n    display: flex;\n    \
         align-items: center;\n    flex: 0 1 calc({}rem + 2 * var(--gap-group));\n    \
         min-width: 0;\n    overflow: hidden;\n    container-type: inline-size;\n}}",
        8.0 * CH_REM
    );
    let _ = writeln!(
        css,
        ".{table} .{cell_in} {{\n    flex: 1 1 auto;\n    min-width: 0;\n    \
         overflow: hidden;\n    text-overflow: ellipsis;\n}}"
    );

    // The header strip. Its ink goes on the heading row and is inherited,
    // because `state_rules` colours the sort caret on `.cell-in::after` and a
    // colour on the heading itself would override it. `content-secondary`
    // rather than muted: the strip separates the header now, so the label does
    // not have to stay quiet by being pale. The edge sits under the strip, as
    // in the specimen. The strip is on the row as well as on each heading, so a
    // table whose columns do not fill its width has no gap in it, and on each
    // heading so one that sticks while the rows scroll under it stays opaque.
    // Tracking is typography's to own and nothing in the family owns it yet, so
    // it is a property with the specimen's value behind it.
    //
    // The edge is on the row too, and only there: a table whose columns do
    // not fill it ended the edge at its last heading while the hairlines under
    // it ran on to the frame.
    let _ = writeln!(
        css,
        ".{table} .{head} {{\n    color: var(--content-secondary);\n    \
         background: var(--surface-sunken);\n    border-bottom: {bw} solid var(--bevel-dark);\n}}"
    );
    let _ = writeln!(
        css,
        ".{table} .{heading} {{\n    align-items: flex-end;\n    \
         background: var(--surface-sunken);\n}}"
    );
    let _ = writeln!(
        css,
        ".{table} .{heading} > .{cell_in} {{\n    padding: var(--gap-peer) var(--gap-group);\n    \
         font-size: var(--text-fine);\n    font-weight: bold;\n    \
         letter-spacing: var(--table-heading-tracking, 0.06em);\n    \
         text-transform: uppercase;\n    text-align: start;\n    white-space: nowrap;\n}}"
    );

    // Rows: a hairline between rows and a hover, at every row count. Between
    // rows and not above the first, where the header's edge already draws the
    // line.
    //
    // No stripe. The hairline already separates one row from the next, and
    // makeover 5 has no `row-stripe` to draw one with: the step of the neutral
    // ramp a stripe could take instead is the page, which sits as far as
    // 1.48:1 from raised (goingson) and read as gaps in the table rather than
    // as rows. The terminal, which has no hairline, keeps a stripe on the
    // page; wiki `table-model`.
    let _ = writeln!(
        css,
        ".{table} .{row} > .{cell} > .{cell_in} {{\n    padding: var(--gap-bound) var(--gap-group);\n}}"
    );
    let _ = writeln!(
        css,
        ".{table} .{row} + .{row} {{\n    border-top: {bw} solid var(--row-rule);\n}}"
    );
    css.push_str(&gated(
        hover_condition(),
        &format!(".{table} .{row}:hover {{\n    background: var(--row-hover);\n}}\n"),
    ));

    let [
        kind_identifier,
        kind_date,
        kind_number,
        kind_status,
        kind_actions,
        kind_code,
    ] = [
        "kind-identifier",
        "kind-date",
        "kind-number",
        "kind-status",
        "kind-actions",
        "kind-code",
    ]
    .map(|name| class(name, opts));
    // A column's kind, wiki `table-model`. The class is on the heading and on
    // every cell. A heading takes the column's alignment and nowrap, so it lines
    // up with what is under it, and keeps the header's own type: the face and
    // figures a kind sets go on its cells. Prose has no class and no rule.
    let _ = writeln!(
        css,
        ".{table} .{cell}.{kind_identifier} > .{cell_in} {{\n    font-family: var(--font-mono);\n    \
         font-size: var(--text-fine);\n    overflow-wrap: anywhere;\n}}"
    );
    let _ = writeln!(
        css,
        ".{table} .{kind_date} > .{cell_in} {{\n    white-space: nowrap;\n}}\n\
         .{table} .{cell}.{kind_date} > .{cell_in} {{\n    font-variant-numeric: tabular-nums;\n}}"
    );
    let _ = writeln!(
        css,
        ".{table} .{kind_number} > .{cell_in} {{\n    white-space: nowrap;\n    text-align: var(--table-stacked, end) var(--table-grid, start);\n}}\n\
         .{table} .{cell}.{kind_number} > .{cell_in} {{\n    font-variant-numeric: tabular-nums;\n}}"
    );
    let _ = writeln!(
        css,
        ".{table} .{kind_status} > .{cell_in} {{\n    white-space: nowrap;\n}}"
    );
    let _ = writeln!(
        css,
        ".{table} .{kind_actions} > .{cell_in} {{\n    white-space: nowrap;\n    text-align: var(--table-stacked, end) var(--table-grid, start);\n}}"
    );
    let _ = writeln!(
        css,
        ".{table} .{cell}.{kind_code} > .{cell_in} {{\n    font-family: var(--font-mono);\n    \
         white-space: pre;\n}}"
    );
    // A table holding code is read as code: one row per source line, where a
    // hairline, a padded block and a 45px row each break the reading. So it
    // keeps the frame and the header and drops the record treatment. Keyed on
    // the kind being present, which the table's own markup already says.
    //
    // And it never clips a line. The code column is sized by what it holds,
    // past the reading measure a fill column is capped at, and takes no part
    // in narrowing. Addressed through the row and the head so it outranks the
    // floor rung that gives a fill column its basis. So a code table scrolls sideways, and its
    // head sticks inside it rather than to the page. A prose table never
    // needs to scroll: under its `stacks-N` rung it draws as records.
    let code = format!(".{table}:has(.{kind_code})");
    let _ = writeln!(css, "{code} {{\n    overflow-x: auto;\n}}");
    // One track for every row, as wide as the widest line or the frame,
    // whichever is wider: each row was its own flex line at its own
    // max-content, so a long line moved its own row's columns and the code
    // started at a different place on every row. The code column takes all
    // of a row's slack and the others none, since slack split between columns
    // differs per row with the line's length.
    let _ = writeln!(
        css,
        "{code} {{\n    display: grid;\n    grid-template-columns: minmax(100%, max-content);\n}}"
    );
    let _ = writeln!(
        css,
        "{code} .{row} {{\n    min-height: auto;\n    align-items: baseline;\n}}\n\
         {code} .{row} + .{row} {{\n    border-top: 0;\n}}\n\
         {code} .{row} > .{cell} > .{cell_in} {{\n    padding-block: 0;\n}}\n\
         {code} .{row} > .{cell}.{kind_code},\n{code} .{head} > .{heading}.{kind_code} {{\n    \
         flex: 1 0 auto;\n    max-inline-size: none;\n    overflow: visible;\n    container-type: normal;\n}}\n\
         {code} .{row} > .{cell}:not(.{kind_code}),\n{code} .{head} > .{heading}:not(.{kind_code}) {{\n    \
         flex-grow: 0;\n}}"
    );
    css.push_str(&gated(
        hover_condition(),
        &format!("{code} .{row}:hover {{\n    background: none;\n}}\n"),
    ));
    // A fill column takes most of the slack, bounded at the reading measure,
    // and a content or fixed column grows too, by a quarter of the fill's
    // share: a value longer than its kind floor then takes some room rather
    // than ending in an ellipsis, without the fixed columns splitting the slack
    // evenly with the fill. Even shares left goingson's task titles at 220px
    // with three fixed columns at 238px each at 1440. Once the fill reaches
    // its cap, the rest goes to the others. The cap stops a description column
    // running wider than a readable line. Scoped under `.{table}` for the
    // reason every rule here is: the model is what a cell takes by being in a
    // table, and `.table .cell` at (0,2,0) would outrank a bare class.
    let _ = writeln!(
        css,
        ".{table} .{} {{\n    flex-grow: {FILL_GROW};\n    max-inline-size: var(--measure-reading, 42rem);\n}}",
        class("cell-fill", opts)
    );
    let _ = writeln!(
        css,
        ".{table} .{},\n.{table} .{} {{\n    flex-grow: 1;\n}}",
        class("cell-content", opts),
        class("cell-fixed", opts)
    );
    // And a fill column's text wraps all the way down. It already wrapped at
    // spaces and hyphens, but a word longer than the column overflowed
    // `.cell-in` and was clipped to an ellipsis mid-word, which reads as a
    // different word: MNW's Media table at 360px showed `hydroph...` for
    // `covers/hydrophone-rig.png`. `anywhere` breaks only a word that cannot
    // fit whole, so every other line wraps exactly as it did.
    let _ = writeln!(
        css,
        ".{table} .{} > .{cell_in} {{\n    overflow-wrap: anywhere;\n}}",
        class("cell-fill", opts)
    );
    let _ = writeln!(
        css,
        ".{table} .{} > .{cell_in},\n.{table} .{} > .{cell_in} {{\n    white-space: nowrap;\n}}",
        class("cell-content", opts),
        class("cell-fixed", opts)
    );

    // A table narrows itself. Priority is how much room a column gives up when
    // the table runs out of it: the browser takes the shortfall from optional
    // columns until they reach nothing, then from secondary ones, and an
    // essential column only gives up what a thousandth of the others would.
    // No breakpoint and nothing per table, so a described table narrows at the
    // width its own floors add up to, which is the number the terminal counts
    // in cells and the immediate-mode painter budgets in points.
    //
    // An essential column does not shrink at all. A floor is the narrowest a
    // column may be *before it drops*, which is what the column's floor says
    // and what the other two hosts narrow by, so a column drawn under its floor
    // is the model being disobeyed rather than a tighter fit.
    //
    // It holds for every essential column, whatever its kind. Held for some
    // kinds only, an essential column of the others would have no floor and no
    // blank, and the browser would take the whole shortfall from it while the
    // columns beside it stood at full width.
    //
    // Kind is also the wrong axis for a floor. Shrink is distributed in
    // proportion to basis, and an undeclared basis comes from the heading's
    // length, so between two columns of the same priority the one with the
    // shorter heading would reach illegibility first. A column's minimum
    // legible width is not a fact about its label.
    //
    // What a kind actually governs is what happens *above* the floor, which the
    // rules further up already say: prose and identifiers wrap or end in an
    // ellipsis, the rest stay on one line. Priority governs the floor, and it
    // governs it for every kind.
    let [drops_first, drops_next, keeps] =
        ["cell-drops-first", "cell-drops-next", "cell-keeps"].map(|name| class(name, opts));
    let _ = writeln!(
        css,
        ".{table} .{drops_first} {{\n    flex-shrink: 1000000;\n}}\n\
         .{table} .{drops_next} {{\n    flex-shrink: 1000;\n}}"
    );
    let _ = writeln!(css, ".{table} .{keeps} {{\n    flex-shrink: 0;\n}}");

    // A kept column holding a sizer is as wide as its widest copy: its basis is
    // its content, which the copies fix at one width down the table, rather
    // than a floor counted in `ch` and drawn in another face. See
    // `list::CELL_SIZER` for what was measured.
    //
    // It stops being a size container to do it, since a size container's
    // content counts for nothing, and nothing asks a kept column its size: only
    // a droppable one blanks. Keyed on the sizer being there, so a table written
    // by hand without one keeps its floor. `:has` counts its argument, so this
    // outranks the rung that set the basis wherever the two are written.
    //
    // The copies take the row's type whatever the cell they sit in says: in a
    // heading they would inherit its capitals, weight and tracking, and the
    // head measured 6px wider than the rows under it.
    //
    // Stacked, a cell of controls and the fill column are not lines of the
    // record: the controls are a run at its foot, as wide as they draw, and
    // the fill column is its first line beside whatever gutters the host
    // draws, growing from its floor. So neither takes the whole line every
    // other cell takes. Written
    // per rung because the grid half is the rung's own basis, and before the
    // sizer rule, which has to win the tie for a column holding copies.
    let rungs = (2..=makeover_layout::MIN_CEILING).step_by(2);
    let fill = class("cell-fill", opts);
    for n in rungs.clone() {
        let min = class(&format!("min-{n}"), opts);
        let basis = f32::from(n) * CH_REM;
        let _ = writeln!(
            css,
            ".{table} .{row} > .{kind_actions}.{min} {{\n    \
             flex-basis: var(--table-stacked, calc({basis}rem + 2 * var(--gap-group))) var(--table-grid, auto);\n}}\n\
             .{table} .{row} > .{fill}.{min} {{\n    \
             flex-basis: calc({basis}rem + 2 * var(--gap-group));\n}}"
        );
    }
    let sizer = class(list::CELL_SIZER, opts);
    let _ = writeln!(
        css,
        ".{table} .{keeps}:has(> .{cell_in} > .{sizer}) {{\n    flex-basis: auto;\n    \
         container-type: normal;\n}}\n\
         .{table} .{sizer} {{\n    display: block;\n    block-size: 0;\n    overflow: hidden;\n    \
         visibility: hidden;\n    white-space: nowrap;\n    font-size: {TABLE_TYPE};\n    \
         font-weight: normal;\n    letter-spacing: normal;\n    text-transform: none;\n}}\n\
         .{table} .{sizer} > * {{\n    display: flex;\n    inline-size: max-content;\n}}"
    );

    // The floors, as a ladder. A column's floor is even and at most
    // `MIN_CEILING`, so every floor a description can reach is one of these.
    // Stacked, a cell is a line of its record and takes the whole of it.
    for n in rungs.clone() {
        let _ = writeln!(
            css,
            ".{table} .{} {{\n    flex-basis: var(--table-stacked, calc({}rem + 2 * var(--gap-group))) var(--table-grid, 100%);\n}}",
            class(&format!("min-{n}"), opts),
            f32::from(n) * CH_REM
        );
    }

    // Whole or blank, never a fragment. A droppable column that has lost more
    // than one gap of its floor shows nothing, and its blank then closes over
    // the width it held.
    //
    // Nothing means no height either. Hidden text still lays out, and in a
    // column narrowed toward nothing a fill cell's words wrap one to a line: a
    // two-sentence description stood a row nine lines tall at 420px (MNW's
    // membership tiers). So the blank also holds its block size at zero and
    // clips what it holds.
    //
    // Not `display: none`, which would say the same thing more shortly. Apps
    // set `display` on `.cell-in` for layouts of their own (MNW's feed stacks a
    // two-line name with it), app CSS outranks this layer, and such a rule would
    // quietly bring every dropped cell back; the `build` feature's drift check
    // refuses the pairing for that reason. Nothing sets a block size here.
    //
    // One gap and not none. The shrink factors order the tiers only nearly: a
    // secondary column gives up a fraction of a pixel while the optional ones
    // close, and a condition at the floor itself blanked it the moment they
    // started (measured, at 520px). One gap is far more than that leak and no
    // more than the column's own end padding, so whatever it holds overflows
    // into padding it still has rather than being cut.
    //
    // A container condition cannot read a custom property, so the gap is
    // written in rather than taken from `--gap-group`: once for the pointer
    // gap, and again under the touch condition where the gap is wider.
    for &density in Density::all() {
        let step = Gap::Group.step_at(density).ratio();
        let edge = f32::from(step.numerator) / f32::from(step.denominator);
        let mut block = String::new();
        for n in rungs.clone() {
            let _ = writeln!(
                block,
                "@container (inline-size < {}rem) {{\n    \
                 .{table} :is(.{drops_first}, .{drops_next}).{} > .{cell_in} {{\n        \
                 visibility: hidden;\n        max-block-size: 0;\n        overflow: hidden;\n    }}\n}}",
                f32::from(n) * CH_REM + edge,
                class(&format!("min-{n}"), opts)
            );
            // A kept column that fills is never blanked, and under its floor it
            // truncates on one line rather than wrapping. Its words already
            // break anywhere so an overlong word is not clipped mid-way, and a
            // column squeezed toward nothing then broke at every letter: a
            // subject stood one character to a line at 420px, the whole row as
            // tall as the subject was long. Truncated and present is the
            // table model's rule for essentials that do not fit, and the
            // `cell-in` already clips with an ellipsis once the line is one.
            let _ = writeln!(
                block,
                "@container (inline-size < {}rem) {{\n    \
                 .{table} .{keeps}.{fill}.{} > .{cell_in} {{\n        \
                 white-space: nowrap;\n        overflow-wrap: normal;\n    }}\n}}",
                f32::from(n) * CH_REM + edge,
                class(&format!("min-{n}"), opts),
                fill = class("cell-fill", opts),
            );
        }
        match density {
            Density::Pointer => css.push_str(&block),
            Density::Touch => css.push_str(&gated(Some(density.media_condition()), &block)),
        }
    }

    // Short of room: see [`list::SHORT_RUNGS`]. A table carrying a rung is a
    // size container, and under the rung's width its rows flip a pair of
    // space toggles. Only the rows: a container's own styles cannot depend on
    // its own size. Every table's rows start roomy, so a rule choosing
    // between two values reads the same on a table that never carries a rung.
    let _ = writeln!(
        css,
        ".{table} > * {{\n    --table-roomy: ;\n    --table-grid: ;\n}}"
    );
    let (low, high, step) = list::SHORT_RUNGS;
    for n in (low..=high).step_by(usize::from(step)) {
        let short = class(&format!("short-{n}"), opts);
        let _ = writeln!(
            css,
            ".{table}.{short} {{\n    container: table-frame / inline-size;\n}}\n\
             @container table-frame (inline-size < {n}rem) {{\n    \
             .{table}.{short} > * {{\n        --table-short: ;\n        \
             --table-roomy: initial;\n    }}\n}}"
        );
    }

    // A tier drops whole: see [`list::DROP_RUNGS`]. Under a `drop-optional-N`
    // rung the table's optional columns close outright, and under a
    // `drop-secondary-N` its secondary ones, so the width they held goes to
    // the column that fills. Blanking inside the cell cannot do that, since a
    // size container cannot change its own size: a blank column kept the width
    // the shrink left it. These are the tiers the terminal and egui drop, at
    // the same floors.
    let [optional, secondary] = ["optional", "secondary"];
    let _ = writeln!(
        css,
        ".{table} .{drops_first} {{\n    max-inline-size: var(--table-{optional}-closed, none);\n}}\n\
         .{table} .{drops_next} {{\n    max-inline-size: var(--table-{secondary}-closed, none);\n}}"
    );
    let (low, high, step) = list::DROP_RUNGS;
    for tier in [optional, secondary] {
        for n in (low..=high).step_by(usize::from(step)) {
            let drop = class(&format!("drop-{tier}-{n}"), opts);
            let _ = writeln!(
                css,
                ".{table}.{drop} {{\n    container: table-frame / inline-size;\n}}\n\
                 @container table-frame (inline-size < {n}rem) {{\n    \
                 .{table}.{drop} > * {{\n        --table-{tier}-closed: 0;\n    }}\n}}"
            );
        }
    }

    // Out of room: see [`list::STACKS_RUNGS`]. Under a `stacks-N` rung the
    // table's essentials no longer fit even folded, so each row draws as a
    // record (wiki `table-stacked-records`): its fill column first, every
    // other value on a line of its own under its heading, its controls a run
    // at the foot. The rows flip `--table-stacked` / `--table-grid`, which the
    // pairs above and below read.
    //
    // A stacked table is roomy again, its tiers open. The fold and the drops
    // closed columns to buy width for a grid, and a record has no grid to buy
    // width for: every value comes back, so the narrow reading holds more than
    // the narrow grid did. Written after the short rungs so it wins the tie on a
    // table carrying both.
    let (low, high, step) = list::STACKS_RUNGS;
    for n in (low..=high).step_by(usize::from(step)) {
        let stacks = class(&format!("stacks-{n}"), opts);
        let _ = writeln!(
            css,
            ".{table}.{stacks} {{\n    container: table-frame / inline-size;\n}}\n\
             @container table-frame (inline-size < {n}rem) {{\n    \
             .{table}.{stacks} > * {{\n        --table-stacked: ;\n        \
             --table-grid: initial;\n        --table-short: initial;\n        \
             --table-roomy: ;\n        --table-optional-closed: initial;\n        \
             --table-secondary-closed: initial;\n    }}\n}}"
        );
    }
    // A record's lines: the fill column leads and the rest keep their column
    // order after it. The fill column takes the first line whole, beside
    // whatever gutters lead it, and the row's `::after` is the line's end: it
    // is a line wide and cannot fit beside the fill column's floor, so it
    // wraps and the fill column grows into the room it leaves.
    // Ordered -2 and -1, so a host's own leading cells go at -3. Where its controls go is the host's to say, since only
    // the host knows which of its cells are gutters and which are the row's
    // acts. A value's heading is the line's label, set as the head strip set
    // it, and only a cell the host labelled has one: a host leaves the fill
    // column and the controls unlabelled, since `Name: Commons Sampler` reads
    // worse than the name and a control says what it does.
    let _ = writeln!(
        css,
        ".{table} .{row} > .{cell} {{\n    \
         align-items: var(--table-stacked, center) var(--table-grid, baseline);\n}}\n\
         .{table} .{row} > .{fill} {{\n    order: var(--table-stacked, 0) var(--table-grid, -2);\n}}\n\
         .{table} .{row}::after {{\n    content: var(--table-stacked, none) var(--table-grid, \"\");\n    \
         flex-basis: 100%;\n    order: -1;\n}}\n\
         .{table} .{row} > .{cell}[data-heading]::before {{\n    \
         content: var(--table-stacked, none) var(--table-grid, attr(data-heading));\n    \
         flex: 0 0 var(--table-record-label, 40%);\n    padding-inline-start: var(--gap-group);\n    \
         color: var(--content-secondary);\n    font-size: var(--text-fine);\n    \
         font-weight: bold;\n    letter-spacing: var(--table-heading-tracking, 0.06em);\n    \
         text-transform: uppercase;\n    overflow-wrap: anywhere;\n}}"
    );

    // Tiles, for a table holding a `ColumnKind::Picture` column: the host
    // wraps it in `table-tiles` with `tiles-always`, `tiles-never`, or a
    // `tiles-N` rung beside the table's `stacks-N` (`makeover_layout::Tiling`).
    // The wrapper is the container because the rows' grid is the table's own
    // display, which a query on the table itself cannot change. Under the rung
    // the table flips `--tiled-on` / `--tiled-off`, the pair the rules below
    // read: a grid of tiles, each the picture, then the fill column, then the
    // kept columns, with the optional tier left out. As rows, the picture
    // column is not drawn.
    let tiles = class("table-tiles", opts);
    let always = class("tiles-always", opts);
    let picture = class("kind-picture", opts);
    let drops_first = class("cell-drops-first", opts);
    let tile_floor = f32::from(makeover_layout::ColumnKind::Picture.floor()) * CH_REM;
    let on = "--tiled-on: ;\n    --tiled-off: initial;\n    --table-secondary-closed: initial;";
    let _ = writeln!(css, ".{tiles}.{always} > .{table} {{\n    {on}\n}}");
    for n in (low..=high).step_by(usize::from(step)) {
        let rung = class(&format!("tiles-{n}"), opts);
        let _ = writeln!(
            css,
            ".{tiles}.{rung} {{\n    container: tile-frame / inline-size;\n}}\n\
             @container tile-frame (inline-size < {n}rem) {{\n    \
             .{tiles}.{rung} > .{table} {{\n        {on}\n    }}\n}}"
        );
    }
    let _ = writeln!(
        css,
        ".{tiles} > .{table} {{\n    \
         display: var(--tiled-off, block) var(--tiled-on, grid);\n    \
         grid-template-columns: repeat(auto-fill, minmax(min(100%, calc({tile_floor}rem + 2 * var(--gap-group))), 1fr));\n    \
         gap: var(--tiled-off, 0) var(--tiled-on, var(--gap-group));\n    \
         border-color: var(--tiled-off, var(--row-rule)) var(--tiled-on, transparent);\n    \
         background: var(--tiled-off, var(--surface-raised)) var(--tiled-on, transparent);\n}}\n\
         .{tiles} > .{table} > .{head} {{\n    display: var(--tiled-off, flex) var(--tiled-on, none);\n}}\n\
         .{tiles} > .{table} > .{row} {{\n    \
         flex-direction: var(--tiled-off, row) var(--tiled-on, column);\n    \
         min-inline-size: var(--tiled-off, max-content) var(--tiled-on, 0);\n    \
         gap: var(--tiled-off, 0) var(--tiled-on, var(--gap-bound));\n    \
         padding-block: var(--tiled-off, 0) var(--tiled-on, 0 var(--gap-group));\n}}\n\
         .{tiles} > .{table} > .{row} > .{cell} {{\n    flex: var(--tiled-off, 0 1 auto) var(--tiled-on, none);\n}}\n\
         .{tiles} > .{table} > .{row} > .{drops_first} {{\n    display: var(--tiled-off, flex) var(--tiled-on, none);\n}}\n\
         .{tiles} > .{table} > .{row} > .{fill} {{\n    \
         order: var(--tiled-off, 0) var(--tiled-on, -1);\n    \
         font-weight: var(--tiled-off, inherit) var(--tiled-on, var(--weight-strong));\n}}\n\
         .{tiles} .{picture} {{\n    display: var(--tiled-off, none) var(--tiled-on, flex);\n    order: -2;\n}}\n\
         .{tiles} .{picture} img {{\n    \
         inline-size: 100%;\n    aspect-ratio: 1;\n    object-fit: cover;\n    \
         border-radius: var(--radius-control);\n}}"
    );

    // What is inside a cell. Without this every cell is one `.cell` and one
    // content colour, so a button in a cell is painted as text: the drift
    // `RowPart::intent` prevents for list rows.
    //
    // The colour goes on `.cell-value` rather than on `.cell`, and that
    // placement is the whole fix. On the container it would cascade into the
    // tokens and the controls sitting beside the text, which is the bug said
    // in one rule; on the part that is text, it reaches text and stops.
    for part in [
        CellPart::Value,
        CellPart::Tokens,
        CellPart::Actions,
        CellPart::Link,
    ] {
        // Two of the four inherit: a token carries its own tone and an action
        // is a control rather than text. A link is text, the title of its row,
        // so it takes its intent like a value does. Left to inherit, it took
        // nothing: the colour is on `.cell-value` rather than on `.cell`, and an
        // anchor with no colour of its own draws in the browser's link blue.
        //
        // Written as a skip-list rather than as a match on Value, so a member
        // added upstream gets its intent emitted rather than being silently
        // dropped. That is the same trade `part_class`'s fallback makes: land
        // plainly, never land as nothing.
        if !matches!(part, CellPart::Tokens | CellPart::Actions) {
            // The underline says "this goes somewhere" in running text. In a
            // table the row already says it, by being the thing that activates.
            let underline = if part == CellPart::Link {
                "\n    text-decoration: none;"
            } else {
                ""
            };
            let _ = writeln!(
                css,
                ".{} {{\n    color: var(--{});{underline}\n}}",
                class(cell_part_class(part), opts),
                part.intent()
            );
        }
    }

    css
}