@domandigital/craft 0.18.2 → 0.19.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.
package/dist/index.cjs CHANGED
@@ -74,6 +74,7 @@ __export(index_exports, {
74
74
  MOOD: () => MOOD,
75
75
  NEGATIVE_REASSURANCE: () => NEGATIVE_REASSURANCE,
76
76
  NULL_VERSION: () => NULL_VERSION,
77
+ OPENING_BANDS: () => OPENING_BANDS,
77
78
  PAGE_TYPES: () => PAGE_TYPES,
78
79
  PILL_LIMIT: () => PILL_LIMIT,
79
80
  PLAINER_WORDS: () => PLAINER_WORDS,
@@ -117,6 +118,9 @@ __export(index_exports, {
117
118
  STOCK_PHRASES: () => STOCK_PHRASES,
118
119
  STUDY_WORDS: () => STUDY_WORDS,
119
120
  TELL_HEAVY: () => TELL_HEAVY,
121
+ TEMPERATURE_FULL: () => TEMPERATURE_FULL,
122
+ TEMPERATURE_NAMED: () => TEMPERATURE_NAMED,
123
+ TEMPERATURE_WEIGHT: () => TEMPERATURE_WEIGHT,
120
124
  TRADE_NOUNS: () => TRADE_NOUNS,
121
125
  TYPE_STEPS: () => TYPE_STEPS,
122
126
  TYPICAL_AT: () => TYPICAL_AT,
@@ -128,6 +132,7 @@ __export(index_exports, {
128
132
  applyHouseGate: () => applyHouseGate,
129
133
  applySeverity: () => applySeverity,
130
134
  auditSnapshot: () => auditSnapshot,
135
+ bandOf: () => bandOf,
131
136
  briefTerms: () => briefTerms,
132
137
  calibrate: () => calibrate,
133
138
  catalogueTable: () => catalogueTable,
@@ -191,10 +196,14 @@ __export(index_exports, {
191
196
  globToRegExp: () => globToRegExp,
192
197
  gradientContrast: () => gradientContrast,
193
198
  gradientStops: () => gradientStops,
199
+ groundDistance: () => groundDistance,
200
+ groundLabel: () => groundLabel,
194
201
  groundName: () => groundName,
202
+ groundTemperature: () => groundTemperature,
195
203
  harvest: () => harvest,
196
204
  hedgeCount: () => hedgeCount,
197
205
  hedgedClose: () => hedgedClose,
206
+ heroDistance: () => heroDistance,
198
207
  hexToOklch: () => hexToOklch,
199
208
  holdHouseBlocks: () => holdHouseBlocks,
200
209
  houseRule: () => houseRule,
@@ -242,6 +251,8 @@ __export(index_exports, {
242
251
  renderedOrder: () => renderedOrder,
243
252
  researchPrompt: () => researchPrompt,
244
253
  retrofitPlan: () => retrofitPlan,
254
+ rhythmDistance: () => rhythmDistance,
255
+ rhythmOf: () => rhythmOf,
245
256
  runTell: () => runTell,
246
257
  scanProvenance: () => scanProvenance,
247
258
  scanSource: () => scanSource,
@@ -268,6 +279,7 @@ __export(index_exports, {
268
279
  tailwindV3Preset: () => tailwindV3Preset,
269
280
  tailwindV4Theme: () => tailwindV4Theme,
270
281
  tellById: () => tellById,
282
+ temperatureName: () => temperatureName,
271
283
  toGamut: () => toGamut,
272
284
  toJson: () => toJson,
273
285
  toSarif: () => toSarif,
@@ -4332,8 +4344,100 @@ function markupParts(file) {
4332
4344
  function textNodeIsProse(body) {
4333
4345
  return /[a-zA-Z]{2,}/.test(body) && /\s/.test(body.trim()) || /[a-zA-Z]{4,}[.!?]/.test(body);
4334
4346
  }
4347
+ var FRONT_MATTER_PROSE = /* @__PURE__ */ new Set(["title", "description"]);
4348
+ var KEY_LINE = /^(\s*)(?:-\s+)?([A-Za-z][\w-]*)[ \t]*:[ \t]*/;
4349
+ var BLOCK_SCALAR = /^(?:[|>][+-]?\d*|[|>]\d+[+-]?)$/;
4350
+ function readFrontKey(line) {
4351
+ const m = KEY_LINE.exec(line);
4352
+ if (!m) return null;
4353
+ return { indent: m[1].length, name: m[2], valueAt: m[0].length, value: line.slice(m[0].length) };
4354
+ }
4355
+ function indentOf(line) {
4356
+ let i = 0;
4357
+ while (i < line.length && (line[i] === " " || line[i] === " ")) i += 1;
4358
+ return i;
4359
+ }
4360
+ function commentAt(value) {
4361
+ if (value.startsWith("#")) return 0;
4362
+ const found = /[ \t]+#/.exec(value);
4363
+ return found ? found.index : -1;
4364
+ }
4365
+ function keepSpan(line, from, to) {
4366
+ const start = Math.max(0, Math.min(from, line.length));
4367
+ const end = Math.max(start, Math.min(to, line.length));
4368
+ return " ".repeat(start) + line.slice(start, end) + " ".repeat(line.length - end);
4369
+ }
4370
+ function quoteEnd(line, from, quote) {
4371
+ for (let i = from; i < line.length; i += 1) {
4372
+ if (quote === '"' && line[i] === "\\") {
4373
+ i += 1;
4374
+ continue;
4375
+ }
4376
+ if (quote === "'" && line[i] === "'" && line[i + 1] === "'") {
4377
+ i += 1;
4378
+ continue;
4379
+ }
4380
+ if (line[i] === quote) return i;
4381
+ }
4382
+ return -1;
4383
+ }
4384
+ function maskFrontMatter(block) {
4385
+ const lines = block.split("\n");
4386
+ const out = lines.map((line) => blank(line));
4387
+ for (let i = 1; i < lines.length; ) {
4388
+ const line = lines[i];
4389
+ if (line === "---" || line === "...") break;
4390
+ const key = readFrontKey(line);
4391
+ if (!key || !FRONT_MATTER_PROSE.has(key.name.toLowerCase())) {
4392
+ i += 1;
4393
+ continue;
4394
+ }
4395
+ const quote = key.value.startsWith('"') ? '"' : key.value.startsWith("'") ? "'" : "";
4396
+ if (quote) {
4397
+ let cursor = key.valueAt + 1;
4398
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
4399
+ const current = lines[i];
4400
+ const end = quoteEnd(current, cursor, quote);
4401
+ out[i] = keepSpan(current, cursor, end === -1 ? current.length : end);
4402
+ i += 1;
4403
+ cursor = 0;
4404
+ if (end !== -1) break;
4405
+ }
4406
+ continue;
4407
+ }
4408
+ const hash = commentAt(key.value);
4409
+ const body = (hash === -1 ? key.value : key.value.slice(0, hash)).trim();
4410
+ if (BLOCK_SCALAR.test(body)) {
4411
+ i += 1;
4412
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
4413
+ const current = lines[i];
4414
+ if (current.trim() !== "" && indentOf(current) <= key.indent) break;
4415
+ if (current.trim()) out[i] = current;
4416
+ i += 1;
4417
+ }
4418
+ continue;
4419
+ }
4420
+ if (body === "" || body === "~" || /^null$/i.test(body) || body.startsWith("[") || body.startsWith("{")) {
4421
+ i += 1;
4422
+ continue;
4423
+ }
4424
+ out[i] = keepSpan(line, key.valueAt, hash === -1 ? line.length : key.valueAt + hash);
4425
+ i += 1;
4426
+ while (i < lines.length && lines[i] !== "---" && lines[i] !== "...") {
4427
+ const current = lines[i];
4428
+ if (current.trim() === "" || indentOf(current) <= key.indent) break;
4429
+ if (!/^\s*#/.test(current)) {
4430
+ const cut = commentAt(current);
4431
+ out[i] = cut === -1 ? current : keepSpan(current, 0, cut);
4432
+ }
4433
+ i += 1;
4434
+ }
4435
+ }
4436
+ const masked = out.join("\n");
4437
+ return masked.length === block.length ? masked : blank(block);
4438
+ }
4335
4439
  function proseBlocks(file) {
4336
- const masked = file.text.replace(/^(```|~~~)[\s\S]*?^\1/gm, blank).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length)).replace(/^---\n[\s\S]*?\n---\n/, blank).replace(/<!--[\s\S]*?-->/g, blank);
4440
+ const masked = file.text.replace(/^(```|~~~)[\s\S]*?^\1/gm, blank).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length)).replace(/^---\n[\s\S]*?\n---\n/, (block) => maskFrontMatter(block)).replace(/<!--[\s\S]*?-->/g, blank);
4337
4441
  return [{ path: file.path, text: masked, offset: 0 }];
4338
4442
  }
4339
4443
  function isData(path) {
@@ -5321,7 +5425,7 @@ var RENDERED_PATHS = {
5321
5425
  };
5322
5426
 
5323
5427
  // src/fingerprint/index.ts
5324
- var FINGERPRINT_VERSION = 2;
5428
+ var FINGERPRINT_VERSION = 3;
5325
5429
  var CHROMATIC = 0.03;
5326
5430
  var MIN_ACCENT_AREA = 2e4;
5327
5431
  var oklch = (value) => {
@@ -5357,6 +5461,24 @@ function median(values) {
5357
5461
  const mid = Math.floor(sorted.length / 2);
5358
5462
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
5359
5463
  }
5464
+ var FULL_WIDTH_IMAGE = 0.8;
5465
+ function heroOf(s) {
5466
+ const h1 = s.headings.find((h) => h.level === 1);
5467
+ const inside = (top, x) => top >= x.top && top < x.top + x.height;
5468
+ const index = (() => {
5469
+ const withH1 = h1 ? s.sections.findIndex((x) => x.kind === "hero" && inside(h1.top, x)) : -1;
5470
+ return withH1 >= 0 ? withH1 : s.sections.findIndex((x) => x.kind === "hero");
5471
+ })();
5472
+ if (index < 0) return void 0;
5473
+ const section = s.sections[index];
5474
+ const headline = h1 && inside(h1.top, section) ? { sizePx: h1.sizePx, weight: h1.weight, class: fontClass(normaliseFamily(h1.family)), italic: h1.italic || h1.italicPart } : null;
5475
+ if (!s.images) return { headline };
5476
+ 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];
5477
+ if (!picture2) return { headline, image: "none" };
5478
+ if (picture2.left === void 0) return { headline };
5479
+ const image = picture2.width >= FULL_WIDTH_IMAGE * s.viewport.width ? "full" : picture2.left + picture2.width / 2 < s.viewport.width / 2 ? "left" : "right";
5480
+ return { headline, image };
5481
+ }
5360
5482
  function fingerprint(s) {
5361
5483
  const effects = [];
5362
5484
  if (s.effects.glass > 0) effects.push("glass-panel");
@@ -5368,6 +5490,7 @@ function fingerprint(s) {
5368
5490
  if (s.effects.eyebrowChip) effects.push("hero-eyebrow-chip");
5369
5491
  if (s.motion.introOverlay) effects.push("intro-cinematic");
5370
5492
  const below = s.sections.filter((x) => x.top > s.viewport.height).length;
5493
+ const hero = heroOf(s);
5371
5494
  return {
5372
5495
  version: FINGERPRINT_VERSION,
5373
5496
  accent: accentOf(s),
@@ -5391,10 +5514,28 @@ function fingerprint(s) {
5391
5514
  carousel: x.carousel ?? false,
5392
5515
  figures: x.figures ?? 0
5393
5516
  }))
5394
- } : {}
5517
+ } : {},
5518
+ ...hero ? { hero } : {}
5395
5519
  };
5396
5520
  }
5397
- var DISTANCE_WEIGHTS = { accent: 0.25, type: 0.25, ground: 0.1, shape: 0.1, motion: 0.1, effects: 0.1, layout: 0.1 };
5521
+ var WARM_HUE = [
5522
+ [330, 360],
5523
+ [0, 130]
5524
+ ];
5525
+ var COOL_HUE = [[160, 310]];
5526
+ var TEMPERATURE_FULL = 6e-3;
5527
+ var TEMPERATURE_NAMED = 0.4;
5528
+ function groundTemperature(o) {
5529
+ const h = (o.h % 360 + 360) % 360;
5530
+ const within = (bands) => bands.some(([lo, hi]) => h >= lo && h < hi);
5531
+ const strength = Math.min(1, o.c / TEMPERATURE_FULL);
5532
+ return within(WARM_HUE) ? strength : within(COOL_HUE) ? -strength : 0;
5533
+ }
5534
+ function temperatureName(o) {
5535
+ const t = groundTemperature(o);
5536
+ return t >= TEMPERATURE_NAMED ? "warm" : t <= -TEMPERATURE_NAMED ? "cool" : "neutral";
5537
+ }
5538
+ 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 };
5398
5539
  function levenshtein(a, b, cost = (x, y) => x === y ? 0 : 1) {
5399
5540
  const row = Array.from({ length: b.length + 1 }, (_, i) => i);
5400
5541
  for (let i = 1; i <= a.length; i += 1) {
@@ -5421,18 +5562,62 @@ function layoutDistance(a, b) {
5421
5562
  return longest === 0 ? 0 : levenshtein(a.layout, b.layout) / longest;
5422
5563
  }
5423
5564
  var clamp = (n) => Math.max(0, Math.min(1, n));
5565
+ function bandOf(l) {
5566
+ return l < 0.4 ? "dark" : l >= 0.75 ? "light" : "mid";
5567
+ }
5568
+ function rhythmOf(fp) {
5569
+ if (!fp.sections) return null;
5570
+ const out = [];
5571
+ for (const s of fp.sections) {
5572
+ if (s.background === void 0) return null;
5573
+ const band = bandOf((s.background ?? fp.ground).l);
5574
+ if (out[out.length - 1] !== band) out.push(band);
5575
+ }
5576
+ return out;
5577
+ }
5578
+ var OPENING_BANDS = 3;
5579
+ function rhythmDistance(a, b) {
5580
+ const ra = rhythmOf(a);
5581
+ const rb = rhythmOf(b);
5582
+ if (!ra || !rb) return null;
5583
+ const over = (x, y) => {
5584
+ const longest = Math.max(x.length, y.length);
5585
+ return longest === 0 ? 0 : levenshtein(x, y) / longest;
5586
+ };
5587
+ return (over(ra.slice(0, OPENING_BANDS), rb.slice(0, OPENING_BANDS)) + over(ra, rb)) / 2;
5588
+ }
5589
+ var HEADLINE_OCTAVES = 1;
5590
+ function heroSection(fp) {
5591
+ return fp.sections?.find((s) => s.role === "hero");
5592
+ }
5593
+ function heroDistance(a, b) {
5594
+ const sa = heroSection(a);
5595
+ const sb = heroSection(b);
5596
+ if (!sa || !sb) return null;
5597
+ const parts2 = [Math.abs(sa.centred - sb.centred), clamp(Math.abs(sa.width - sb.width) / 0.5)];
5598
+ const ha = a.hero?.headline;
5599
+ const hb = b.hero?.headline;
5600
+ 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));
5601
+ else if (a.hero && b.hero && (ha || hb)) parts2.push(1);
5602
+ if (a.hero?.image && b.hero?.image) parts2.push(a.hero.image === b.hero.image ? 0 : 1);
5603
+ return parts2.reduce((s, p) => s + p, 0) / parts2.length;
5604
+ }
5424
5605
  function faceDistance(a, b) {
5425
5606
  if (a.family.toLowerCase() === b.family.toLowerCase()) return 0;
5426
5607
  return a.class === b.class ? 0.5 : 1;
5427
5608
  }
5428
5609
  var ACCENT_SCALE = 0.3;
5429
5610
  var GROUND_SCALE = 0.15;
5611
+ var TEMPERATURE_WEIGHT = 0.6;
5430
5612
  var SHARED_PART = 0.2;
5431
5613
  var ACCENT_SHARED = ACCENT_SCALE * SHARED_PART;
5432
5614
  var GROUND_SHARED = GROUND_SCALE * SHARED_PART;
5615
+ function groundDistance(a, b) {
5616
+ return clamp(deltaEOk(a, b) / GROUND_SCALE + TEMPERATURE_WEIGHT * Math.abs(groundTemperature(a) - groundTemperature(b)) / 2);
5617
+ }
5433
5618
  function fingerprintDistance(a, b) {
5434
5619
  const accent = a.accent && b.accent ? clamp(deltaEOk(a.accent, b.accent) / ACCENT_SCALE) : a.accent || b.accent ? 1 : 0;
5435
- const ground = clamp(deltaEOk(a.ground, b.ground) / GROUND_SCALE);
5620
+ const ground = groundDistance(a.ground, b.ground);
5436
5621
  const type = 0.6 * faceDistance(a.display, b.display) + 0.4 * faceDistance(a.body, b.body);
5437
5622
  const shape = a.roundness === null || b.roundness === null ? a.roundness === b.roundness ? 0 : 0.5 : clamp(Math.abs(a.roundness - b.roundness) / 0.5);
5438
5623
  const motion = clamp(Math.abs(a.motion - b.motion));
@@ -5440,9 +5625,18 @@ function fingerprintDistance(a, b) {
5440
5625
  const shared = a.effects.filter((e) => b.effects.includes(e)).length;
5441
5626
  const effects = union.size === 0 ? 0 : 1 - shared / union.size;
5442
5627
  const layout = layoutDistance(a, b);
5443
- const parts2 = { accent, ground, type, shape, motion, effects, layout };
5444
- const total = Object.keys(DISTANCE_WEIGHTS).reduce((sum, k) => sum + DISTANCE_WEIGHTS[k] * parts2[k], 0);
5445
- return { total: Math.round(total * 1e3) / 1e3, parts: parts2 };
5628
+ const rhythm = rhythmDistance(a, b);
5629
+ const hero = heroDistance(a, b);
5630
+ const parts2 = { accent, ground, type, shape, motion, effects, layout, rhythm, hero };
5631
+ let sum = 0;
5632
+ let weight = 0;
5633
+ for (const k of Object.keys(DISTANCE_WEIGHTS)) {
5634
+ const p = parts2[k];
5635
+ if (p === null) continue;
5636
+ sum += DISTANCE_WEIGHTS[k] * p;
5637
+ weight += DISTANCE_WEIGHTS[k];
5638
+ }
5639
+ return { total: Math.round(sum / weight * 1e3) / 1e3, parts: parts2 };
5446
5640
  }
5447
5641
  function nearest(target2, set, k = set.length) {
5448
5642
  return set.map((item) => ({ id: item.id, distance: fingerprintDistance(target2, item.fingerprint) })).sort((x, y) => x.distance.total - y.distance.total).slice(0, k);
@@ -5520,6 +5714,12 @@ function groundName(o) {
5520
5714
  if (o.l >= 0.97 && o.c < 0.012) return "white";
5521
5715
  return o.c < 0.02 ? "grey" : "tinted";
5522
5716
  }
5717
+ function groundLabel(o) {
5718
+ const name = groundName(o);
5719
+ if (name === "cream" || name === "tinted") return name;
5720
+ const temperature = temperatureName(o);
5721
+ return temperature === "neutral" ? name : `${temperature} ${name}`;
5722
+ }
5523
5723
  function shapeName(roundness) {
5524
5724
  if (roundness === null) return "no buttons";
5525
5725
  if (roundness >= 0.45) return "pill";
@@ -8794,6 +8994,33 @@ var LICENCES = [
8794
8994
  checked: "2026-09-24",
8795
8995
  source: "https://iconscout.com/licenses",
8796
8996
  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."
8997
+ },
8998
+ {
8999
+ name: "Saira Semi Condensed",
9000
+ subject: "face",
9001
+ licence: "OFL-1.1",
9002
+ multiClient: "yes",
9003
+ checked: "2026-10-09",
9004
+ source: "https://github.com/google/fonts/blob/main/ofl/sairasemicondensed/OFL.txt",
9005
+ 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)."
9006
+ },
9007
+ {
9008
+ name: "Public Sans",
9009
+ subject: "face",
9010
+ licence: "OFL-1.1",
9011
+ multiClient: "yes",
9012
+ checked: "2026-10-09",
9013
+ source: "https://github.com/google/fonts/blob/main/ofl/publicsans/OFL.txt",
9014
+ 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)."
9015
+ },
9016
+ {
9017
+ name: "Martian Mono",
9018
+ subject: "face",
9019
+ licence: "OFL-1.1",
9020
+ multiClient: "yes",
9021
+ checked: "2026-10-09",
9022
+ source: "https://github.com/google/fonts/blob/main/ofl/martianmono/OFL.txt",
9023
+ 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)."
8797
9024
  }
8798
9025
  ];
8799
9026
  function faceLicence(family) {
@@ -9160,7 +9387,7 @@ var PART_AREA = {
9160
9387
  effects: { area: "effects", choice: "motif" },
9161
9388
  motion: { area: "motion", choice: "signature" }
9162
9389
  };
9163
- var DEFAULTS = /^(?:ground )?(?:white|grey)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons/;
9390
+ var DEFAULTS = /^(?:ground )?(?:white|grey|light)(?: ground)?$|no scroll reveals|no reveals|accent (?:none|neutral)|^no buttons|shape no buttons|^light page throughout$/;
9164
9391
  function designFindings(findings, copyTells) {
9165
9392
  return (findings?.findings ?? []).filter((f4) => !copyTells.has(f4.tell));
9166
9393
  }
@@ -9267,7 +9494,7 @@ function areaOfShared(part) {
9267
9494
  if (/accent|ground/.test(part)) return "colour";
9268
9495
  if (/buttons/.test(part)) return "shape";
9269
9496
  if (/reveal/.test(part)) return "motion";
9270
- if (/running order/.test(part)) return "layout";
9497
+ if (/running order|hero|page throughout/.test(part)) return "layout";
9271
9498
  return "effects";
9272
9499
  }
9273
9500
  function choiceOfShared(part) {
@@ -9277,7 +9504,7 @@ function choiceOfShared(part) {
9277
9504
  if (/ground/.test(part)) return "ground";
9278
9505
  if (/buttons/.test(part)) return "shape";
9279
9506
  if (/reveal/.test(part)) return "signature";
9280
- return /running order/.test(part) ? void 0 : "motif";
9507
+ return /running order|hero|page throughout/.test(part) ? void 0 : "motif";
9281
9508
  }
9282
9509
 
9283
9510
  // src/calibrate/index.ts
@@ -9384,19 +9611,45 @@ function addSite(register, site) {
9384
9611
  const sites = [...register.sites.filter((s) => s.id !== site.id), site].sort((a, b) => a.id.localeCompare(b.id));
9385
9612
  return { version: ESTATE_VERSION, sites };
9386
9613
  }
9614
+ function sharedGround(a, b) {
9615
+ const [x, y] = [groundLabel(a.ground), groundLabel(b.ground)];
9616
+ if (x === y) return `${x} ground`;
9617
+ const temperature = temperatureName(a.ground);
9618
+ if (temperature !== "neutral" && temperature === temperatureName(b.ground)) return `${temperature} ground`;
9619
+ return `${a.ground.l < 0.3 ? "dark" : "light"} ground`;
9620
+ }
9621
+ function sharedRhythm(a, b) {
9622
+ const bands = rhythmOf(a) ?? [];
9623
+ const word = (band) => band;
9624
+ if (bands.length <= 1) return `${bands[0] ? word(bands[0]) : "light"} page throughout`;
9625
+ const second = bands[1] === "mid" ? "mid-tone band" : `${word(bands[1])} band`;
9626
+ return `${word(bands[0])} hero then ${second}`;
9627
+ }
9628
+ function sharedHero(a, b) {
9629
+ const hero = a.sections?.find((s) => s.role === "hero");
9630
+ const placement = hero && hero.centred >= 0.5 ? "centred" : "left-aligned";
9631
+ const headline = a.hero?.headline && b.hero?.headline && a.hero.headline.class === b.hero.headline.class ? `${a.hero.headline.class} ` : "";
9632
+ 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}`}` : "";
9633
+ return `${placement} ${headline}hero${image}`;
9634
+ }
9387
9635
  function sharedParts(a, b, d = fingerprintDistance(a, b)) {
9388
9636
  const out = [];
9389
- const close = (k) => d.parts[k] <= SHARED_PART;
9637
+ const close = (k) => {
9638
+ const p = d.parts[k];
9639
+ return p !== null && p <= SHARED_PART;
9640
+ };
9390
9641
  if (close("accent")) out.push(a.accent || b.accent ? `${hueName(a.accent)} accent` : "no accent");
9391
9642
  if (close("type")) {
9392
9643
  if (a.display.family === b.display.family) out.push(`${a.display.family} headline`);
9393
9644
  if (a.body.family === b.body.family) out.push(`${a.body.family} body`);
9394
9645
  }
9395
- if (close("ground")) out.push(`${groundName(a.ground)} ground`);
9646
+ if (close("ground")) out.push(sharedGround(a, b));
9396
9647
  if (close("shape")) out.push(`${shapeName(a.roundness)} buttons`);
9397
9648
  if (close("motion")) out.push(a.motion > 0 || b.motion > 0 ? "scroll reveals" : "no reveals");
9398
9649
  if (close("effects") && a.effects.length) out.push(a.effects.filter((e) => b.effects.includes(e)).join(", "));
9399
- if (close("layout")) out.push("running order");
9650
+ if (close("hero")) out.push(sharedHero(a, b));
9651
+ if (close("rhythm")) out.push(sharedRhythm(a, b));
9652
+ if (close("layout")) out.push("same running order");
9400
9653
  return out;
9401
9654
  }
9402
9655
  function compareToEstate(target2, register, options = {}) {
@@ -9536,6 +9789,7 @@ function retrofitPlan(report2, direction) {
9536
9789
  MOOD,
9537
9790
  NEGATIVE_REASSURANCE,
9538
9791
  NULL_VERSION,
9792
+ OPENING_BANDS,
9539
9793
  PAGE_TYPES,
9540
9794
  PILL_LIMIT,
9541
9795
  PLAINER_WORDS,
@@ -9579,6 +9833,9 @@ function retrofitPlan(report2, direction) {
9579
9833
  STOCK_PHRASES,
9580
9834
  STUDY_WORDS,
9581
9835
  TELL_HEAVY,
9836
+ TEMPERATURE_FULL,
9837
+ TEMPERATURE_NAMED,
9838
+ TEMPERATURE_WEIGHT,
9582
9839
  TRADE_NOUNS,
9583
9840
  TYPE_STEPS,
9584
9841
  TYPICAL_AT,
@@ -9590,6 +9847,7 @@ function retrofitPlan(report2, direction) {
9590
9847
  applyHouseGate,
9591
9848
  applySeverity,
9592
9849
  auditSnapshot,
9850
+ bandOf,
9593
9851
  briefTerms,
9594
9852
  calibrate,
9595
9853
  catalogueTable,
@@ -9653,10 +9911,14 @@ function retrofitPlan(report2, direction) {
9653
9911
  globToRegExp,
9654
9912
  gradientContrast,
9655
9913
  gradientStops,
9914
+ groundDistance,
9915
+ groundLabel,
9656
9916
  groundName,
9917
+ groundTemperature,
9657
9918
  harvest,
9658
9919
  hedgeCount,
9659
9920
  hedgedClose,
9921
+ heroDistance,
9660
9922
  hexToOklch,
9661
9923
  holdHouseBlocks,
9662
9924
  houseRule,
@@ -9704,6 +9966,8 @@ function retrofitPlan(report2, direction) {
9704
9966
  renderedOrder,
9705
9967
  researchPrompt,
9706
9968
  retrofitPlan,
9969
+ rhythmDistance,
9970
+ rhythmOf,
9707
9971
  runTell,
9708
9972
  scanProvenance,
9709
9973
  scanSource,
@@ -9730,6 +9994,7 @@ function retrofitPlan(report2, direction) {
9730
9994
  tailwindV3Preset,
9731
9995
  tailwindV4Theme,
9732
9996
  tellById,
9997
+ temperatureName,
9733
9998
  toGamut,
9734
9999
  toJson,
9735
10000
  toSarif,