its-swiss 0.3.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07e25008eecb7b3bdc7e35b37c934d1c948e30f36bca49283798499740fc6c14
4
- data.tar.gz: 987b4c9b997effa03a2b2071a7d7072580b36d3655d3f760a6299b5e80e3eab4
3
+ metadata.gz: 2a5ab32d42d182f1c9e0c8efa403cd4f90857e9c8c1b955ba31023330b1095a8
4
+ data.tar.gz: 0f39bbc46d60b4374c1334b644c28e4d061cc9778b08b2f9a474cbb4aa824341
5
5
  SHA512:
6
- metadata.gz: 2651041e9048e42c29b62456ebd9d9c8f3ad4077315ea8b73c5e839442c7994031d3e7336efcf02435326da91daa3d5ec853919d906664350fc9537c9a799a08
7
- data.tar.gz: 8ec6e22df73670a4a118cf10d60a87fc756f5fdae210a2134f3a9c3a38be5210ef30187518046f638a58abff2e4c02b463f809ee7a57d22de32eaf8bd17573db
6
+ metadata.gz: 80cff5ca6de7fa9aa649fcf1146e78b9315d61531ef4968875e895db0abd8bd636fd07b33df90b41b38a6374a530945459e4032534c303f599774abd347182c3
7
+ data.tar.gz: 3090006b7c520d07a88c2527a73b18f94cac3a484d225c393d88e8956a1bb8af559e26dafc8bc384922659892f0577638fa9174d2c03a168b23e43fb8f7d4171
data/CHANGELOG.md CHANGED
@@ -2,6 +2,258 @@
2
2
 
3
3
  Semver. Consumers pin `~> 0.1`.
4
4
 
5
+ ## Unreleased
6
+
7
+ ### Fixed
8
+
9
+ - **A pinned specimen stopped existing.** Pages replaces the whole site on
10
+ every deploy, so a version-stamped page survives only if the run writes it
11
+ again. The step meant to carry them forward read them back off the live
12
+ site, for released tags only — so it preserved nothing that a deploy had
13
+ already dropped, and nothing at all for a version that was never tagged.
14
+ `0.6.0.html`, which a published post points at, was gone the next time main
15
+ moved; the deploy after it uploaded `index.html` and `0.6.1.html` and
16
+ nothing else.
17
+
18
+ Pinned versions are kept in `published/` and copied into the site each
19
+ deploy, so what is published is reproducible from the repository rather than
20
+ from itself. `0.6.0.html` is restored there, rendered from the 0.6.0 commit
21
+ by that version's own `bin/specimen`.
22
+
23
+ ## 0.7.0 — Unreleased
24
+
25
+ Every baseline on the grid, in every browser, whatever the font.
26
+
27
+ - **The type is set in faces the library declares.** A line box puts its
28
+ baseline half the leading down and then the font's ascent, and the ascent
29
+ is a number in the font file the library has never been told — so every
30
+ line of type sat on a baseline the library could not move, and a page on
31
+ the grid in one font was off it in the next. `faces.css` declares the
32
+ machine's grotesque three times over `src: local()`, once per ratio of
33
+ leading to size the ladder produces, each with `ascent-override` set to
34
+ that ratio and `descent-override` and `line-gap-override` set to nothing.
35
+ The baseline is then the under edge of the line box, in every line and in
36
+ any browser that honours a `@font-face` descriptor. A register names its
37
+ face — `font-family: var(--face-200), var(--font-family)` — and the tokens
38
+ `--face-150`, `--face-200` and `--face-100` are the three. Chromium and
39
+ Firefox honour the descriptors.
40
+ - **Safari does not, so there the trim is the grid.** WebKit loads the face
41
+ and keeps the font's own metrics. Every text block is still trimmed to its
42
+ cap and its baseline and padded back up — to its *own* leading now,
43
+ `round(up, 1cap, 1lh)`, so a subhead set on two lines is no longer trimmed
44
+ to one, and less the cap as WebKit trims it, which is rounded to a pixel —
45
+ and only where it is needed, and only when told: a trimmed box is a 64th
46
+ short as often as not on the engine the correction is written for and
47
+ half a pixel out on one that trims to the exact cap, which down a column
48
+ is a visible drift, so one line of script ahead of the stylesheets marks
49
+ the document `no-metric-overrides` where the faces are not honoured and
50
+ the trim steps in there alone. `its_swiss_stylesheet_tags`
51
+ writes it; anything linking the stylesheets by hand should too, and the
52
+ README has the line. Buttons, cells and the copy button are trimmed as
53
+ well now, with the correction in their own paddings. A control's own text
54
+ is the one thing neither mechanism reaches in Safari.
55
+ - **Text lives in text elements.** A box that holds blocks is trimmed
56
+ through its first and last child and would be corrected twice, so the trim
57
+ is asked of headings, paragraphs, cells, labels, buttons and the like, and
58
+ of list items and definitions only when they hold text. The shell puts
59
+ the `:footer` slot's text in a paragraph.
60
+ - **Nothing inside a line changes the line.** `code`, `kbd`, `samp`, `small`,
61
+ `sup` and `sub` are given no leading, so a second size on a line never
62
+ asks it for room; the glyphs still sit on the strut's baseline.
63
+ - **No row asks the browser to find a baseline.** `.run` and `.field--inline`
64
+ align on their over edge and `.choice` on its under edge, and a table cell
65
+ on `top`: every box's baselines are whole lines below its own over edge,
66
+ so starting items on one line puts their baselines on one line, without a
67
+ question that a button, a form and a block of text answer three ways.
68
+ - **A button is a box, with its label centred in it.** Two lines tall, on
69
+ the grid, and the label's baseline is the one baseline in the library that
70
+ is not on a line, on purpose: a button is read as a shape, and a label set
71
+ on the second line of a two-line box reads as a field with a rule under
72
+ it. Centred by cap where the faces are honoured, since a face puts the
73
+ label's baseline on the under edge of its line; by the font's own metrics
74
+ otherwise. The keyline is an inset shadow rather than a border, so the box
75
+ is the label's line and two paddings and nothing else. A table cell is a
76
+ line the type is set on and a line the rule closes, with nothing above its
77
+ type.
78
+ - **Each bold face names the regular ones after its own.** A face whose
79
+ every `local()` fails does not load, and the browser goes on to the next
80
+ family — the grotesque stack, with its own metrics — for every heading on
81
+ the page while the body stays on the grid. Falling through to the regular
82
+ file keeps the metrics and loses the weight.
83
+ - **The specimen measures itself.** A third button asks the page the
84
+ question the suite asks: which mechanism this browser is on, which faces
85
+ it loaded, and every box and run of type off the grid, printed on the
86
+ page to be read or pasted.
87
+ - **`its_swiss_typeface`** writes an application's own font under the
88
+ library's face names with the library's descriptors, from a regular and a
89
+ bold file or one variable file, and a monospace if there is one.
90
+ - **The guards measure type, not boxes standing in for it.** Every run of
91
+ text on the page has its baseline on a line: the under edge of a trimmed
92
+ block less its padding, or the under edge of the rectangle the engine
93
+ reports for an untrimmed run, which with no descent is the baseline and
94
+ with the font's own is a descent off it. The question is one function; a
95
+ Playwright job asks it of the published specimen in Chromium, WebKit and
96
+ Firefox, and the Chromium suite asks it again with the faces taken away,
97
+ which is Safari's page. A page that trims is measured box by box rather
98
+ than from the top, so the 64ths are let go and every whole pixel is not.
99
+
100
+ Why: 0.5.0 made the baseline real with a property one engine had, and 0.6.1
101
+ found three ways the page came apart in the others and fixed the three. The
102
+ faces replace the font's metrics with the ladder's, which is the only thing
103
+ that was ever going to hold in a browser nobody had checked.
104
+
105
+ A consumer that set a register of its own with a size and a leading should
106
+ add the face for their ratio; a consumer that declared `--font-family` and
107
+ nothing else keeps a readable page in step, and adds `its_swiss_typeface` to
108
+ register it.
109
+
110
+ ## 0.6.1 — 2026-09-02
111
+
112
+ Three ways the column came apart in a browser that was not Chromium.
113
+
114
+ - **`.footer` and `.field__error` take their leading from `--line`.** The
115
+ footer was led on `--space-3` and the field error on `--space-2`. The first
116
+ happens to be twenty-four pixels and so was only wrong to read; the second
117
+ is sixteen, and put eight pixels into the column that everything below it
118
+ then carried.
119
+ - **The leading guard is asked of every stylesheet, not only `type.css`.** It
120
+ passed for as long as it did because a register can be declared in any file
121
+ and it was only ever looking in one.
122
+ - **The masthead, the nav and the pagination align on their under edge
123
+ rather than on a baseline.** A trimmed block's under edge *is* the baseline
124
+ of its last line, and every child of those three rows is one line — so
125
+ aligning the edges aligns the baselines by construction, instead of asking
126
+ the browser to find a baseline. Browsers do not agree on that answer once
127
+ trimming is involved, and the masthead is where it shows: it puts a block
128
+ beside a flex container, and a browser that synthesizes those two a few
129
+ pixels apart grows the row past its three lines and carries the difference
130
+ down every section below it. The rows that keep `baseline` are the ones
131
+ that need it, where a line of type sits beside something taller — `.run`,
132
+ `.field--inline`, `.choice`.
133
+ - **The grid is measured without `text-box-trim` as well as with it,** and the
134
+ published page is measured at all. A trimmed box is its cap rounded up to a
135
+ whole line whatever the leading under it says, so trimming hides exactly
136
+ this class of error; a browser without it got a page that came apart from
137
+ the form down. The check now runs twice, and the file `bin/specimen` writes
138
+ is loaded in a browser over `file://` rather than only read as text.
139
+
140
+ Why: the vertical grid was a claim about Chromium. All three were in
141
+ `components.css`, none was visible to the guard that exists to catch it, and
142
+ none was visible on screen in the one browser everything was checked in —
143
+ which is three ways of saying the same thing, and the reason the fix is
144
+ mostly test. The baseline one is the sharpest version of it: the box check
145
+ exempts an item placed by a baseline row, on the grounds that a row is where
146
+ a short thing legitimately sits off the line. So for as long as the masthead
147
+ asked for a baseline, nothing measured where its two halves landed. It does
148
+ not ask any more, and now they are measured like anything else.
149
+
150
+ ## 0.6.0 — 2026-09-02
151
+
152
+ The specimen is published, so nothing has to keep a copy of it.
153
+
154
+ - **`bin/specimen`** writes the page as one static file: the six stylesheets
155
+ inlined verbatim, the engine's own markup, and the accent and the baseline
156
+ as buttons rather than as a second rendering. It needs nothing to display —
157
+ no Rails, no network, no stylesheet it has to go and fetch.
158
+ - **A Pages workflow publishes it on every push to `main`**, keeping every
159
+ released version alongside the current one:
160
+
161
+ | | |
162
+ | --- | --- |
163
+ | `bobbymeyer.github.io/its-swiss/` | the current specimen |
164
+ | `bobbymeyer.github.io/its-swiss/0.6.0.html` | the one a post can pin |
165
+
166
+ Point a post or a release note at the version-stamped copy. A page that
167
+ silently changes what it depicts is worse than one a little behind.
168
+
169
+ Why: the specimen is ERB rendered by Rails, so anything that cannot run Rails
170
+ has had to keep a copy made by hand. Two did, and both drifted — one declared
171
+ a `--baseline` the library had renamed and a `--measure` it never had. A copy
172
+ made by hand is a copy that will be wrong; the fix is to publish the real one.
173
+
174
+ The page also broadcasts its own height by `postMessage`, so an iframe can
175
+ size itself rather than have a height guessed at one width and wrong at every
176
+ other.
177
+
178
+ ## 0.5.0 — 2026-09-02
179
+
180
+ Asking the specimen to show everything found that it did not, and that one
181
+ rule had quietly stopped doing anything.
182
+
183
+ - **`.micro--tap` is gone**, with the rule it led. It gave micro type a taller
184
+ line box so a 14px tap target cleared 24px — and 0.4.0 put `.micro` on a
185
+ whole line, which made every selector in that rule a restatement of what
186
+ the body already said. Nothing caught it: the guards ask whether a leading
187
+ is a whole number of lines, and a redundant rule answers yes.
188
+ - **`.stack`, `.form--inline`, `.field--inline`, `.pairs--stacked` and
189
+ `.figure--cover` are on the specimen**, which is where a component the
190
+ library ships is documented and guarded. They were shipped and shown
191
+ nowhere.
192
+ - **A label beside a control opts out of trimming**, as `.choice label`
193
+ already did. A row that aligns on the baseline cannot align a trimmed box
194
+ against an untrimmed one: it lands on a half pixel and takes the column
195
+ with it, which is what the new inline field did.
196
+
197
+ ### Guards
198
+
199
+ - **Every class the library defines appears on the specimen.** Two documented
200
+ exceptions: `button_to`, which is Rails' wrapper, and `visually-hidden`,
201
+ which is offered to applications and has nothing to show. This is the guard
202
+ that would have caught `.micro--tap`, and it is what makes the specimen a
203
+ claim about coverage rather than a page that happens to be long.
204
+ - The box test exempts a flex item in a baseline-aligned row from the
205
+ column check, as it already exempted an inline-block: both are placed by
206
+ something other than the column. The row itself is still measured, so a row
207
+ that breaks the column still fails.
208
+
209
+ ## 0.4.0 — 2026-08-31
210
+
211
+ **Breaking.** The baseline is now the line, not a third of it. Applications
212
+ setting `--baseline` need to set `--line` instead; everything else follows.
213
+
214
+ ### The grid
215
+
216
+ 0.3.0 put the type on a baseline grid and then registered it to eight pixels —
217
+ a third of the body line. A block could be a whole number of thirds and still
218
+ land every line of type after it somewhere new, which is what a label on a
219
+ sixteen-pixel leading and a section head on thirty-two did, all the way down a
220
+ column. A third of a line is a spacing unit. It is not a baseline.
221
+
222
+ - **`--line` (24px) replaces `--baseline` (8px)** as the interval everything
223
+ vertical registers to. It is also the body leading, because in a baseline
224
+ grid those are one number.
225
+ - **Every leading is a whole number of lines.** `h1` and `h2` move from 40 and
226
+ 32 to 48; the page title from 48 to 72; labels and captions from 16 to 24.
227
+ The page is airier, and that is what the convention looks like.
228
+ - **`--line-2`, `--line-3`, `--line-4`, `--line-6`** for whole-line spacing.
229
+ `--space-*` survives, renamed at its root to `--space-unit`, as the
230
+ **horizontal** step: an inline gap has no baseline to miss.
231
+ - **`--half-line` and `.subgrid`**, the one subgrid, for a block of small
232
+ type. It halves `--line` for the block's *children* — the block's own
233
+ margins belong to the column outside it and are owed whole lines. Declaring
234
+ it on the block itself halved the gap above it and landed the column half a
235
+ line out, which the specimen caught.
236
+
237
+ ### Pictures, rules, controls
238
+
239
+ - **`.figure` puts a picture on the line at any width.** Its box is the
240
+ natural height taken up to the next whole line with `round()`, recomputed as
241
+ the container resizes; the picture is fitted inside with `contain`, so
242
+ nothing of it is lost. `.figure--cover` crops instead. `--ratio` is the
243
+ application's to declare. Tested at four widths.
244
+ - `hr` is a line-tall box rather than a rule with margins either side.
245
+ - A text control is two lines of box: one the text sits on, one the rule
246
+ closes. A checkbox is reset out of that, since the browser draws it at a
247
+ size of its own.
248
+
249
+ ### Guards
250
+
251
+ - The box test measures in lines, and honours `.subgrid` — a block that
252
+ declares a half-line may use one; the block itself still owes whole lines.
253
+ - A picture is measured at 1400, 1100, 903 and 712 pixels wide.
254
+ - `--baseline` may not reappear in the library. A token that no longer means
255
+ what it says is worse than one that is gone.
256
+
5
257
  ## 0.3.0 — 2026-09-01
6
258
 
7
259
  The vertical rhythm, twice: once to put every box on the baseline, and again
@@ -131,7 +383,19 @@ upgrading.
131
383
  - The leading guard now admits `0`, the one value that is not a measurement:
132
384
  an inline box that takes no part in the line it sits on.
133
385
 
134
- ## 0.2.0 — 2026-08-31
386
+ ## 0.2.0 — 2026-08-31 · never published
387
+
388
+ **This version is tagged but is not on RubyGems, and will not be.** Its
389
+ release run passed the tag check, Rubocop and the suite, then failed at
390
+ `rubygems/release-gem`, which runs `bundle exec rake release` — a task the
391
+ Rakefile did not define until 0.3.0 added `require "bundler/gem_tasks"`. By
392
+ the time that was fixed, 0.3.0 was the next release, and publishing an older
393
+ version after two newer ones is worse than the gap.
394
+
395
+ Nothing here is lost: 0.3.0 was cut from a `main` that already contained all
396
+ of it, so every published version since carries these fixes. Only the version
397
+ number is missing — `~> 0.2.0` resolves to nothing, and `~> 0.1` skips over
398
+ it. Everything below shipped in 0.3.0.
135
399
 
136
400
  Everything here was found by the second consumer inside a day of building on
137
401
  0.1.0, which is roughly the point of having one.
data/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  A Swiss typographic style for Rails applications, in two layers.
4
4
 
5
- The **core** is plain CSS and one small piece of JavaScript: tokens, a reset,
6
- typography, grid primitives, components and view transition rules. It needs
7
- nothing but a `<link>` tag.
5
+ The **core** is plain CSS and one small piece of JavaScript: tokens, faces, a
6
+ reset, typography, grid primitives, components and view transition rules. It
7
+ needs nothing but a `<link>` tag.
8
8
 
9
9
  The **engine** ships that core through `app/assets`, plus a base layout shell,
10
10
  partials, a form builder, helpers and an install generator.
@@ -23,6 +23,12 @@ bin/rails generate its_swiss:install
23
23
 
24
24
  Then open `/its-swiss/specimen`.
25
25
 
26
+ The current specimen is also published, for anywhere that cannot run Rails:
27
+ **[bobbymeyer.github.io/its-swiss](https://bobbymeyer.github.io/its-swiss/)**.
28
+ `bin/specimen out` writes it as one static file, and a Pages workflow does
29
+ that on every push to `main`, keeping each released version alongside the
30
+ current one at `/<version>.html` for anything that needs to pin.
31
+
26
32
  ## What is in the gem, and what stays in the application
27
33
 
28
34
  | In the gem | Stays in the application |
@@ -32,7 +38,7 @@ Then open `/its-swiss/specimen`.
32
38
  | A base layout shell with `content_for` slots | Page layouts beyond the shell |
33
39
  | View transition names and durations | Which pages transition to which |
34
40
  | The value scale and the accent slot | Any hue, any palette knowledge |
35
- | A fallback grotesque stack | The typeface |
41
+ | The machine's grotesque, declared to sit on the baseline | The typeface |
36
42
 
37
43
  A pattern enters the gem after it appears in two applications, not before.
38
44
 
@@ -44,11 +50,11 @@ specificity — so the application always wins, without having to out-specify
44
50
  anything or reach for `!important`.
45
51
 
46
52
  ```
47
- @layer its-swiss.tokens, its-swiss.reset, its-swiss.type,
53
+ @layer its-swiss.tokens, its-swiss.faces, its-swiss.reset, its-swiss.type,
48
54
  its-swiss.grid, its-swiss.components, its-swiss.transitions;
49
55
  ```
50
56
 
51
- Each file declares its own layer, so linking the six files individually (what
57
+ Each file declares its own layer, so linking the seven files individually (what
52
58
  `its_swiss_stylesheet_tags` does) and linking the single `its-swiss.css` that
53
59
  imports them resolve identically.
54
60
 
@@ -60,47 +66,162 @@ holding all of them. Nothing here has a default the gem could pick honestly.
60
66
  | Slot | What it is |
61
67
  | --- | --- |
62
68
  | `--accent`, `--accent-ink` | State and emphasis only. Unset, the accent is ink |
63
- | `--font-family` | The typeface. The gem ships none — declare `@font-face` and name it |
69
+ | `--font-family` | The typeface. The gem ships none — declare yours with `its_swiss_typeface`, and this is what a machine without it falls through to |
64
70
  | `--value-chroma`, `--value-hue` | Warms the whole value scale together. Neutral as shipped |
65
- | `--columns`, `--gutter`, `--baseline` | How many fields this problem has, and the unit everything vertical is measured in |
66
- | `--cap-correction` | Read, not set: what a trimmed register adds above its cap to reach the next baseline |
71
+ | `--columns`, `--gutter` | How many fields this problem has |
72
+ | `--line` | The baseline: the interval everything vertical registers to |
73
+ | `--ratio` | A picture's aspect ratio, per figure — the library cannot read one |
74
+ | `--face-150`, `--face-200`, `--face-100` | Read, not set: the face a register names for its ratio of leading to size |
67
75
 
68
76
  ### The baseline
69
77
 
70
- Everything vertical is measured in baselines, and the type sits on them rather
71
- than merely in step with them.
78
+ `--line` (24px) is the interval everything vertical registers to, and the
79
+ leading of the body text. In this style those are one number, because that is
80
+ what a baseline grid is: Müller-Brockmann's horizontal lines are one line of
81
+ text apart and a field is a whole number of them.
82
+
83
+ Every leading is `var(--line)` or a whole multiple of it — `--line-2`,
84
+ `--line-3`, `--line-4`, `--line-6`. Every margin, padding and gap on the
85
+ vertical axis is too. `--space-*` survives as the **horizontal** step: an
86
+ inline gap has no baseline to miss.
87
+
88
+ Boxes in step are only the easy half. Where a line's baseline falls inside its
89
+ line box is the font's decision: half the leading down, then the font's own
90
+ ascent, and the ascent is a number in the font file that the library has never
91
+ been told. A caption and a paragraph can both be in step and still be out of
92
+ register with each other, and a page on the grid in one font is off it in the
93
+ next. 0.5.0 trimmed every register to its type with `text-box-trim`, which
94
+ registered the page in the one browser that trims and left the rest to their
95
+ fonts.
96
+
97
+ So the library sets every register in a face of its own. `faces.css` declares
98
+ the machine's grotesque three times over `src: local()`, each time with its
99
+ ascent set to a ratio of leading to size and its descent and line gap set to
100
+ nothing:
101
+
102
+ ```css
103
+ @font-face {
104
+ font-family: "its-swiss-150";
105
+ src: local("Helvetica Neue"), local("Arial"), local("Liberation Sans"), …;
106
+ ascent-override: 150%;
107
+ descent-override: 0%;
108
+ line-gap-override: 0%;
109
+ }
110
+ ```
72
111
 
73
- Boxes in step are the easy half: line boxes and spacing are whole numbers of
74
- `--baseline`, so blocks that start together stay together down the column.
75
- That alone is not a baseline grid — where a line's baseline falls inside its
76
- line box depends on the font's ascent and the leading either side of it, so a
77
- caption and a paragraph can both be in step and still be three pixels out of
78
- register with each other.
112
+ A line box is then exactly the ascent tall, there is no half-leading for the
113
+ type to sit inside, and the baseline is the under edge of the line box — in
114
+ every line, whatever the font underneath, in a browser that honours the
115
+ descriptors. Chromium and Firefox do. The ladder produces three ratios and
116
+ there are three faces: body, subhead and page title are set on one and a half
117
+ times their size, the small register and the section on twice it, and the
118
+ subgrid puts the small register on its own size. A register is three
119
+ declarations, and the third is what makes the first two a grid:
120
+
121
+ ```css
122
+ .micro { font-family: var(--face-200), var(--font-family); font-size: var(--size-1); line-height: var(--line); }
123
+ ```
79
124
 
80
- So every text register is trimmed to its own type: `text-box: trim-both cap
81
- alphabetic` makes a block's over edge the cap of its first line and its under
82
- edge the baseline of its last, and one padding rounds the cap height up to the
83
- next baseline
125
+ Safari loads the faces and ignores what they say about their metrics, so
126
+ there every text block is also trimmed to its type: `text-box: trim-both cap
127
+ alphabetic` makes the block's over edge the cap of its first line and its
128
+ under edge the baseline of its last, and one padding rounds the cap up to the
129
+ block's own leading —
84
130
 
85
131
  ```css
86
- padding-block-start: calc(round(up, 1cap, var(--baseline)) - 1cap);
132
+ padding-block-start: calc(round(up, 1cap, 1lh) - round(1cap, 1px));
133
+ ```
134
+
135
+ — measured by the browser in cap and line units, so the library still never
136
+ has to be told the font's metrics. The cap is rounded to a pixel because that
137
+ is the cap WebKit trims to, and WebKit is the browser this is for. It is
138
+ published as `--cap-correction`: a component that puts padding above its type
139
+ adds it. Only where it is needed, though, and only when told: a trimmed box
140
+ is a 64th of a pixel short as often as not on the engine the correction is
141
+ written for, and half a pixel out per block on one that trims to the exact
142
+ cap, which down a long column is a visible drift. So one line of script ahead
143
+ of the stylesheets marks the document `no-metric-overrides` where the faces
144
+ are not honoured, and the trim steps in there and nowhere else;
145
+ `its_swiss_stylesheet_tags` writes it, and anything linking the stylesheets
146
+ by hand should too:
147
+
148
+ ```html
149
+ <script>if (!("ascentOverride" in FontFace.prototype)) document.documentElement.classList.add("no-metric-overrides")</script>
150
+ ```
151
+
152
+ Without it the page is on the faces alone: exact in Chromium and Firefox, and
153
+ in Safari in step but not registered. The faces are the mechanism and the
154
+ trim is the fallback. Three things follow.
155
+
156
+ Anything that changes size *inside* a line — `code`, a `small`, a
157
+ superscript — is given no leading at all, so it never asks the line for room;
158
+ its glyphs still sit on the strut's baseline, which is the grid's.
159
+
160
+ No row asks the browser to find a baseline. Every box's baselines are whole
161
+ lines below its own over edge, so a row that starts its items on one line has
162
+ put their baselines on one line; `.run` aligns on `flex-start`, the masthead
163
+ and the nav on `end`, a table cell on `top`, and none of them on `baseline`,
164
+ which is a question three kinds of box answer three ways.
165
+
166
+ Text lives in text elements. The trim is asked of headings, paragraphs,
167
+ terms, cells, captions, labels, list items and definitions that hold text,
168
+ nav links, pagination, buttons — and not of the boxes that hold those, since
169
+ a box that holds blocks is trimmed through its first and last child and would
170
+ be corrected twice. Plain text dropped straight into a `<footer>` or a `<div>`
171
+ is in step and, in Safari, off the baseline; put it in a paragraph. The shell
172
+ does, for the `:footer` slot.
173
+
174
+ A control is two lines: one the type is set on, one the rule closes, with the
175
+ rule's width taken out of the second. A button is a box, two lines tall with
176
+ its label centred in it, and its label's baseline is the one baseline in the
177
+ library that is not on a line — on purpose, since a label set on the second
178
+ line of a two-line box reads as a field with a rule under it. Its keyline is
179
+ an inset shadow rather than a border, so the box is the label's line and two
180
+ paddings and nothing else. A control's own text is the one thing neither
181
+ mechanism reaches in Safari: an input, a select or a textarea cannot be
182
+ trimmed, and with the faces ignored its text sits where the font puts it, a
183
+ few pixels above the line. The box is on the grid; the type in it is the
184
+ font's until WebKit honours the descriptors.
185
+
186
+ #### Your own typeface
187
+
188
+ Declared under the same names with the same descriptors, and the helper writes
189
+ it:
190
+
191
+ ```erb
192
+ <%= its_swiss_typeface regular: "inter-regular.woff2", bold: "inter-bold.woff2" %>
193
+ <%= its_swiss_typeface variable: "inter.woff2", mono: "jetbrains-mono.woff2" %>
87
194
  ```
88
195
 
89
- which the browser computes in cap units, so the library still never has to
90
- be told the font's metrics. It is published as `--cap-correction`: a component
91
- that sets its own padding on trimmed text adds it, as
92
- `calc(var(--space-1) + var(--cap-correction))`, which is the right padding
93
- whether or not the browser trims.
196
+ Put it after the library's stylesheets. The declarations are unlayered, and a
197
+ name defined outside a layer beats the same name defined inside one the way
198
+ the application's rules beat the library's but a browser that resolves a
199
+ name by order rather than by layer wants it last too. `--font-family` is only
200
+ what a machine with none of the faces falls through to: the page is still
201
+ readable and still in step, and no longer registered.
94
202
 
95
- Two things opt out, with `text-box: normal; padding-block-start: 0`: a control,
96
- whose box is the target and whose label is centred in it, and any block whose
97
- content is not type — an image, a swatch, a diagram.
203
+ The faces are the ladder's. Re-proportion `--line` or a size and the ratios
204
+ move with them, and the faces have to be declared again for the ratios the new
205
+ ladder produces.
206
+
207
+ #### The one subgrid
208
+
209
+ A block of small type may sit on a half-line — a dense run of captions, a
210
+ table of figures:
211
+
212
+ ```html
213
+ <div class="subgrid">…</div>
214
+ ```
215
+
216
+ It halves `--line` for the block's **children** and moves the small register
217
+ onto the face for its new ratio, and the leadings and the spacing follow. The
218
+ children, not the block: a block's own
219
+ margins belong to the column outside it and are owed whole lines. Set on the
220
+ block itself it halves the gap above it and lands the column half a line out.
221
+
222
+ A block, and only a block. An inline `<small>` shares its paragraph's line and
223
+ must not change it.
98
224
 
99
- It is behind `@supports`, and the fallback is the box rhythm above. Chromium
100
- and Safari trim; Firefox does not yet. The two do not render identically:
101
- trimming takes the leading out of a block's own box, so a trimmed page is a
102
- few pixels tighter per block and the ladder means what it says rather than
103
- what it says plus half a line. Both stay on the grid.
104
225
 
105
226
  ### The value scale
106
227
 
@@ -136,6 +257,27 @@ Within three units a channel, which is the cost of the four hand-picked
136
257
  values becoming one ladder with a single chroma and a single hue. Measured in
137
258
  Chromium, not calculated.
138
259
 
260
+ ### Pictures
261
+
262
+ The one case that nearly does not transfer from print. A picture's height is
263
+ its fluid width over its ratio, so without help one picture puts the whole
264
+ column below it off the grid at every width but a few.
265
+
266
+ ```html
267
+ <figure class="figure" style="--ratio: 1.618">
268
+ <img src="…" alt="…">
269
+ <figcaption class="micro">…</figcaption>
270
+ </figure>
271
+ ```
272
+
273
+ The box is the picture's natural height taken up to the next whole line, by
274
+ `round()`, recomputed as the container resizes. The picture is fitted inside
275
+ it — `contain` by default, because a library must not crop an image it did not
276
+ choose. `.figure--cover` crops instead, which is the Müller-Brockmann move and
277
+ yours to make where you own the picture.
278
+
279
+ `--ratio` is yours to declare; CSS cannot read an intrinsic one.
280
+
139
281
  ### The grid
140
282
 
141
283
  The gem ships primitives and never a grid.
@@ -182,7 +324,7 @@ run while a view is rendering, and these slots are set once for the whole
182
324
  application — naming the shell on the controller renders it but leaves
183
325
  nowhere to fill it, so every view ends up writing the same masthead.
184
326
 
185
- Note the `:head` slot links `theme.css`. The shell links the library's six
327
+ Note the `:head` slot links `theme.css`. The shell links the library's seven
186
328
  stylesheets and stops; the accent and the grid live in yours, and nothing
187
329
  links it but this.
188
330
 
@@ -197,7 +339,7 @@ Slots, all optional:
197
339
  | `:main_class` | What the page's main region is, if it is a grid |
198
340
  | `:footer` | Whatever belongs after the page |
199
341
 
200
- The shell writes the view transition opt-in, the six stylesheet links, the
342
+ The shell writes the view transition opt-in, the seven stylesheet links, the
201
343
  importmap tags, a skip link, and the flash. It stops there — a page layout
202
344
  beyond the shell is the application's, for the same reason its grid is.
203
345
 
@@ -205,7 +347,8 @@ beyond the shell is the application's, for the same reason its grid is.
205
347
 
206
348
  | | |
207
349
  | --- | --- |
208
- | `its_swiss_stylesheet_tags` | The six links, tracked for Turbo |
350
+ | `its_swiss_stylesheet_tags` | The seven links, tracked for Turbo |
351
+ | `its_swiss_typeface(regular:, bold:)` | The application's typeface, declared under the library's face names |
209
352
  | `nav_link_to(name, url, current:)` | A destination, with `aria-current` when you are at it |
210
353
  | `copy_button(value)` | A value that copies itself |
211
354
  | `its_swiss_form_with(...)` | `form_with`, already holding the library's builder |
@@ -263,8 +406,9 @@ It is the documentation and the regression fixture.
263
406
  ## Tests
264
407
 
265
408
  ```sh
266
- bin/test # everything; browser tests skip
409
+ bin/test # everything; browser tests skip
267
410
  CHROME_BINARY=... CHROMEDRIVER=... bin/test # including the browser tests
411
+ bin/specimen tmp/specimen && node test/browsers/grid.mjs tmp/specimen/index.html # chromium, webkit, firefox
268
412
  ```
269
413
 
270
414
  Tests come first, and a guard is only kept if removing what it guards makes it
@@ -279,10 +423,18 @@ The rest needs a browser, because a rule on the wrong selector reads correctly
279
423
  in the CSS and does nothing on a page. Those assert what Chromium actually
280
424
  resolved: that an unlayered declaration beats the library's layered one, that
281
425
  the accent unset is ink, that quiet ink clears 4.5:1, that every box on the
282
- specimen starts on a baseline and is a whole number of them tall, that every
283
- register's last baseline lands on one, that nothing escapes the page at 390px,
284
- and that the measure lands on a field line the browser really laid out. They
285
- skip loudly rather than pretending to have checked.
426
+ specimen starts and ends on a line, that every run of type on it has its
427
+ baseline on one, that nothing escapes the page at 390px, and that the measure
428
+ lands on a field line the browser really laid out. They skip loudly rather
429
+ than pretending to have checked.
430
+
431
+ The two grid questions are one function, `test/support/on_the_grid.js`, and
432
+ `test/browsers/grid.mjs` asks it of the published specimen in Chromium, WebKit
433
+ and Firefox through Playwright. CI runs all three; a grid checked in one
434
+ browser is a claim about that browser. Chromium is also asked with the faces
435
+ taken away and the document marked, which is the page as Safari lays it out,
436
+ so the trim is measured on every push and not only in the one job that has
437
+ WebKit.
286
438
 
287
439
  There are no pixel tests.
288
440
 
@@ -298,7 +450,8 @@ ladder; the names are the library's rather than the application's.
298
450
  | `--ink-quiet`, `--ink` | unchanged |
299
451
  | `--accent` | unchanged — still the application's to set |
300
452
  | `--font` | `--font-family` |
301
- | `--size-1..5`, `--space-N`, `--baseline`, `--measure`, `--page-max` | unchanged |
453
+ | `--size-1..5`, `--space-N`, `--measure`, `--page-max` | unchanged |
454
+ | `--baseline` (8px) | **gone.** `--line` (24px) is the baseline now — see 0.4.0 |
302
455
  | `.masthead__nav` | `.nav` |
303
456
  | `.channels` | `.pairs` |
304
457
  | `.form`, `.field`, `.button*`, `.copy`, `.errors`, `.hint`, `.empty` | unchanged |
@@ -3,12 +3,13 @@
3
3
  * The layer order is stated before the first import rather than left to the
4
4
  * order the files arrive in, so a slow network cannot reorder the cascade.
5
5
  * Every file states its own layer as well, which is what makes this file and
6
- * six separate <link> tags resolve identically — the Rails helper writes the
7
- * six, and anything else can point at this.
6
+ * seven separate <link> tags resolve identically — the Rails helper writes
7
+ * the seven, and anything else can point at this.
8
8
  */
9
- @layer its-swiss.tokens, its-swiss.reset, its-swiss.type, its-swiss.grid, its-swiss.components, its-swiss.transitions;
9
+ @layer its-swiss.tokens, its-swiss.faces, its-swiss.reset, its-swiss.type, its-swiss.grid, its-swiss.components, its-swiss.transitions;
10
10
 
11
11
  @import url("its_swiss/tokens.css");
12
+ @import url("its_swiss/faces.css");
12
13
  @import url("its_swiss/reset.css");
13
14
  @import url("its_swiss/type.css");
14
15
  @import url("its_swiss/grid.css");