grailwind 0.8.1 → 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
@@ -78,19 +78,37 @@
78
78
  syntax: "*";
79
79
  inherits: false;
80
80
  }
81
- /* A field's first tincture. The escutcheon copies it to --the-first, which inherits,
81
+ /* A field's first tincture. The heater's field copies it to --the-first, which inherits,
82
82
  * so a canton's own field can't change it. */
83
83
  @property --field-first {
84
84
  syntax: "*";
85
85
  inherits: false;
86
86
  }
87
- /* 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
88
95
  * inherits, so a canton can step out over the shield's bordure and not its own. */
89
96
  @property --bordure-inset {
90
97
  syntax: "<length>";
91
98
  inherits: false;
92
99
  initial-value: 0px;
93
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
+ }
94
112
 
95
113
  /* ---------------------------------------------------------------------------
96
114
  * Tinctures
@@ -117,30 +135,51 @@
117
135
  @utility or {
118
136
  background-color: var(--color-or);
119
137
  --field-first: var(--color-or);
138
+ .heater:has(> &) {
139
+ --frame-first: var(--color-or);
140
+ }
120
141
  }
121
142
  @utility argent {
122
143
  background-color: var(--color-argent);
123
144
  --field-first: var(--color-argent);
145
+ .heater:has(> &) {
146
+ --frame-first: var(--color-argent);
147
+ }
124
148
  }
125
149
  @utility gules {
126
150
  background-color: var(--color-gules);
127
151
  --field-first: var(--color-gules);
152
+ .heater:has(> &) {
153
+ --frame-first: var(--color-gules);
154
+ }
128
155
  }
129
156
  @utility azure {
130
157
  background-color: var(--color-azure);
131
158
  --field-first: var(--color-azure);
159
+ .heater:has(> &) {
160
+ --frame-first: var(--color-azure);
161
+ }
132
162
  }
133
163
  @utility vert {
134
164
  background-color: var(--color-vert);
135
165
  --field-first: var(--color-vert);
166
+ .heater:has(> &) {
167
+ --frame-first: var(--color-vert);
168
+ }
136
169
  }
137
170
  @utility purpure {
138
171
  background-color: var(--color-purpure);
139
172
  --field-first: var(--color-purpure);
173
+ .heater:has(> &) {
174
+ --frame-first: var(--color-purpure);
175
+ }
140
176
  }
141
177
  @utility sable {
142
178
  background-color: var(--color-sable);
143
179
  --field-first: var(--color-sable);
180
+ .heater:has(> &) {
181
+ --frame-first: var(--color-sable);
182
+ }
144
183
  }
145
184
  /* The field's first tincture, for charges and for a canton or inescutcheon field. */
146
185
  @utility of-the-first {
@@ -151,7 +190,9 @@
151
190
  * Field divisions
152
191
  * ------------------------------------------------------------------------- */
153
192
 
154
- /* 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. */
155
196
  @utility per-pale {
156
197
  display: grid;
157
198
  grid-auto-flow: column;
@@ -159,9 +200,10 @@
159
200
 
160
201
  /* On a shield, each part is a coat of its own: what's in it is placed, stacked and
161
202
  * counterchanged within it. */
162
- &:is(.escutcheon, .escutcheon *) > * {
203
+ &:is(.heater, .heater *) > :not(.charges, .inescutcheon, .canton) {
163
204
  position: relative;
164
205
  contain: layout;
206
+ --layer-inset: 0px;
165
207
  }
166
208
  }
167
209
 
@@ -173,15 +215,20 @@
173
215
 
174
216
  /* On a shield, two parts split at the fess point, FESS_POINT in divisions.js.
175
217
  * More parts stay equal. */
176
- &:is(.escutcheon, .escutcheon *) {
177
- &: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
+ ) {
178
224
  grid-template-rows: 45fr 55fr;
179
225
  }
180
226
 
181
227
  /* See per-pale. */
182
- & > * {
228
+ & > :not(.charges, .inescutcheon, .canton) {
183
229
  position: relative;
184
230
  contain: layout;
231
+ --layer-inset: 0px;
185
232
  }
186
233
  }
187
234
  }
@@ -194,19 +241,20 @@
194
241
  grid-template-rows: 1fr 1fr;
195
242
 
196
243
  /* Each quarter is a coat of its own, so it starts its own count. */
197
- & > * {
244
+ & > :not(.charges, .inescutcheon, .canton) {
198
245
  --the-first: var(--field-first);
199
246
  --outer-inset: 0px;
200
247
  }
201
248
 
202
249
  /* On a shield, split at the fess point, as per-fess. */
203
- &:is(.escutcheon, .escutcheon *) {
250
+ &:is(.heater, .heater *) {
204
251
  grid-template-rows: 45fr 55fr;
205
252
 
206
253
  /* See per-pale. */
207
- & > * {
254
+ & > :not(.charges, .inescutcheon, .canton) {
208
255
  position: relative;
209
256
  contain: layout;
257
+ --layer-inset: 0px;
210
258
  }
211
259
  }
212
260
  }
@@ -215,37 +263,117 @@
215
263
  * Subordinaries
216
264
  * ------------------------------------------------------------------------- */
217
265
 
218
- /* 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. */
219
271
  @utility bordure-* {
220
272
  border-style: solid;
221
273
  border-width: var(--bordure-width, 0.75rem);
222
274
  border-color: --value(--color-*);
223
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
+ }
224
311
  }
225
312
  @utility bordure-of-the-first {
226
313
  border-style: solid;
227
314
  border-width: var(--bordure-width, 0.75rem);
228
315
  border-color: var(--the-first, transparent);
229
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
+ }
230
352
  }
231
353
 
232
- /* Inescutcheon: a shield a third as wide, centred on the fess point (50%, 45%),
233
- * 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. */
234
357
  @utility inescutcheon {
235
358
  position: absolute;
236
- z-index: 2;
359
+ z-index: 5;
237
360
  width: 33%;
238
361
  left: 33.5%;
239
362
  top: 28.5%;
363
+ display: grid;
240
364
  aspect-ratio: 5 / 6;
241
365
  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
366
  contain: layout;
246
- /* In proportion with the shield's own bordure. */
247
- --bordure-width: 0.25rem;
248
- --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
+ }
249
377
  }
250
378
 
251
379
  /* Canton: a third of the shield each way in dexter chief, so a ninth of it, and
@@ -253,7 +381,7 @@
253
381
  * ordinary and charge, as a later addition to the arms, and over the bordure. */
254
382
  @utility canton {
255
383
  position: absolute;
256
- z-index: 2;
384
+ z-index: 5;
257
385
  width: calc((100% + 2 * var(--outer-inset, 0px)) / 3);
258
386
  left: calc(-1 * var(--outer-inset, 0px));
259
387
  top: calc(-1 * var(--outer-inset, 0px));
@@ -262,26 +390,46 @@
262
390
  overflow: clip;
263
391
  overflow-clip-margin: border-box;
264
392
  contain: layout;
265
- --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);
266
396
  }
267
397
 
268
398
  /* ---------------------------------------------------------------------------
269
399
  * The shield itself
270
400
  * ------------------------------------------------------------------------- */
271
401
 
272
- /* Escutcheon: a shield shape to draw arms in. */
273
- @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 {
274
405
  position: relative;
406
+ display: grid;
275
407
  aspect-ratio: 5 / 6;
276
- /* At the border's edge, so a canton or chief can surmount the bordure. */
277
408
  overflow: clip;
278
- overflow-clip-margin: border-box;
279
- border-radius: 0 0 50% 50% / 0 0 45% 45%;
280
- box-shadow: 0 0 0 2px var(--color-sable);
281
409
  /* The containing block for counterchanged charges' fixed layers. */
282
410
  contain: layout;
283
- --the-first: var(--field-first);
284
- --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);
285
433
  }
286
434
 
287
435
  /* ---------------------------------------------------------------------------