@domandigital/craft 0.18.1 → 0.19.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.
@@ -237,7 +237,7 @@ function fontClass(family) {
237
237
  }
238
238
 
239
239
  // src/fingerprint/index.ts
240
- var FINGERPRINT_VERSION = 2;
240
+ var FINGERPRINT_VERSION = 3;
241
241
  var CHROMATIC = 0.03;
242
242
  var MIN_ACCENT_AREA = 2e4;
243
243
  var oklch = (value) => {
@@ -273,6 +273,24 @@ function median(values) {
273
273
  const mid = Math.floor(sorted.length / 2);
274
274
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
275
275
  }
276
+ var FULL_WIDTH_IMAGE = 0.8;
277
+ function heroOf(s) {
278
+ const h1 = s.headings.find((h) => h.level === 1);
279
+ const inside = (top, x) => top >= x.top && top < x.top + x.height;
280
+ const index = (() => {
281
+ const withH1 = h1 ? s.sections.findIndex((x) => x.kind === "hero" && inside(h1.top, x)) : -1;
282
+ return withH1 >= 0 ? withH1 : s.sections.findIndex((x) => x.kind === "hero");
283
+ })();
284
+ if (index < 0) return void 0;
285
+ const section = s.sections[index];
286
+ const headline = h1 && inside(h1.top, section) ? { sizePx: h1.sizePx, weight: h1.weight, class: fontClass(normaliseFamily(h1.family)), italic: h1.italic || h1.italicPart } : null;
287
+ if (!s.images) return { headline };
288
+ const picture2 = s.images.filter((i) => (i.role === "photo" || i.role === "illustration") && (i.section === index || i.section === null && inside(i.top, section))).sort((a, b) => b.width * b.height - a.width * a.height)[0];
289
+ if (!picture2) return { headline, image: "none" };
290
+ if (picture2.left === void 0) return { headline };
291
+ const image = picture2.width >= FULL_WIDTH_IMAGE * s.viewport.width ? "full" : picture2.left + picture2.width / 2 < s.viewport.width / 2 ? "left" : "right";
292
+ return { headline, image };
293
+ }
276
294
  function fingerprint(s) {
277
295
  const effects = [];
278
296
  if (s.effects.glass > 0) effects.push("glass-panel");
@@ -284,6 +302,7 @@ function fingerprint(s) {
284
302
  if (s.effects.eyebrowChip) effects.push("hero-eyebrow-chip");
285
303
  if (s.motion.introOverlay) effects.push("intro-cinematic");
286
304
  const below = s.sections.filter((x) => x.top > s.viewport.height).length;
305
+ const hero = heroOf(s);
287
306
  return {
288
307
  version: FINGERPRINT_VERSION,
289
308
  accent: accentOf(s),
@@ -307,10 +326,28 @@ function fingerprint(s) {
307
326
  carousel: x.carousel ?? false,
308
327
  figures: x.figures ?? 0
309
328
  }))
310
- } : {}
329
+ } : {},
330
+ ...hero ? { hero } : {}
311
331
  };
312
332
  }
313
- var DISTANCE_WEIGHTS = { accent: 0.25, type: 0.25, ground: 0.1, shape: 0.1, motion: 0.1, effects: 0.1, layout: 0.1 };
333
+ var WARM_HUE = [
334
+ [330, 360],
335
+ [0, 130]
336
+ ];
337
+ var COOL_HUE = [[160, 310]];
338
+ var TEMPERATURE_FULL = 6e-3;
339
+ var TEMPERATURE_NAMED = 0.4;
340
+ function groundTemperature(o) {
341
+ const h = (o.h % 360 + 360) % 360;
342
+ const within = (bands) => bands.some(([lo, hi]) => h >= lo && h < hi);
343
+ const strength = Math.min(1, o.c / TEMPERATURE_FULL);
344
+ return within(WARM_HUE) ? strength : within(COOL_HUE) ? -strength : 0;
345
+ }
346
+ function temperatureName(o) {
347
+ const t = groundTemperature(o);
348
+ return t >= TEMPERATURE_NAMED ? "warm" : t <= -TEMPERATURE_NAMED ? "cool" : "neutral";
349
+ }
350
+ var DISTANCE_WEIGHTS = { accent: 0.2, type: 0.2, ground: 0.1, shape: 0.1, motion: 0.05, effects: 0.1, layout: 0.1, rhythm: 0.1, hero: 0.05 };
314
351
  function levenshtein(a, b, cost = (x, y) => x === y ? 0 : 1) {
315
352
  const row = Array.from({ length: b.length + 1 }, (_, i) => i);
316
353
  for (let i = 1; i <= a.length; i += 1) {
@@ -337,18 +374,62 @@ function layoutDistance(a, b) {
337
374
  return longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
338
375
  }
339
376
  var clamp = (n) => Math.max(0, Math.min(1, n));
377
+ function bandOf(l) {
378
+ return l < 0.4 ? "dark" : l >= 0.75 ? "light" : "mid";
379
+ }
380
+ function rhythmOf(fp) {
381
+ if (!fp.sections) return null;
382
+ const out = [];
383
+ for (const s of fp.sections) {
384
+ if (s.background === void 0) return null;
385
+ const band = bandOf((s.background ?? fp.ground).l);
386
+ if (out[out.length - 1] !== band) out.push(band);
387
+ }
388
+ return out;
389
+ }
390
+ var OPENING_BANDS = 3;
391
+ function rhythmDistance(a, b) {
392
+ const ra = rhythmOf(a);
393
+ const rb = rhythmOf(b);
394
+ if (!ra || !rb) return null;
395
+ const over = (x, y) => {
396
+ const longest = Math.max(x.length, y.length);
397
+ return longest === 0 ? 0 : levenshtein(x, y) / longest;
398
+ };
399
+ return (over(ra.slice(0, OPENING_BANDS), rb.slice(0, OPENING_BANDS)) + over(ra, rb)) / 2;
400
+ }
401
+ var HEADLINE_OCTAVES = 1;
402
+ function heroSection(fp) {
403
+ return fp.sections?.find((s) => s.role === "hero");
404
+ }
405
+ function heroDistance(a, b) {
406
+ const sa = heroSection(a);
407
+ const sb = heroSection(b);
408
+ if (!sa || !sb) return null;
409
+ const parts2 = [Math.abs(sa.centred - sb.centred), clamp(Math.abs(sa.width - sb.width) / 0.5)];
410
+ const ha = a.hero?.headline;
411
+ const hb = b.hero?.headline;
412
+ if (ha && hb) parts2.push(0.5 * (ha.class === hb.class ? 0 : 1) + 0.5 * clamp(Math.abs(Math.log2(ha.sizePx / hb.sizePx)) / HEADLINE_OCTAVES));
413
+ else if (a.hero && b.hero && (ha || hb)) parts2.push(1);
414
+ if (a.hero?.image && b.hero?.image) parts2.push(a.hero.image === b.hero.image ? 0 : 1);
415
+ return parts2.reduce((s, p) => s + p, 0) / parts2.length;
416
+ }
340
417
  function faceDistance(a, b) {
341
418
  if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
342
419
  return a.class === b.class ? 0.5 : 1;
343
420
  }
344
421
  var ACCENT_SCALE = 0.3;
345
422
  var GROUND_SCALE = 0.15;
423
+ var TEMPERATURE_WEIGHT = 0.6;
346
424
  var SHARED_PART = 0.2;
347
425
  var ACCENT_SHARED = ACCENT_SCALE * SHARED_PART;
348
426
  var GROUND_SHARED = GROUND_SCALE * SHARED_PART;
427
+ function groundDistance(a, b) {
428
+ return clamp(deltaEOk(a, b) / GROUND_SCALE + TEMPERATURE_WEIGHT * Math.abs(groundTemperature(a) - groundTemperature(b)) / 2);
429
+ }
349
430
  function fingerprintDistance(a, b) {
350
431
  const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) / ACCENT_SCALE) : a.accent || b.accent ? 1 : 0;
351
- const ground = clamp(deltaEOk(a.ground, b.ground) / GROUND_SCALE);
432
+ const ground = groundDistance(a.ground, b.ground);
352
433
  const type = 0.6 * faceDistance(a.display, b.display) + 0.4 * faceDistance(a.body, b.body);
353
434
  const shape = a.roundness === null || b.roundness === null ? a.roundness === b.roundness ? 0 : 0.5 : clamp(Math.abs(a.roundness - b.roundness) / 0.5);
354
435
  const motion = clamp(Math.abs(a.motion - b.motion));
@@ -356,9 +437,18 @@ function fingerprintDistance(a, b) {
356
437
  const shared = a.effects.filter((e) => b.effects.includes(e)).length;
357
438
  const effects = union.size === 0 ? 0 : 1 - shared / union.size;
358
439
  const layout = layoutDistance(a, b);
359
- const parts2 = { accent, ground, type, shape, motion, effects, layout };
360
- const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts2[k], 0);
361
- return { total: Math.round(total * 1e3) / 1e3, parts: parts2 };
440
+ const rhythm = rhythmDistance(a, b);
441
+ const hero = heroDistance(a, b);
442
+ const parts2 = { accent, ground, type, shape, motion, effects, layout, rhythm, hero };
443
+ let sum = 0;
444
+ let weight = 0;
445
+ for (const k of Object.keys(DISTANCE_WEIGHTS)) {
446
+ const p = parts2[k];
447
+ if (p === null) continue;
448
+ sum += DISTANCE_WEIGHTS[k] * p;
449
+ weight += DISTANCE_WEIGHTS[k];
450
+ }
451
+ return { total: Math.round(sum / weight * 1e3) / 1e3, parts: parts2 };
362
452
  }
363
453
 
364
454
  // src/character/tells/source.ts
@@ -1323,6 +1413,12 @@ function groundName(o) {
1323
1413
  if (o.l >= 0.97 && o.c < 0.012) return "white";
1324
1414
  return o.c < 0.02 ? "grey" : "tinted";
1325
1415
  }
1416
+ function groundLabel(o) {
1417
+ const name = groundName(o);
1418
+ if (name === "cream" || name === "tinted") return name;
1419
+ const temperature = temperatureName(o);
1420
+ return temperature === "neutral" ? name : `${temperature} ${name}`;
1421
+ }
1326
1422
  function shapeName(roundness) {
1327
1423
  if (roundness === null) return "no buttons";
1328
1424
  if (roundness >= 0.45) return "pill";
@@ -3057,7 +3153,9 @@ export {
3057
3153
  holdHouseBlocks,
3058
3154
  normaliseFamily,
3059
3155
  fingerprint,
3156
+ temperatureName,
3060
3157
  levenshtein,
3158
+ rhythmOf,
3061
3159
  SHARED_PART,
3062
3160
  ACCENT_SHARED,
3063
3161
  GROUND_SHARED,
@@ -3068,7 +3166,7 @@ export {
3068
3166
  MIN_RUNS,
3069
3167
  typicality,
3070
3168
  hueName,
3071
- groundName,
3169
+ groundLabel,
3072
3170
  shapeName,
3073
3171
  harvest,
3074
3172
  hueSwatch,
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  snapshotUrl,
3
3
  snapshotUrls
4
- } from "./chunk-SL5VZJ2J.js";
4
+ } from "./chunk-W7PMRSDG.js";
5
5
  import {
6
6
  describeTypicality,
7
7
  loadNull
8
- } from "./chunk-ZUYERAFM.js";
8
+ } from "./chunk-NCBCZFDY.js";
9
9
  import {
10
10
  SOURCE_FILE,
11
11
  finish,
@@ -14,7 +14,7 @@ import {
14
14
  parseFlags,
15
15
  prepareReport,
16
16
  readPaths
17
- } from "./chunk-O5B5ELJH.js";
17
+ } from "./chunk-GNVZGG3E.js";
18
18
  import {
19
19
  toJson
20
20
  } from "./chunk-QIEDBL4J.js";
@@ -25,9 +25,10 @@ import {
25
25
  auditSnapshot,
26
26
  componentTypicalities,
27
27
  fingerprint,
28
+ rhythmOf,
28
29
  scanSource,
29
30
  typicality
30
- } from "./chunk-WFB6F3BN.js";
31
+ } from "./chunk-IHT6CJOM.js";
31
32
 
32
33
  // src/audit/cli.ts
33
34
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -69,9 +70,17 @@ function describeFingerprint(fp) {
69
70
  ` roundness ${fp.roundness === null ? "no buttons" : fp.roundness.toFixed(2)}`,
70
71
  ` motion ${Math.round(fp.motion * 100)}% of sections below the fold reveal on scroll`,
71
72
  ` effects ${fp.effects.join(", ") || "none"}`,
72
- ` layout ${fp.layout.join(" > ") || "none"}`
73
+ ` layout ${fp.layout.join(" > ") || "none"}`,
74
+ ` rhythm ${rhythmOf(fp)?.join(" > ") ?? "not measured: sections carry no backgrounds before snapshot version 2"}`,
75
+ ` hero ${describeHero(fp)}`
73
76
  ].join("\n");
74
77
  }
78
+ function describeHero(fp) {
79
+ if (!fp.hero) return "none";
80
+ const section = fp.sections?.find((s) => s.role === "hero");
81
+ const parts = [section ? section.centred >= 0.5 ? "centred" : "left-aligned" : null, fp.hero.headline ? `${fp.hero.headline.class} headline at ${Math.round(fp.hero.headline.sizePx)}px` : "no headline", fp.hero.image ? fp.hero.image === "none" ? "no picture" : fp.hero.image === "full" ? "full-width picture" : `picture ${fp.hero.image}` : null];
82
+ return parts.filter((p) => p !== null).join(", ");
83
+ }
75
84
  function merge(a, b) {
76
85
  const byTell = { ...a.summary.byTell };
77
86
  for (const [k, v] of Object.entries(b.summary.byTell)) byTell[k] = (byTell[k] ?? 0) + v;
@@ -126,18 +135,20 @@ async function runAudit(command, args, io) {
126
135
  if (flags.pages && target) throw new Error("give a page or --pages, not both");
127
136
  if (!target && !flags.pages) throw new Error(`usage: craft ${command} <url${command === "audit" ? " | snapshot.json> | --pages <sitemap.xml | urls.txt>" : ">"}`);
128
137
  const viewports = viewportsOf(flags);
138
+ if (flags.scheme && flags.scheme !== "light" && flags.scheme !== "dark") throw new Error("--scheme takes light or dark");
139
+ const colorScheme = flags.scheme;
129
140
  const audited = [];
130
141
  const failed = [];
131
142
  const local = target ? resolve(io.cwd, target) : "";
132
143
  if (target && command === "audit" && /\.json$/i.test(target) && existsSync(local)) {
133
144
  audited.push({ url: target, viewport: null, snapshot: readSnapshotFile(local) });
134
145
  } else if (target && viewports.length === 1) {
135
- const snapshot = await snapshotUrl(target, { viewport: viewports[0] });
146
+ const snapshot = await snapshotUrl(target, { viewport: viewports[0], colorScheme });
136
147
  audited.push({ url: target, viewport: viewports[0] ?? null, snapshot });
137
148
  } else {
138
149
  const urls = flags.pages ? await resolvePages(flags.pages, io.cwd) : [target];
139
150
  for (const viewport of viewports) {
140
- for (const r of await snapshotUrls(urls, { viewport })) {
151
+ for (const r of await snapshotUrls(urls, { viewport, colorScheme })) {
141
152
  if (r.snapshot) audited.push({ url: r.url, viewport: viewport ?? null, snapshot: r.snapshot });
142
153
  else failed.push({ url: r.url, viewport: viewport ?? null, error: r.error ?? "no snapshot" });
143
154
  }
@@ -4,13 +4,13 @@ import {
4
4
  formatDirectionMatches,
5
5
  summariseDirection,
6
6
  validateDirection
7
- } from "./chunk-6TSDYETQ.js";
7
+ } from "./chunk-RNHV2SXC.js";
8
8
  import {
9
9
  loadNull
10
- } from "./chunk-ZUYERAFM.js";
10
+ } from "./chunk-NCBCZFDY.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-O5B5ELJH.js";
13
+ } from "./chunk-GNVZGG3E.js";
14
14
  import {
15
15
  toJson
16
16
  } from "./chunk-QIEDBL4J.js";
@@ -25,10 +25,12 @@ import {
25
25
  estateComponentPairs,
26
26
  fingerprint,
27
27
  fingerprintDistance,
28
- groundName,
28
+ groundLabel,
29
29
  hueName,
30
- shapeName
31
- } from "./chunk-WFB6F3BN.js";
30
+ rhythmOf,
31
+ shapeName,
32
+ temperatureName
33
+ } from "./chunk-IHT6CJOM.js";
32
34
 
33
35
  // src/estate/cli.ts
34
36
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -45,19 +47,45 @@ function addSite(register, site) {
45
47
  const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
46
48
  return { version: ESTATE_VERSION, sites };
47
49
  }
50
+ function sharedGround(a, b) {
51
+ const [x, y] = [groundLabel(a.ground), groundLabel(b.ground)];
52
+ if (x === y) return `${x} ground`;
53
+ const temperature = temperatureName(a.ground);
54
+ if (temperature !== "neutral" && temperature === temperatureName(b.ground)) return `${temperature} ground`;
55
+ return `${a.ground.l < 0.3 ? "dark" : "light"} ground`;
56
+ }
57
+ function sharedRhythm(a, b) {
58
+ const bands = rhythmOf(a) ?? [];
59
+ const word = (band) => band;
60
+ if (bands.length <= 1) return `${bands[0] ? word(bands[0]) : "light"} page throughout`;
61
+ const second = bands[1] === "mid" ? "mid-tone band" : `${word(bands[1])} band`;
62
+ return `${word(bands[0])} hero then ${second}`;
63
+ }
64
+ function sharedHero(a, b) {
65
+ const hero = a.sections?.find((s) => s.role === "hero");
66
+ const placement = hero && hero.centred >= 0.5 ? "centred" : "left-aligned";
67
+ const headline = a.hero?.headline && b.hero?.headline && a.hero.headline.class === b.hero.headline.class ? `${a.hero.headline.class} ` : "";
68
+ const image = a.hero?.image && a.hero.image === b.hero?.image && a.hero.image !== "none" ? `, ${a.hero.image === "full" ? "full-width picture" : `photo ${a.hero.image}`}` : "";
69
+ return `${placement} ${headline}hero${image}`;
70
+ }
48
71
  function sharedParts(a, b, d = fingerprintDistance(a, b)) {
49
72
  const out = [];
50
- const close = (k) => d.parts[k] <= SHARED_PART;
73
+ const close = (k) => {
74
+ const p = d.parts[k];
75
+ return p !== null && p <= SHARED_PART;
76
+ };
51
77
  if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
52
78
  if (close("type")) {
53
79
  if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
54
80
  if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
55
81
  }
56
- if (close("ground")) out.push(`${groundName(a.ground)} ground`);
82
+ if (close("ground")) out.push(sharedGround(a, b));
57
83
  if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
58
84
  if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
59
85
  if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
60
- if (close("layout")) out.push("running order");
86
+ if (close("hero")) out.push(sharedHero(a, b));
87
+ if (close("rhythm")) out.push(sharedRhythm(a, b));
88
+ if (close("layout")) out.push("same running order");
61
89
  return out;
62
90
  }
63
91
  function compareToEstate(target, register, options = {}) {
@@ -105,7 +133,7 @@ async function snapshotOf(target, cwd) {
105
133
  return snap;
106
134
  }
107
135
  if (!/^https?:\/\//.test(target)) throw new Error(`${target} is neither a URL, a snapshot file, nor a site in the register`);
108
- const { snapshotUrl } = await import("./audit-4SFSLCKY.js");
136
+ const { snapshotUrl } = await import("./audit-X7LGATAQ.js");
109
137
  return snapshotUrl(target);
110
138
  }
111
139
  var line = (distance, sibling, shared) => `${distance.toFixed(2)}${sibling ? " SIBLING" : ""}${shared.length ? ` shares ${shared.join(", ")}` : ""}`;
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-OQXTQQAU.js";
4
4
  import {
5
5
  parseFlags
6
- } from "./chunk-O5B5ELJH.js";
6
+ } from "./chunk-GNVZGG3E.js";
7
7
  import {
8
8
  toJson
9
9
  } from "./chunk-QIEDBL4J.js";
@@ -22,7 +22,7 @@ import {
22
22
  hueSwatch,
23
23
  nullPrompt,
24
24
  scanSource
25
- } from "./chunk-WFB6F3BN.js";
25
+ } from "./chunk-IHT6CJOM.js";
26
26
  import {
27
27
  extractStrings
28
28
  } from "./chunk-4QWEAL4G.js";
@@ -120,7 +120,7 @@ async function build(flags, io) {
120
120
  const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
121
121
  if (toShoot.length) {
122
122
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
123
- const { snapshotUrls } = await import("./audit-4SFSLCKY.js");
123
+ const { snapshotUrls } = await import("./audit-X7LGATAQ.js");
124
124
  const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
125
125
  results.forEach((r, i) => {
126
126
  const id = toShoot[i];
@@ -241,7 +241,7 @@ async function importPages(flags, io) {
241
241
  const toShoot = pages.filter((p) => !existsSync(join(shots, `${p.id}.json`)));
242
242
  if (toShoot.length) {
243
243
  io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"} from ${flags.builder}`);
244
- const { snapshotUrls } = await import("./audit-4SFSLCKY.js");
244
+ const { snapshotUrls } = await import("./audit-X7LGATAQ.js");
245
245
  const servers = /* @__PURE__ */ new Map();
246
246
  try {
247
247
  for (const p of toShoot) if (!servers.has(p.root)) servers.set(p.root, await serveDir(p.root));
@@ -33,7 +33,7 @@ var PART_AREA = {
33
33
  effects: { area: "effects", choice: "motif" },
34
34
  motion: { area: "motion", choice: "signature" }
35
35
  };
36
- var DEFAULTS = /^(?:ground )?(?:white|grey)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons/;
36
+ var DEFAULTS = /^(?:ground )?(?:white|grey|light)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons|^light page throughout$/;
37
37
  function designFindings(findings, copyTells) {
38
38
  return (findings?.findings ?? []).filter((f) => !copyTells.has(f.tell));
39
39
  }
@@ -140,7 +140,7 @@ function areaOfShared(part) {
140
140
  if (/accent|ground/.test(part)) return "colour";
141
141
  if (/buttons/.test(part)) return "shape";
142
142
  if (/reveal/.test(part)) return "motion";
143
- if (/running order/.test(part)) return "layout";
143
+ if (/running order|hero|page throughout/.test(part)) return "layout";
144
144
  return "effects";
145
145
  }
146
146
  function choiceOfShared(part) {
@@ -150,7 +150,7 @@ function choiceOfShared(part) {
150
150
  if (/ground/.test(part)) return "ground";
151
151
  if (/buttons/.test(part)) return "shape";
152
152
  if (/reveal/.test(part)) return "signature";
153
- return /running order/.test(part) ? void 0 : "motif";
153
+ return /running order|hero|page throughout/.test(part) ? void 0 : "motif";
154
154
  }
155
155
 
156
156
  export {
@@ -10,7 +10,7 @@ import {
10
10
  levenshtein,
11
11
  normaliseFamily,
12
12
  parseColour
13
- } from "./chunk-WFB6F3BN.js";
13
+ } from "./chunk-IHT6CJOM.js";
14
14
  import {
15
15
  TRADE_NOUNS
16
16
  } from "./chunk-4QWEAL4G.js";
@@ -541,6 +541,33 @@ var LICENCES = [
541
541
  checked: "2026-09-24",
542
542
  source: "https://iconscout.com/licenses",
543
543
  note: "Each licence covers a single end product. Never in a tool, template or shared kit (create-site, craft, a house component library), even modified. Record the assets each site uses. Assets come from different contributors, so take one site's set from one contributor."
544
+ },
545
+ {
546
+ name: "Saira Semi Condensed",
547
+ subject: "face",
548
+ licence: "OFL-1.1",
549
+ multiClient: "yes",
550
+ checked: "2026-10-09",
551
+ source: "https://github.com/google/fonts/blob/main/ofl/sairasemicondensed/OFL.txt",
552
+ note: "Copyright 2016 The Saira Project Authors (omnibus.type@gmail.com), reserved font name Saira. Keep the OFL notice with the font files; a modified build may not be called Saira. Chosen for Doman Digital's own site (DOM-397, 9 October 2026)."
553
+ },
554
+ {
555
+ name: "Public Sans",
556
+ subject: "face",
557
+ licence: "OFL-1.1",
558
+ multiClient: "yes",
559
+ checked: "2026-10-09",
560
+ source: "https://github.com/google/fonts/blob/main/ofl/publicsans/OFL.txt",
561
+ note: "Copyright 2015 The Public Sans Project Authors (https://github.com/uswds/public-sans). Keep the OFL notice with the font files. Chosen for Doman Digital's own site (DOM-397, 9 October 2026)."
562
+ },
563
+ {
564
+ name: "Martian Mono",
565
+ subject: "face",
566
+ licence: "OFL-1.1",
567
+ multiClient: "yes",
568
+ checked: "2026-10-09",
569
+ source: "https://github.com/google/fonts/blob/main/ofl/martianmono/OFL.txt",
570
+ note: "Copyright 2021 The Martian Mono Project Authors (https://github.com/evilmartians/mono). Keep the OFL notice with the font files. Chosen for Doman Digital's own site (DOM-397, 9 October 2026)."
544
571
  }
545
572
  ];
546
573
  function faceLicence(family) {
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  auditSnapshot,
6
6
  fingerprint
7
- } from "./chunk-WFB6F3BN.js";
7
+ } from "./chunk-IHT6CJOM.js";
8
8
  import {
9
9
  SNAPSHOT_VERSION
10
10
  } from "./chunk-QYBAVU3P.js";
@@ -505,6 +505,7 @@ function collectInPage() {
505
505
  width: Math.round(b.width),
506
506
  height: Math.round(b.height),
507
507
  top: Math.round(b.top),
508
+ left: Math.round(b.left),
508
509
  role: roleOfImage(el, src, b.width, b.height, alt),
509
510
  alt,
510
511
  decorative,
@@ -752,7 +753,7 @@ async function snapshotUrls(urls, options = {}) {
752
753
  const out = [];
753
754
  try {
754
755
  for (const url of urls) {
755
- const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
756
+ const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 }, ...options.colorScheme ? { colorScheme: options.colorScheme } : {} });
756
757
  try {
757
758
  const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 45e3 });
758
759
  const status = typeof response?.status === "function" ? response.status() : 200;
@@ -9,12 +9,12 @@ import {
9
9
  runHarvest,
10
10
  runNull,
11
11
  serveDir
12
- } from "./chunk-ZUYERAFM.js";
12
+ } from "./chunk-NCBCZFDY.js";
13
13
  import "./chunk-OQXTQQAU.js";
14
- import "./chunk-O5B5ELJH.js";
14
+ import "./chunk-GNVZGG3E.js";
15
15
  import "./chunk-QIEDBL4J.js";
16
16
  import "./chunk-WUACK2HX.js";
17
- import "./chunk-WFB6F3BN.js";
17
+ import "./chunk-IHT6CJOM.js";
18
18
  import "./chunk-4QWEAL4G.js";
19
19
  import "./chunk-QYBAVU3P.js";
20
20
  export {
@@ -7,10 +7,10 @@ import {
7
7
  PROPOSED,
8
8
  researchPrompt,
9
9
  validateDirection
10
- } from "./chunk-6TSDYETQ.js";
10
+ } from "./chunk-RNHV2SXC.js";
11
11
  import {
12
12
  parseFlags
13
- } from "./chunk-O5B5ELJH.js";
13
+ } from "./chunk-GNVZGG3E.js";
14
14
  import {
15
15
  toJson
16
16
  } from "./chunk-QIEDBL4J.js";
@@ -26,7 +26,7 @@ import {
26
26
  isCream,
27
27
  oklchToRgb,
28
28
  parseColour
29
- } from "./chunk-WFB6F3BN.js";
29
+ } from "./chunk-IHT6CJOM.js";
30
30
  import "./chunk-4QWEAL4G.js";
31
31
  import "./chunk-QYBAVU3P.js";
32
32
 
@@ -208,7 +208,7 @@ var readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
208
208
  async function snapshotFrom(target, cwd) {
209
209
  if (!target) return void 0;
210
210
  if (/^https?:\/\//.test(target)) {
211
- const { snapshotUrl } = await import("./audit-4SFSLCKY.js");
211
+ const { snapshotUrl } = await import("./audit-X7LGATAQ.js");
212
212
  return snapshotUrl(target);
213
213
  }
214
214
  return readSnapshot(readJson(resolve(cwd, target)), target);
@@ -1,23 +1,23 @@
1
1
  import {
2
2
  characterReport
3
- } from "./chunk-NTIPJ5U7.js";
3
+ } from "./chunk-OKVHWXYV.js";
4
4
  import {
5
5
  merge
6
- } from "./chunk-DARKHE6A.js";
7
- import "./chunk-SL5VZJ2J.js";
6
+ } from "./chunk-J35UYAVV.js";
7
+ import "./chunk-W7PMRSDG.js";
8
8
  import "./chunk-JYNXGCYZ.js";
9
9
  import {
10
10
  SIBLING_AT,
11
11
  compareToEstate,
12
12
  loadEstate
13
- } from "./chunk-RJVG5KA6.js";
13
+ } from "./chunk-LWKMGJSW.js";
14
14
  import {
15
15
  PAGE_TYPES,
16
16
  validateDirection
17
- } from "./chunk-6TSDYETQ.js";
17
+ } from "./chunk-RNHV2SXC.js";
18
18
  import {
19
19
  loadNull
20
- } from "./chunk-ZUYERAFM.js";
20
+ } from "./chunk-NCBCZFDY.js";
21
21
  import "./chunk-OQXTQQAU.js";
22
22
  import {
23
23
  COPY_FILE,
@@ -28,7 +28,7 @@ import {
28
28
  loadExceptions,
29
29
  parseFlags,
30
30
  readPaths
31
- } from "./chunk-O5B5ELJH.js";
31
+ } from "./chunk-GNVZGG3E.js";
32
32
  import {
33
33
  toJson
34
34
  } from "./chunk-QIEDBL4J.js";
@@ -42,7 +42,7 @@ import {
42
42
  fingerprint,
43
43
  scanSource,
44
44
  typicality
45
- } from "./chunk-WFB6F3BN.js";
45
+ } from "./chunk-IHT6CJOM.js";
46
46
  import "./chunk-4QWEAL4G.js";
47
47
  import "./chunk-QYBAVU3P.js";
48
48
 
@@ -126,7 +126,7 @@ async function runReport(command, args, io) {
126
126
  if (/\.json$/i.test(target2) && existsSync(local)) {
127
127
  snap = readSnapshot(JSON.parse(readFileSync(local, "utf8")), target2);
128
128
  } else {
129
- const { snapshotUrl } = await import("./audit-4SFSLCKY.js");
129
+ const { snapshotUrl } = await import("./audit-X7LGATAQ.js");
130
130
  snap = await snapshotUrl(target2);
131
131
  }
132
132
  const fp = fingerprint(snap);
@@ -4,14 +4,14 @@ import {
4
4
  formatPairs,
5
5
  loadEstate,
6
6
  runEstate
7
- } from "./chunk-RJVG5KA6.js";
8
- import "./chunk-6TSDYETQ.js";
9
- import "./chunk-ZUYERAFM.js";
7
+ } from "./chunk-LWKMGJSW.js";
8
+ import "./chunk-RNHV2SXC.js";
9
+ import "./chunk-NCBCZFDY.js";
10
10
  import "./chunk-OQXTQQAU.js";
11
- import "./chunk-O5B5ELJH.js";
11
+ import "./chunk-GNVZGG3E.js";
12
12
  import "./chunk-QIEDBL4J.js";
13
13
  import "./chunk-WUACK2HX.js";
14
- import "./chunk-WFB6F3BN.js";
14
+ import "./chunk-IHT6CJOM.js";
15
15
  import "./chunk-4QWEAL4G.js";
16
16
  import "./chunk-QYBAVU3P.js";
17
17
  export {
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  TELL_HEAVY
3
- } from "./chunk-NTIPJ5U7.js";
3
+ } from "./chunk-OKVHWXYV.js";
4
4
  import {
5
5
  expandNullTargets,
6
6
  loadNull
7
- } from "./chunk-ZUYERAFM.js";
7
+ } from "./chunk-NCBCZFDY.js";
8
8
  import "./chunk-OQXTQQAU.js";
9
9
  import {
10
10
  parseFlags
11
- } from "./chunk-O5B5ELJH.js";
11
+ } from "./chunk-GNVZGG3E.js";
12
12
  import {
13
13
  toJson
14
14
  } from "./chunk-QIEDBL4J.js";
@@ -21,7 +21,7 @@ import {
21
21
  fingerprint,
22
22
  scanSource,
23
23
  typicality
24
- } from "./chunk-WFB6F3BN.js";
24
+ } from "./chunk-IHT6CJOM.js";
25
25
  import "./chunk-4QWEAL4G.js";
26
26
  import {
27
27
  SNAPSHOT_VERSION
@@ -188,7 +188,7 @@ async function runCalibrate(args, io) {
188
188
  if (toTake.length) {
189
189
  io.err(`craft calibrate: taking ${toTake.length} snapshot${toTake.length === 1 ? "" : "s"}`);
190
190
  try {
191
- const { snapshotUrls } = await import("./audit-4SFSLCKY.js");
191
+ const { snapshotUrls } = await import("./audit-X7LGATAQ.js");
192
192
  const results = await snapshotUrls(toTake.map((t) => t.url));
193
193
  results.forEach((r, i) => {
194
194
  const { entry, keep } = toTake[i];