its-swiss 0.4.0 → 0.7.1

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: d30726333f8265818004958fdb2276a6421ee240b3fbc0b7fc977adf1f39ddb3
4
- data.tar.gz: 73e52320a14d39e0cb6da2fc22c8b55de7a57167d746a650232c603d12a7d7fc
3
+ metadata.gz: 21caf44ed83d26ed7f70b687206792252028a0120aafd843821322c776f0fd87
4
+ data.tar.gz: 3b3612935647de8dfc405d205a3adf6fd170b5f0493faa34d019cac02d237e4c
5
5
  SHA512:
6
- metadata.gz: b8c711d2e82584e464c4bc47777a6340a118d53165e20664aff36ab5a43b582273dc60e7f13bbeb55de368054f9e7e9924d637e164d10608db09f1ef97f38631
7
- data.tar.gz: 6dc95c59d9c700ac68c18c2f8915e0a44e2a79d3701fdaa30ea579ec024cb39a65379498ee40f778f09fa2d3575b3830f6c4eec9e626c586d6e1e443acb71ee0
6
+ metadata.gz: 478f0c8ded5c737b5370029359ab04c18b717b6398a9c2c6c3985a2289ebe8e8fc62797e3303d3e05bc593a23b0c565ae0fc88591afb53de3d4f20be638f54a5
7
+ data.tar.gz: 8bcebb3045f45a9d2a9a636b1544936badd47ea45c16ff6f8bcf79c45fe9706b1663da0f70b7d8ad262744d70589f8ede84fca40f30ca4289f4944df30c35ed6
data/CHANGELOG.md CHANGED
@@ -2,6 +2,221 @@
2
2
 
3
3
  Semver. Consumers pin `~> 0.1`.
4
4
 
5
+ ## 0.7.1 — 2026-09-04
6
+
7
+ ### Fixed
8
+
9
+ - **A control's text was clipped at the rule.** The faces put every
10
+ baseline on the under edge of its line by giving the type no descent, and
11
+ a browser centres an input's text in a box of its own and clips it there
12
+ — so with `font: inherit` every descender in a field, a select or a
13
+ textarea was cut off. A control's text is now set in `--font-family`
14
+ itself, with the font's own metrics, on one line inside the two-line box:
15
+ the box stays on the grid and the type is deliberately off the baseline,
16
+ the way a button's label is. `its_swiss_typeface` takes `family:` and
17
+ declares the typeface under its own name as well, so that `--font-family`
18
+ resolves to it.
19
+
20
+ - **A pinned specimen stopped existing.** Pages replaces the whole site on
21
+ every deploy, so a version-stamped page survives only if the run writes it
22
+ again. The step meant to carry them forward read them back off the live
23
+ site, for released tags only — so it preserved nothing that a deploy had
24
+ already dropped, and nothing at all for a version that was never tagged.
25
+ `0.6.0.html`, which a published post points at, was gone the next time main
26
+ moved; the deploy after it uploaded `index.html` and `0.6.1.html` and
27
+ nothing else.
28
+
29
+ Pinned versions are kept in `published/` and copied into the site each
30
+ deploy, so what is published is reproducible from the repository rather than
31
+ from itself. `0.6.0.html` is restored there, rendered from the 0.6.0 commit
32
+ by that version's own `bin/specimen`.
33
+
34
+ ## 0.7.0 — 2026-09-04
35
+
36
+ Every baseline on the grid, in every browser, whatever the font.
37
+
38
+ - **The type is set in faces the library declares.** A line box puts its
39
+ baseline half the leading down and then the font's ascent, and the ascent
40
+ is a number in the font file the library has never been told — so every
41
+ line of type sat on a baseline the library could not move, and a page on
42
+ the grid in one font was off it in the next. `faces.css` declares the
43
+ machine's grotesque three times over `src: local()`, once per ratio of
44
+ leading to size the ladder produces, each with `ascent-override` set to
45
+ that ratio and `descent-override` and `line-gap-override` set to nothing.
46
+ The baseline is then the under edge of the line box, in every line and in
47
+ any browser that honours a `@font-face` descriptor. A register names its
48
+ face — `font-family: var(--face-200), var(--font-family)` — and the tokens
49
+ `--face-150`, `--face-200` and `--face-100` are the three. Chromium and
50
+ Firefox honour the descriptors.
51
+ - **Safari does not, so there the trim is the grid.** WebKit loads the face
52
+ and keeps the font's own metrics. Every text block is still trimmed to its
53
+ cap and its baseline and padded back up — to its *own* leading now,
54
+ `round(up, 1cap, 1lh)`, so a subhead set on two lines is no longer trimmed
55
+ to one, and less the cap as WebKit trims it, which is rounded to a pixel —
56
+ and only where it is needed, and only when told: a trimmed box is a 64th
57
+ short as often as not on the engine the correction is written for and
58
+ half a pixel out on one that trims to the exact cap, which down a column
59
+ is a visible drift, so one line of script ahead of the stylesheets marks
60
+ the document `no-metric-overrides` where the faces are not honoured and
61
+ the trim steps in there alone. `its_swiss_stylesheet_tags`
62
+ writes it; anything linking the stylesheets by hand should too, and the
63
+ README has the line. Buttons, cells and the copy button are trimmed as
64
+ well now, with the correction in their own paddings. A control's own text
65
+ is the one thing neither mechanism reaches in Safari.
66
+ - **Text lives in text elements.** A box that holds blocks is trimmed
67
+ through its first and last child and would be corrected twice, so the trim
68
+ is asked of headings, paragraphs, cells, labels, buttons and the like, and
69
+ of list items and definitions only when they hold text. The shell puts
70
+ the `:footer` slot's text in a paragraph.
71
+ - **Nothing inside a line changes the line.** `code`, `kbd`, `samp`, `small`,
72
+ `sup` and `sub` are given no leading, so a second size on a line never
73
+ asks it for room; the glyphs still sit on the strut's baseline.
74
+ - **No row asks the browser to find a baseline.** `.run` and `.field--inline`
75
+ align on their over edge and `.choice` on its under edge, and a table cell
76
+ on `top`: every box's baselines are whole lines below its own over edge,
77
+ so starting items on one line puts their baselines on one line, without a
78
+ question that a button, a form and a block of text answer three ways.
79
+ - **A button is a box, with its label centred in it.** Two lines tall, on
80
+ the grid, and the label's baseline is the one baseline in the library that
81
+ is not on a line, on purpose: a button is read as a shape, and a label set
82
+ on the second line of a two-line box reads as a field with a rule under
83
+ it. Centred by cap where the faces are honoured, since a face puts the
84
+ label's baseline on the under edge of its line; by the font's own metrics
85
+ otherwise. The keyline is an inset shadow rather than a border, so the box
86
+ is the label's line and two paddings and nothing else. A table cell is a
87
+ line the type is set on and a line the rule closes, with nothing above its
88
+ type.
89
+ - **Each bold face names the regular ones after its own.** A face whose
90
+ every `local()` fails does not load, and the browser goes on to the next
91
+ family — the grotesque stack, with its own metrics — for every heading on
92
+ the page while the body stays on the grid. Falling through to the regular
93
+ file keeps the metrics and loses the weight.
94
+ - **The specimen measures itself.** A third button asks the page the
95
+ question the suite asks: which mechanism this browser is on, which faces
96
+ it loaded, and every box and run of type off the grid, printed on the
97
+ page to be read or pasted.
98
+ - **`its_swiss_typeface`** writes an application's own font under the
99
+ library's face names with the library's descriptors, from a regular and a
100
+ bold file or one variable file, and a monospace if there is one.
101
+ - **The guards measure type, not boxes standing in for it.** Every run of
102
+ text on the page has its baseline on a line: the under edge of a trimmed
103
+ block less its padding, or the under edge of the rectangle the engine
104
+ reports for an untrimmed run, which with no descent is the baseline and
105
+ with the font's own is a descent off it. The question is one function; a
106
+ Playwright job asks it of the published specimen in Chromium, WebKit and
107
+ Firefox, and the Chromium suite asks it again with the faces taken away,
108
+ which is Safari's page. A page that trims is measured box by box rather
109
+ than from the top, so the 64ths are let go and every whole pixel is not.
110
+
111
+ Why: 0.5.0 made the baseline real with a property one engine had, and 0.6.1
112
+ found three ways the page came apart in the others and fixed the three. The
113
+ faces replace the font's metrics with the ladder's, which is the only thing
114
+ that was ever going to hold in a browser nobody had checked.
115
+
116
+ A consumer that set a register of its own with a size and a leading should
117
+ add the face for their ratio; a consumer that declared `--font-family` and
118
+ nothing else keeps a readable page in step, and adds `its_swiss_typeface` to
119
+ register it.
120
+
121
+ ## 0.6.1 — 2026-09-02
122
+
123
+ Three ways the column came apart in a browser that was not Chromium.
124
+
125
+ - **`.footer` and `.field__error` take their leading from `--line`.** The
126
+ footer was led on `--space-3` and the field error on `--space-2`. The first
127
+ happens to be twenty-four pixels and so was only wrong to read; the second
128
+ is sixteen, and put eight pixels into the column that everything below it
129
+ then carried.
130
+ - **The leading guard is asked of every stylesheet, not only `type.css`.** It
131
+ passed for as long as it did because a register can be declared in any file
132
+ and it was only ever looking in one.
133
+ - **The masthead, the nav and the pagination align on their under edge
134
+ rather than on a baseline.** A trimmed block's under edge *is* the baseline
135
+ of its last line, and every child of those three rows is one line — so
136
+ aligning the edges aligns the baselines by construction, instead of asking
137
+ the browser to find a baseline. Browsers do not agree on that answer once
138
+ trimming is involved, and the masthead is where it shows: it puts a block
139
+ beside a flex container, and a browser that synthesizes those two a few
140
+ pixels apart grows the row past its three lines and carries the difference
141
+ down every section below it. The rows that keep `baseline` are the ones
142
+ that need it, where a line of type sits beside something taller — `.run`,
143
+ `.field--inline`, `.choice`.
144
+ - **The grid is measured without `text-box-trim` as well as with it,** and the
145
+ published page is measured at all. A trimmed box is its cap rounded up to a
146
+ whole line whatever the leading under it says, so trimming hides exactly
147
+ this class of error; a browser without it got a page that came apart from
148
+ the form down. The check now runs twice, and the file `bin/specimen` writes
149
+ is loaded in a browser over `file://` rather than only read as text.
150
+
151
+ Why: the vertical grid was a claim about Chromium. All three were in
152
+ `components.css`, none was visible to the guard that exists to catch it, and
153
+ none was visible on screen in the one browser everything was checked in —
154
+ which is three ways of saying the same thing, and the reason the fix is
155
+ mostly test. The baseline one is the sharpest version of it: the box check
156
+ exempts an item placed by a baseline row, on the grounds that a row is where
157
+ a short thing legitimately sits off the line. So for as long as the masthead
158
+ asked for a baseline, nothing measured where its two halves landed. It does
159
+ not ask any more, and now they are measured like anything else.
160
+
161
+ ## 0.6.0 — 2026-09-02
162
+
163
+ The specimen is published, so nothing has to keep a copy of it.
164
+
165
+ - **`bin/specimen`** writes the page as one static file: the six stylesheets
166
+ inlined verbatim, the engine's own markup, and the accent and the baseline
167
+ as buttons rather than as a second rendering. It needs nothing to display —
168
+ no Rails, no network, no stylesheet it has to go and fetch.
169
+ - **A Pages workflow publishes it on every push to `main`**, keeping every
170
+ released version alongside the current one:
171
+
172
+ | | |
173
+ | --- | --- |
174
+ | `bobbymeyer.github.io/its-swiss/` | the current specimen |
175
+ | `bobbymeyer.github.io/its-swiss/0.6.0.html` | the one a post can pin |
176
+
177
+ Point a post or a release note at the version-stamped copy. A page that
178
+ silently changes what it depicts is worse than one a little behind.
179
+
180
+ Why: the specimen is ERB rendered by Rails, so anything that cannot run Rails
181
+ has had to keep a copy made by hand. Two did, and both drifted — one declared
182
+ a `--baseline` the library had renamed and a `--measure` it never had. A copy
183
+ made by hand is a copy that will be wrong; the fix is to publish the real one.
184
+
185
+ The page also broadcasts its own height by `postMessage`, so an iframe can
186
+ size itself rather than have a height guessed at one width and wrong at every
187
+ other.
188
+
189
+ ## 0.5.0 — 2026-09-02
190
+
191
+ Asking the specimen to show everything found that it did not, and that one
192
+ rule had quietly stopped doing anything.
193
+
194
+ - **`.micro--tap` is gone**, with the rule it led. It gave micro type a taller
195
+ line box so a 14px tap target cleared 24px — and 0.4.0 put `.micro` on a
196
+ whole line, which made every selector in that rule a restatement of what
197
+ the body already said. Nothing caught it: the guards ask whether a leading
198
+ is a whole number of lines, and a redundant rule answers yes.
199
+ - **`.stack`, `.form--inline`, `.field--inline`, `.pairs--stacked` and
200
+ `.figure--cover` are on the specimen**, which is where a component the
201
+ library ships is documented and guarded. They were shipped and shown
202
+ nowhere.
203
+ - **A label beside a control opts out of trimming**, as `.choice label`
204
+ already did. A row that aligns on the baseline cannot align a trimmed box
205
+ against an untrimmed one: it lands on a half pixel and takes the column
206
+ with it, which is what the new inline field did.
207
+
208
+ ### Guards
209
+
210
+ - **Every class the library defines appears on the specimen.** Two documented
211
+ exceptions: `button_to`, which is Rails' wrapper, and `visually-hidden`,
212
+ which is offered to applications and has nothing to show. This is the guard
213
+ that would have caught `.micro--tap`, and it is what makes the specimen a
214
+ claim about coverage rather than a page that happens to be long.
215
+ - The box test exempts a flex item in a baseline-aligned row from the
216
+ column check, as it already exempted an inline-block: both are placed by
217
+ something other than the column. The row itself is still measured, so a row
218
+ that breaks the column still fails.
219
+
5
220
  ## 0.4.0 — 2026-08-31
6
221
 
7
222
  **Breaking.** The baseline is now the line, not a third of it. Applications
@@ -179,7 +394,19 @@ upgrading.
179
394
  - The leading guard now admits `0`, the one value that is not a measurement:
180
395
  an inline box that takes no part in the line it sits on.
181
396
 
182
- ## 0.2.0 — 2026-08-31
397
+ ## 0.2.0 — 2026-08-31 · never published
398
+
399
+ **This version is tagged but is not on RubyGems, and will not be.** Its
400
+ release run passed the tag check, Rubocop and the suite, then failed at
401
+ `rubygems/release-gem`, which runs `bundle exec rake release` — a task the
402
+ Rakefile did not define until 0.3.0 added `require "bundler/gem_tasks"`. By
403
+ the time that was fixed, 0.3.0 was the next release, and publishing an older
404
+ version after two newer ones is worse than the gap.
405
+
406
+ Nothing here is lost: 0.3.0 was cut from a `main` that already contained all
407
+ of it, so every published version since carries these fixes. Only the version
408
+ number is missing — `~> 0.2.0` resolves to nothing, and `~> 0.1` skips over
409
+ it. Everything below shipped in 0.3.0.
183
410
 
184
411
  Everything here was found by the second consumer inside a day of building on
185
412
  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,12 +66,12 @@ 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
71
  | `--columns`, `--gutter` | How many fields this problem has |
66
72
  | `--line` | The baseline: the interval everything vertical registers to |
67
73
  | `--ratio` | A picture's aspect ratio, per figure — the library cannot read one |
68
- | `--cap-correction` | Read, not set: what a trimmed register adds above its cap to reach the next baseline |
74
+ | `--face-150`, `--face-200`, `--face-100` | Read, not set: the face a register names for its ratio of leading to size |
69
75
 
70
76
  ### The baseline
71
77
 
@@ -80,31 +86,131 @@ vertical axis is too. `--space-*` survives as the **horizontal** step: an
80
86
  inline gap has no baseline to miss.
81
87
 
82
88
  Boxes in step are only the easy half. Where a line's baseline falls inside its
83
- line box depends on the font's ascent and the leading either side of it, so a
84
- caption and a paragraph can both be in step and still be out of register with
85
- each other. So every text register is trimmed to its own type: `text-box:
86
- trim-both cap alphabetic` makes a block's over edge the cap of its first line
87
- and its under edge the baseline of its last, and one padding rounds the cap
88
- height up to the next line
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
+ ```
111
+
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
+ ```
124
+
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 —
89
130
 
90
131
  ```css
91
- padding-block-start: calc(round(up, 1cap, var(--line)) - 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>
92
150
  ```
93
151
 
94
- which the browser computes in cap units, so the library still never has to
95
- be told the font's metrics. It is published as `--cap-correction`: a component
96
- that sets its own padding on trimmed text adds it, as
97
- `calc(var(--half-line) + var(--cap-correction))`, which is the right padding
98
- whether or not the browser trims.
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.
181
+
182
+ A control's own text — an input, a select, a textarea — is the other run of
183
+ type the library takes off the baseline, and it does so on purpose too. A
184
+ browser sets that text in a box of its own, centres it there and clips it
185
+ there, and on a face with no descent every descender was cut off at the
186
+ rule. So the text is set in `--font-family` itself, with the font's own
187
+ metrics, on one line inside the two-line box: the box is on the grid, the
188
+ line is the ladder's, and where the baseline falls inside the line is the
189
+ font's. Which means the typeface has to exist under its own name as well as
190
+ under the faces; `its_swiss_typeface` declares it there when given
191
+ `family:`, and `--font-family` names it.
192
+
193
+ #### Your own typeface
194
+
195
+ Declared under the same names with the same descriptors, and the helper writes
196
+ it:
197
+
198
+ ```erb
199
+ <%= its_swiss_typeface regular: "inter-regular.woff2", bold: "inter-bold.woff2" %>
200
+ <%= its_swiss_typeface variable: "inter.woff2", mono: "jetbrains-mono.woff2" %>
201
+ <%= its_swiss_typeface variable: "inter.woff2", family: "Inter" %>
202
+ ```
99
203
 
100
- Two things opt out, with `text-box: normal; padding-block-start: 0`: a control,
101
- whose box is the target and whose label is centred in it, and any block whose
102
- content is not typean image, a swatch, a diagram.
204
+ Put it after the library's stylesheets. The declarations are unlayered, and a
205
+ name defined outside a layer beats the same name defined inside one the way
206
+ the application's rules beat the library's but a browser that resolves a
207
+ name by order rather than by layer wants it last too. `--font-family` is only
208
+ what a machine with none of the faces falls through to: the page is still
209
+ readable and still in step, and no longer registered.
103
210
 
104
- It is behind `@supports`, and the fallback is the box rhythm above. Chromium
105
- and Safari trim; Firefox does not yet. The two do not render identically:
106
- trimming takes the leading out of a block's own box, so a trimmed page is a
107
- few pixels tighter per block. Both stay on the grid.
211
+ The faces are the ladder's. Re-proportion `--line` or a size and the ratios
212
+ move with them, and the faces have to be declared again for the ratios the new
213
+ ladder produces.
108
214
 
109
215
  #### The one subgrid
110
216
 
@@ -115,8 +221,9 @@ table of figures:
115
221
  <div class="subgrid">…</div>
116
222
  ```
117
223
 
118
- It halves `--line` for the block's **children**, and the leadings, the spacing
119
- and the cap correction all follow. The children, not the block: a block's own
224
+ It halves `--line` for the block's **children** and moves the small register
225
+ onto the face for its new ratio, and the leadings and the spacing follow. The
226
+ children, not the block: a block's own
120
227
  margins belong to the column outside it and are owed whole lines. Set on the
121
228
  block itself it halves the gap above it and lands the column half a line out.
122
229
 
@@ -225,7 +332,7 @@ run while a view is rendering, and these slots are set once for the whole
225
332
  application — naming the shell on the controller renders it but leaves
226
333
  nowhere to fill it, so every view ends up writing the same masthead.
227
334
 
228
- Note the `:head` slot links `theme.css`. The shell links the library's six
335
+ Note the `:head` slot links `theme.css`. The shell links the library's seven
229
336
  stylesheets and stops; the accent and the grid live in yours, and nothing
230
337
  links it but this.
231
338
 
@@ -240,7 +347,7 @@ Slots, all optional:
240
347
  | `:main_class` | What the page's main region is, if it is a grid |
241
348
  | `:footer` | Whatever belongs after the page |
242
349
 
243
- The shell writes the view transition opt-in, the six stylesheet links, the
350
+ The shell writes the view transition opt-in, the seven stylesheet links, the
244
351
  importmap tags, a skip link, and the flash. It stops there — a page layout
245
352
  beyond the shell is the application's, for the same reason its grid is.
246
353
 
@@ -248,7 +355,8 @@ beyond the shell is the application's, for the same reason its grid is.
248
355
 
249
356
  | | |
250
357
  | --- | --- |
251
- | `its_swiss_stylesheet_tags` | The six links, tracked for Turbo |
358
+ | `its_swiss_stylesheet_tags` | The seven links, tracked for Turbo |
359
+ | `its_swiss_typeface(regular:, bold:)` | The application's typeface, declared under the library's face names |
252
360
  | `nav_link_to(name, url, current:)` | A destination, with `aria-current` when you are at it |
253
361
  | `copy_button(value)` | A value that copies itself |
254
362
  | `its_swiss_form_with(...)` | `form_with`, already holding the library's builder |
@@ -306,8 +414,9 @@ It is the documentation and the regression fixture.
306
414
  ## Tests
307
415
 
308
416
  ```sh
309
- bin/test # everything; browser tests skip
417
+ bin/test # everything; browser tests skip
310
418
  CHROME_BINARY=... CHROMEDRIVER=... bin/test # including the browser tests
419
+ bin/specimen tmp/specimen && node test/browsers/grid.mjs tmp/specimen/index.html # chromium, webkit, firefox
311
420
  ```
312
421
 
313
422
  Tests come first, and a guard is only kept if removing what it guards makes it
@@ -322,10 +431,18 @@ The rest needs a browser, because a rule on the wrong selector reads correctly
322
431
  in the CSS and does nothing on a page. Those assert what Chromium actually
323
432
  resolved: that an unlayered declaration beats the library's layered one, that
324
433
  the accent unset is ink, that quiet ink clears 4.5:1, that every box on the
325
- specimen starts on a baseline and is a whole number of them tall, that every
326
- register's last baseline lands on one, that nothing escapes the page at 390px,
327
- and that the measure lands on a field line the browser really laid out. They
328
- skip loudly rather than pretending to have checked.
434
+ specimen starts and ends on a line, that every run of type on it has its
435
+ baseline on one, that nothing escapes the page at 390px, and that the measure
436
+ lands on a field line the browser really laid out. They skip loudly rather
437
+ than pretending to have checked.
438
+
439
+ The two grid questions are one function, `test/support/on_the_grid.js`, and
440
+ `test/browsers/grid.mjs` asks it of the published specimen in Chromium, WebKit
441
+ and Firefox through Playwright. CI runs all three; a grid checked in one
442
+ browser is a claim about that browser. Chromium is also asked with the faces
443
+ taken away and the document marked, which is the page as Safari lays it out,
444
+ so the trim is measured on every push and not only in the one job that has
445
+ WebKit.
329
446
 
330
447
  There are no pixel tests.
331
448
 
@@ -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");