grailwind 0.8.0 → 0.9.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.
package/furs.js CHANGED
@@ -1,8 +1,21 @@
1
1
  /*
2
- * Grailwind CSS: furs, as a tiled pattern of spots over a ground tincture.
2
+ * Grailwind CSS: furs, as a tiled pattern over a ground tincture: ermine's spots, or the vair
3
+ * family's panes.
3
4
  */
4
5
  import plugin from "tailwindcss/plugin";
5
- import { FURS } from "./data.js";
6
+ import { FUR_PATTERNS, FURS } from "./data.js";
7
+
8
+ /**
9
+ * An SVG image as a CSS background-image value.
10
+ * @param {string} viewBox The SVG's viewBox, e.g. "0 0 50 60".
11
+ * @param {string} fill A CSS colour, e.g. "#f2f2f2", for everything in `body`.
12
+ * @param {string} body SVG elements, drawn in `fill`.
13
+ * @returns {string} A CSS `url("data:image/svg+xml,…")`.
14
+ */
15
+ const svg = (viewBox, fill, body) =>
16
+ `url("data:image/svg+xml,${encodeURIComponent(
17
+ `<svg xmlns='http://www.w3.org/2000/svg' viewBox='${viewBox}'><g fill='${fill}'>${body}</g></svg>`,
18
+ )}")`;
6
19
 
7
20
  const SPOT = [
8
21
  `<circle cx='0' cy='-8.5' r='1.9'/><circle cx='-3.4' cy='-5.6' r='1.9'/><circle cx='3.4' cy='-5.6' r='1.9'/>`,
@@ -18,30 +31,170 @@ export const AT = [
18
31
  [0, 60],
19
32
  [50, 60],
20
33
  ];
21
- const tile = (fill) =>
22
- `url("data:image/svg+xml,${encodeURIComponent(
23
- `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 60'><g fill='${fill}'>${AT.map(
24
- ([x, y]) => `<g transform='translate(${x} ${y})'>${SPOT}</g>`,
25
- ).join("")}</g></svg>`,
26
- )}")`;
34
+ /**
35
+ * Ermine's tile: the spots, on a transparent ground.
36
+ * @param {string} fill A CSS colour for the spots.
37
+ * @returns {string} A CSS `url(…)`, from svg().
38
+ */
39
+ const ermineTile = (fill) =>
40
+ svg(
41
+ "0 0 50 60",
42
+ fill,
43
+ AT.map(([x, y]) => `<g transform='translate(${x} ${y})'>${SPOT}</g>`).join(""),
44
+ );
27
45
 
28
- // The spots are baked into an SVG, which can't read CSS variables, so they take the theme's
29
- // value at build time.
30
- const spots = (fur, theme) => tile(theme(`colors.${FURS[fur][1]}`));
46
+ // The vair family's pane is PANE wide, and its row as high as its outline. Each outline is the
47
+ // pane's right side, from its top down to its base, about the pane's centre line; the left side
48
+ // mirrors it. Each side is symmetric about its own midpoint, so the gap between two panes is the
49
+ // same pane reversed.
50
+ const PANE = 30;
51
+ const VAIR_ROW = 32;
52
+ const SLOPE = VAIR_ROW / 4;
53
+ const POTENT_ROW = 18;
54
+ const OUTLINES = {
55
+ // Slope to the shoulder, upright side, flare to the base corner.
56
+ vair: [
57
+ [0, 0],
58
+ [PANE / 4, SLOPE],
59
+ [PANE / 4, VAIR_ROW - SLOPE],
60
+ [PANE / 2, VAIR_ROW],
61
+ ],
62
+ // A T: the crossbar on top, twice the stem.
63
+ potent: [
64
+ [PANE / 3, 0],
65
+ [PANE / 3, POTENT_ROW / 2],
66
+ [PANE / 6, POTENT_ROW / 2],
67
+ [PANE / 6, POTENT_ROW],
68
+ ],
69
+ };
31
70
 
32
- // Spots across the field, one per tile in each row.
33
- const ACROSS = 6;
71
+ /**
72
+ * @param {string} pattern A value of FUR_PATTERNS, e.g. "counter-vair".
73
+ * @returns {"ermine" | "vair" | "potent"} The family, which sets the pane's outline and density.
74
+ */
75
+ const family = (pattern) =>
76
+ pattern.endsWith("potent") ? "potent" : pattern.endsWith("vair") ? "vair" : "ermine";
77
+
78
+ /**
79
+ * One pane, pointing up, with its top centre at (0, 0).
80
+ * @param {number[][]} outline The pane's right side as [x, y] points, e.g. OUTLINES.vair.
81
+ * @returns {string} An SVG `<path>`, with no fill of its own.
82
+ */
83
+ const pane = (outline) => {
84
+ const right = outline.map(([x, y]) => `${x},${y}`);
85
+ const left = outline.toReversed().map(([x, y]) => `${-x},${y}`);
86
+ return `<path d='M${[...right, ...left].join(" L")} Z'/>`;
87
+ };
34
88
 
89
+ /**
90
+ * The vair family's tile, PANE wide and two rows high. Only the first tincture's panes are drawn;
91
+ * the ground shows through the gaps between them as the second tincture's panes.
92
+ *
93
+ * Row 1 has one pane, centred. Row 2 is offset half a pane in vair and potent, drawn at both edges
94
+ * so the halves join across tiles; in the counter- forms it's mirrored, base to base with row 1.
95
+ *
96
+ * @param {"vair" | "counter-vair" | "potent" | "counter-potent"} pattern
97
+ * @param {string} fill A CSS colour for the drawn panes, e.g. argent's "#f2f2f2" in vair.
98
+ * @returns {string} A CSS `url(…)`, from svg().
99
+ */
100
+ const paneTile = (pattern, fill) => {
101
+ const outline = OUTLINES[family(pattern)];
102
+ const row = outline.at(-1)[1];
103
+ const at = (x, y, path) => `<g transform='translate(${x} ${y})'>${path}</g>`;
104
+ const viewBox = `0 0 ${PANE} ${2 * row}`;
105
+ if (pattern.startsWith("counter-")) {
106
+ // Both rows as one outline: two paths meeting base to base leave a hairline where each one's
107
+ // edge is antialiased.
108
+ const mirrored = outline.toReversed().map(([x, y]) => [x, 2 * row - y]);
109
+ return svg(viewBox, fill, at(PANE / 2, 0, pane([...outline, ...mirrored])));
110
+ }
111
+ const path = pane(outline);
112
+ return svg(
113
+ viewBox,
114
+ fill,
115
+ [at(PANE / 2, 0, path), at(0, row, path), at(PANE, row, path)].join(""),
116
+ );
117
+ };
118
+
119
+ /**
120
+ * A fur's tile. The SVG can't read CSS variables, so it takes the theme's colour at build time.
121
+ * @param {string} fur A key of FURS, e.g. "vairy-or-gules".
122
+ * @param {Function} theme Tailwind's theme() lookup.
123
+ * @returns {string} A CSS `url(…)` of the drawn tincture only; the ground isn't in it.
124
+ */
125
+ const tile = (fur, theme) => {
126
+ const pattern = FUR_PATTERNS[fur];
127
+ const fill = theme(`colors.${FURS[fur][1]}`);
128
+ return pattern === "ermine" ? ermineTile(fill) : paneTile(pattern, fill);
129
+ };
130
+
131
+ /**
132
+ * `fur-<n>` sets --fur-across, n whole tiles across the field. It's left unregistered, so each
133
+ * family keeps its own default: 6 for ermine; 4 for vair, which Fox-Davies calls vair, between
134
+ * beffroi (3) and menu-vair (6); and 3 for potent, as in Fox-Davies' and Parker's figures.
135
+ * @param {number} fallback Tiles across when no `fur-<n>` is set.
136
+ * @returns {string} A CSS var() for the number of tiles across.
137
+ */
138
+ const across = (fallback) => `var(--fur-across, ${fallback})`;
35
139
  // A quarter of a tile up and left, so whole spots sit along the top and sides. A % position is a
36
- // share of (box - tile), which is (ACROSS - 1) tiles.
37
- const SHIFT = `${-100 / (4 * (ACROSS - 1))}%`;
38
- const FIELD = { size: `calc(100% / ${ACROSS}) auto`, position: `${SHIFT} ${SHIFT}` };
140
+ // share of (box - tile), which is (n - 1) tiles.
141
+ const SHIFT = `calc(-100% / (4 * (${across(6)} - 1)))`;
142
+ const FIELDS = {
143
+ ermine: { size: `calc(100% / ${across(6)}) auto`, position: `${SHIFT} ${SHIFT}` },
144
+ // A whole number of panes, from the top left.
145
+ vair: { size: `calc(100% / ${across(4)}) auto`, position: "0 0" },
146
+ potent: { size: `calc(100% / ${across(3)}) auto`, position: "0 0" },
147
+ };
148
+ /**
149
+ * @param {string} fur A key of FURS.
150
+ * @returns {{ size: string, position: string }} The CSS background-size and -position of its tile.
151
+ */
152
+ const field = (fur) => FIELDS[family(FUR_PATTERNS[fur])];
39
153
 
40
- // The layers under a fur in a divided field: the spots, then the ground.
154
+ // Ermine's spots on a bordure are 1.4 bordure widths across. At the default eighth, the side
155
+ // columns are then on the ring's centre lines (3.5 widths = 5 half-tiles), whatever the width.
156
+ const RING_ERMINE_TILE = 1.4;
157
+ // A bordure's panes are one row across its width, so the tile is as wide as the pane's share of it.
158
+ const RINGS = {
159
+ ermine: {
160
+ size: `calc(${RING_ERMINE_TILE} * var(--bordure-width)) auto`,
161
+ position: `center calc(var(--bordure-width) / 2)`,
162
+ },
163
+ vair: { size: `calc(var(--bordure-width) * ${PANE} / ${VAIR_ROW}) auto`, position: "center 0" },
164
+ potent: {
165
+ size: `calc(var(--bordure-width) * ${PANE} / ${POTENT_ROW}) auto`,
166
+ position: "center 0",
167
+ },
168
+ };
169
+
170
+ /**
171
+ * A fur on a frame's bordure ring, sized to the bordure's width, not to `fur-<n>`.
172
+ * @param {string} fur A key of FURS.
173
+ * @param {Function} theme Tailwind's theme() lookup.
174
+ * @returns {Record<string, string>} CSS background declarations for the ring's ::after.
175
+ */
176
+ export const ringBackground = (fur, theme) => {
177
+ const ring = RINGS[family(FUR_PATTERNS[fur])];
178
+ return {
179
+ "background-color": `var(--color-${FURS[fur][0]})`,
180
+ "background-image": tile(fur, theme),
181
+ "background-size": ring.size,
182
+ "background-position": ring.position,
183
+ "background-repeat": "repeat",
184
+ };
185
+ };
186
+
187
+ /**
188
+ * The layers under a fur in a divided field: the pattern, then the ground.
189
+ * @param {string} fur A key of FURS.
190
+ * @param {Function} theme Tailwind's theme() lookup.
191
+ * @returns {{ image: string, size: string, position: string, repeat: string }[]} Background
192
+ * layers, top first.
193
+ */
41
194
  export const furLayers = (fur, theme) => {
42
195
  const ground = `var(--color-${FURS[fur][0]})`;
43
196
  return [
44
- { image: spots(fur, theme), ...FIELD, repeat: "repeat" },
197
+ { image: tile(fur, theme), ...field(fur), repeat: "repeat" },
45
198
  {
46
199
  image: `linear-gradient(${ground}, ${ground})`,
47
200
  size: "100% 100%",
@@ -51,12 +204,17 @@ export const furLayers = (fur, theme) => {
51
204
  ];
52
205
  };
53
206
 
54
- // A fur on its own, as on the field or an ordinary.
207
+ /**
208
+ * A fur on its own, as on the field or an ordinary.
209
+ * @param {string} fur A key of FURS.
210
+ * @param {Function} theme Tailwind's theme() lookup.
211
+ * @returns {Record<string, string>} CSS background declarations.
212
+ */
55
213
  export const furBackground = (fur, theme) => ({
56
214
  "background-color": `var(--color-${FURS[fur][0]})`,
57
- "background-image": spots(fur, theme),
58
- "background-size": FIELD.size,
59
- "background-position": FIELD.position,
215
+ "background-image": tile(fur, theme),
216
+ "background-size": field(fur).size,
217
+ "background-position": field(fur).position,
60
218
  "background-repeat": "repeat",
61
219
  });
62
220
 
@@ -66,7 +224,7 @@ export default plugin(({ addUtilities, theme }) => {
66
224
  for (const fur of Object.keys(FURS)) {
67
225
  utilities[`.${fur}`] = {
68
226
  ...furBackground(fur, theme),
69
- // Two tiles across a charge, centred, so a whole spot sits in its middle.
227
+ // Two tiles across a charge, centred, so ermine has a whole spot in its middle.
70
228
  ".charges > &": {
71
229
  "background-size": "50% auto",
72
230
  "background-position": "center",
package/grailwind.css CHANGED
@@ -42,19 +42,73 @@
42
42
  syntax: "*";
43
43
  inherits: false;
44
44
  }
45
- /* A field's first tincture. The escutcheon copies it to --the-first, which inherits,
45
+ @property --field-image {
46
+ syntax: "*";
47
+ inherits: false;
48
+ }
49
+ @property --field-size {
50
+ syntax: "*";
51
+ inherits: false;
52
+ }
53
+ @property --field-position {
54
+ syntax: "*";
55
+ inherits: false;
56
+ }
57
+ @property --field-repeat {
58
+ syntax: "*";
59
+ inherits: false;
60
+ }
61
+ @property --on-counterchange-color {
62
+ syntax: "*";
63
+ inherits: false;
64
+ }
65
+ @property --on-counterchange-image {
66
+ syntax: "*";
67
+ inherits: false;
68
+ }
69
+ @property --on-counterchange-size {
70
+ syntax: "*";
71
+ inherits: false;
72
+ }
73
+ @property --on-counterchange-position {
74
+ syntax: "*";
75
+ inherits: false;
76
+ }
77
+ @property --on-counterchange-repeat {
78
+ syntax: "*";
79
+ inherits: false;
80
+ }
81
+ /* A field's first tincture. The heater's field copies it to --the-first, which inherits,
46
82
  * so a canton's own field can't change it. */
47
83
  @property --field-first {
48
84
  syntax: "*";
49
85
  inherits: false;
50
86
  }
51
- /* A field's own bordure width, 0 without one. The shield copies it to --outer-inset, which
87
+ /* The shield's first tincture, on the heater, for a bordure ring of the first. It inherits, so
88
+ * the ring on a frame's ::after sees it, an inescutcheon's included: of the first is the
89
+ * shield's. */
90
+ @property --frame-first {
91
+ syntax: "*";
92
+ inherits: true;
93
+ }
94
+ /* A field's own bordure width, 0 without one. A frame's field copies it to --outer-inset, which
52
95
  * inherits, so a canton can step out over the shield's bordure and not its own. */
53
96
  @property --bordure-inset {
54
97
  syntax: "<length>";
55
98
  inherits: false;
56
99
  initial-value: 0px;
57
100
  }
101
+ /* A part's bordure has none on its palar side: 0 there, and the chief steps out by it. Unset
102
+ * elsewhere, so the chief falls back to --bordure-inset, and not inherited, so a canton's own
103
+ * doesn't take its part's. */
104
+ @property --bordure-inset-dexter {
105
+ syntax: "*";
106
+ inherits: false;
107
+ }
108
+ @property --bordure-inset-sinister {
109
+ syntax: "*";
110
+ inherits: false;
111
+ }
58
112
 
59
113
  /* ---------------------------------------------------------------------------
60
114
  * Tinctures
@@ -81,30 +135,51 @@
81
135
  @utility or {
82
136
  background-color: var(--color-or);
83
137
  --field-first: var(--color-or);
138
+ .heater:has(> &) {
139
+ --frame-first: var(--color-or);
140
+ }
84
141
  }
85
142
  @utility argent {
86
143
  background-color: var(--color-argent);
87
144
  --field-first: var(--color-argent);
145
+ .heater:has(> &) {
146
+ --frame-first: var(--color-argent);
147
+ }
88
148
  }
89
149
  @utility gules {
90
150
  background-color: var(--color-gules);
91
151
  --field-first: var(--color-gules);
152
+ .heater:has(> &) {
153
+ --frame-first: var(--color-gules);
154
+ }
92
155
  }
93
156
  @utility azure {
94
157
  background-color: var(--color-azure);
95
158
  --field-first: var(--color-azure);
159
+ .heater:has(> &) {
160
+ --frame-first: var(--color-azure);
161
+ }
96
162
  }
97
163
  @utility vert {
98
164
  background-color: var(--color-vert);
99
165
  --field-first: var(--color-vert);
166
+ .heater:has(> &) {
167
+ --frame-first: var(--color-vert);
168
+ }
100
169
  }
101
170
  @utility purpure {
102
171
  background-color: var(--color-purpure);
103
172
  --field-first: var(--color-purpure);
173
+ .heater:has(> &) {
174
+ --frame-first: var(--color-purpure);
175
+ }
104
176
  }
105
177
  @utility sable {
106
178
  background-color: var(--color-sable);
107
179
  --field-first: var(--color-sable);
180
+ .heater:has(> &) {
181
+ --frame-first: var(--color-sable);
182
+ }
108
183
  }
109
184
  /* The field's first tincture, for charges and for a canton or inescutcheon field. */
110
185
  @utility of-the-first {
@@ -115,7 +190,9 @@
115
190
  * Field divisions
116
191
  * ------------------------------------------------------------------------- */
117
192
 
118
- /* Per pale: split vertically into dexter | sinister. */
193
+ /* Per pale: split vertically into dexter | sinister. A split field's parts are its children that
194
+ * aren't charges, an inescutcheon or a canton: those lie over all of it. The same selector is
195
+ * PART in ordinaries.js. */
119
196
  @utility per-pale {
120
197
  display: grid;
121
198
  grid-auto-flow: column;
@@ -123,9 +200,10 @@
123
200
 
124
201
  /* On a shield, each part is a coat of its own: what's in it is placed, stacked and
125
202
  * counterchanged within it. */
126
- &:is(.escutcheon, .escutcheon *) > * {
203
+ &:is(.heater, .heater *) > :not(.charges, .inescutcheon, .canton) {
127
204
  position: relative;
128
205
  contain: layout;
206
+ --layer-inset: 0px;
129
207
  }
130
208
  }
131
209
 
@@ -137,15 +215,20 @@
137
215
 
138
216
  /* On a shield, two parts split at the fess point, FESS_POINT in divisions.js.
139
217
  * More parts stay equal. */
140
- &:is(.escutcheon, .escutcheon *) {
141
- &:has(> :last-child:nth-child(2)) {
218
+ &:is(.heater, .heater *) {
219
+ &:has(
220
+ > :nth-child(2 of :not(.charges, .inescutcheon, .canton)):nth-last-child(
221
+ 1 of :not(.charges, .inescutcheon, .canton)
222
+ )
223
+ ) {
142
224
  grid-template-rows: 45fr 55fr;
143
225
  }
144
226
 
145
227
  /* See per-pale. */
146
- & > * {
228
+ & > :not(.charges, .inescutcheon, .canton) {
147
229
  position: relative;
148
230
  contain: layout;
231
+ --layer-inset: 0px;
149
232
  }
150
233
  }
151
234
  }
@@ -158,19 +241,20 @@
158
241
  grid-template-rows: 1fr 1fr;
159
242
 
160
243
  /* Each quarter is a coat of its own, so it starts its own count. */
161
- & > * {
244
+ & > :not(.charges, .inescutcheon, .canton) {
162
245
  --the-first: var(--field-first);
163
246
  --outer-inset: 0px;
164
247
  }
165
248
 
166
249
  /* On a shield, split at the fess point, as per-fess. */
167
- &:is(.escutcheon, .escutcheon *) {
250
+ &:is(.heater, .heater *) {
168
251
  grid-template-rows: 45fr 55fr;
169
252
 
170
253
  /* See per-pale. */
171
- & > * {
254
+ & > :not(.charges, .inescutcheon, .canton) {
172
255
  position: relative;
173
256
  contain: layout;
257
+ --layer-inset: 0px;
174
258
  }
175
259
  }
176
260
  }
@@ -179,37 +263,117 @@
179
263
  * Subordinaries
180
264
  * ------------------------------------------------------------------------- */
181
265
 
182
- /* Bordure: a border in a given tincture, e.g. `bordure-or`. */
266
+ /* Bordure, e.g. `bordure-or`: a border, so what's inside it lays out within it. On a frame's
267
+ * field the border is transparent, and a ring on the frame's ::after (see ordinaries.js)
268
+ * paints it, following the frame's outline. On a per-pale part the ring is on that half, and
269
+ * the part has no border on the palar side. A chief on the part goes under the ring. Anywhere
270
+ * else, such as on a quarter or a canton, the border is painted. */
183
271
  @utility bordure-* {
184
272
  border-style: solid;
185
273
  border-width: var(--bordure-width, 0.75rem);
186
274
  border-color: --value(--color-*);
187
275
  --bordure-inset: var(--bordure-width, 0.75rem);
276
+
277
+ :is(.heater, .inescutcheon) > & {
278
+ border-color: transparent;
279
+ }
280
+ :is(.heater, .inescutcheon):has(> &)::after {
281
+ content: "";
282
+ background-color: --value(--color-*);
283
+ }
284
+ :is(.heater, .inescutcheon)
285
+ > .per-pale
286
+ > &:nth-child(1 of :not(.charges, .inescutcheon, .canton)) {
287
+ border-color: transparent;
288
+ border-right-width: 0;
289
+ --bordure-inset-sinister: 0px;
290
+ }
291
+ :is(.heater, .inescutcheon):has(
292
+ > .per-pale > &:nth-child(1 of :not(.charges, .inescutcheon, .canton))
293
+ )::after {
294
+ content: "";
295
+ --ring-dexter: --value(--color-*);
296
+ }
297
+ :is(.heater, .inescutcheon)
298
+ > .per-pale
299
+ > &:nth-last-child(1 of :not(.charges, .inescutcheon, .canton)) {
300
+ border-color: transparent;
301
+ border-left-width: 0;
302
+ --bordure-inset-dexter: 0px;
303
+ }
304
+ :is(.heater, .inescutcheon):has(
305
+ > .per-pale
306
+ > &:nth-last-child(1 of :not(.charges, .inescutcheon, .canton))
307
+ )::after {
308
+ content: "";
309
+ --ring-sinister: --value(--color-*);
310
+ }
188
311
  }
189
312
  @utility bordure-of-the-first {
190
313
  border-style: solid;
191
314
  border-width: var(--bordure-width, 0.75rem);
192
315
  border-color: var(--the-first, transparent);
193
316
  --bordure-inset: var(--bordure-width, 0.75rem);
317
+
318
+ :is(.heater, .inescutcheon) > & {
319
+ border-color: transparent;
320
+ }
321
+ :is(.heater, .inescutcheon):has(> &)::after {
322
+ content: "";
323
+ background-color: var(--frame-first, transparent);
324
+ }
325
+ :is(.heater, .inescutcheon)
326
+ > .per-pale
327
+ > &:nth-child(1 of :not(.charges, .inescutcheon, .canton)) {
328
+ border-color: transparent;
329
+ border-right-width: 0;
330
+ --bordure-inset-sinister: 0px;
331
+ }
332
+ :is(.heater, .inescutcheon):has(
333
+ > .per-pale > &:nth-child(1 of :not(.charges, .inescutcheon, .canton))
334
+ )::after {
335
+ content: "";
336
+ --ring-dexter: var(--frame-first, transparent);
337
+ }
338
+ :is(.heater, .inescutcheon)
339
+ > .per-pale
340
+ > &:nth-last-child(1 of :not(.charges, .inescutcheon, .canton)) {
341
+ border-color: transparent;
342
+ border-left-width: 0;
343
+ --bordure-inset-dexter: 0px;
344
+ }
345
+ :is(.heater, .inescutcheon):has(
346
+ > .per-pale
347
+ > &:nth-last-child(1 of :not(.charges, .inescutcheon, .canton))
348
+ )::after {
349
+ content: "";
350
+ --ring-sinister: var(--frame-first, transparent);
351
+ }
194
352
  }
195
353
 
196
- /* Inescutcheon: a shield a third as wide, centred on the fess point (50%, 45%),
197
- * over everything else on the shield. As both are 5:6, it's a third as tall too. */
354
+ /* Inescutcheon: a heater a third as wide round its own field, centred on the fess point
355
+ * (50%, 45%), over everything else on the shield. As both are 5:6, it's a third as tall too.
356
+ * Its field takes the shield's first, as "of the first" means the shield's. */
198
357
  @utility inescutcheon {
199
358
  position: absolute;
200
- z-index: 2;
359
+ z-index: 5;
201
360
  width: 33%;
202
361
  left: 33.5%;
203
362
  top: 28.5%;
363
+ display: grid;
204
364
  aspect-ratio: 5 / 6;
205
365
  overflow: clip;
206
- overflow-clip-margin: border-box;
207
- border-radius: 0 0 50% 50% / 0 0 45% 45%;
208
- box-shadow: 0 0 0 2px var(--color-sable);
209
366
  contain: layout;
210
- /* In proportion with the shield's own bordure. */
211
- --bordure-width: 0.25rem;
212
- --outer-inset: var(--bordure-inset);
367
+ container-type: inline-size;
368
+ /* An eighth of its own width, like the shield's. */
369
+ --bordure-width: calc(100cqw / 8);
370
+
371
+ /* See heater. */
372
+ & > * {
373
+ position: relative;
374
+ --outer-inset: var(--bordure-inset);
375
+ --layer-inset: var(--bordure-inset);
376
+ }
213
377
  }
214
378
 
215
379
  /* Canton: a third of the shield each way in dexter chief, so a ninth of it, and
@@ -217,7 +381,7 @@
217
381
  * ordinary and charge, as a later addition to the arms, and over the bordure. */
218
382
  @utility canton {
219
383
  position: absolute;
220
- z-index: 2;
384
+ z-index: 5;
221
385
  width: calc((100% + 2 * var(--outer-inset, 0px)) / 3);
222
386
  left: calc(-1 * var(--outer-inset, 0px));
223
387
  top: calc(-1 * var(--outer-inset, 0px));
@@ -226,26 +390,46 @@
226
390
  overflow: clip;
227
391
  overflow-clip-margin: border-box;
228
392
  contain: layout;
229
- --bordure-width: 0.25rem;
393
+ --layer-inset: 0px;
394
+ /* A third of the shield's, as cqw is the shield's width: an eighth of the canton's own. */
395
+ --bordure-width: calc(100cqw / 24);
230
396
  }
231
397
 
232
398
  /* ---------------------------------------------------------------------------
233
399
  * The shield itself
234
400
  * ------------------------------------------------------------------------- */
235
401
 
236
- /* Escutcheon: a shield shape to draw arms in. */
237
- @utility escutcheon {
402
+ /* Heater: a pointed shield round its one child, the field, which carries the blazon. The
403
+ * outline and the clip are in ordinaries.js, from drawings/heater.svg. */
404
+ @utility heater {
238
405
  position: relative;
406
+ display: grid;
239
407
  aspect-ratio: 5 / 6;
240
- /* At the border's edge, so a canton or chief can surmount the bordure. */
241
408
  overflow: clip;
242
- overflow-clip-margin: border-box;
243
- border-radius: 0 0 50% 50% / 0 0 45% 45%;
244
- box-shadow: 0 0 0 2px var(--color-sable);
245
409
  /* The containing block for counterchanged charges' fixed layers. */
246
410
  contain: layout;
247
- --the-first: var(--field-first);
248
- --outer-inset: var(--bordure-inset);
411
+ container-type: inline-size;
412
+ /* An eighth of the shield's width (BORDURE_WIDTH in ordinaries.js). */
413
+ --bordure-width: calc(100cqw / 8);
414
+
415
+ /* The field. A canton steps out over its bordure, and a counterchanged charge's fixed layer
416
+ * in by it, to the field's padding box. */
417
+ & > * {
418
+ position: relative;
419
+ --the-first: var(--field-first);
420
+ --outer-inset: var(--bordure-inset);
421
+ --layer-inset: var(--bordure-inset);
422
+ }
423
+ }
424
+
425
+ /* ---------------------------------------------------------------------------
426
+ * Fur density
427
+ * ------------------------------------------------------------------------- */
428
+
429
+ /* Whole fur tiles across the field, e.g. `fur-3` for beffroi or `fur-6` for menu-vair. Ermine
430
+ * defaults to 6 and vair to 4. */
431
+ @utility fur-* {
432
+ --fur-across: --value(integer);
249
433
  }
250
434
 
251
435
  /* ---------------------------------------------------------------------------