@crewlethq/tokens 0.5.8 → 0.6.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/test/palette.mjs CHANGED
@@ -74,7 +74,7 @@ export const OPAQUE_SURFACES = [
74
74
  * - THE SHEET OVER THE FRAME is the whole layering device: a content sheet
75
75
  * floating inside the application ground. At dE 3, the floor this file holds
76
76
  * every "can a reader notice it" difference to, the sheet reads as a
77
- * separate plane; the approved dark sheet, #0f0f12, measured 2.91 and was
77
+ * separate plane; the approved dark sheet, #0c0e15, measured 2.96 and was
78
78
  * nudged to clear it.
79
79
  * - A CARD OVER THE SHEET is deliberately near-flat, dE 1.5, because a card is
80
80
  * found by its HAIRLINE rather than by its fill: a page of cards each lifted
@@ -105,16 +105,28 @@ export const RUNG_STEPS = [
105
105
  export const CARD_HAIRLINE = ['--color-border-default', '--color-surface-subtle', '--color-surface-background'];
106
106
  export const HAIRLINE_DE = 3;
107
107
 
108
+ /**
109
+ * A chip drawn on the raised rung inside the strong hairline, as [fill,
110
+ * edge]: the neutral pill, and a key cap. Its fill IS raised, so on a raised
111
+ * ground (a lead block, a raised card, the rail's current row) the fill alone
112
+ * draws nothing, and what finds the chip is its edge. So the chip's boundary,
113
+ * the fill or the edge, whichever stands further off its ground, is held to
114
+ * HAIRLINE_DE on every ground it can land on: each opaque rung, and each
115
+ * overlay a hovered, pressed or inset row composites onto one. A component
116
+ * suite holds the chip to drawing this pair; this is what keeps a palette
117
+ * from taking the edge away.
118
+ */
119
+ export const RAISED_CHIP = ['--color-surface-elevated', '--color-border-strong'];
120
+
108
121
  /**
109
122
  * THE OVERLAY STEPS HAVE TO BE SEEN, on every rung.
110
123
  *
111
- * A translucent overlay is one alpha on four grounds, and the alpha is FITTED
112
- * (the hover token says how): the approved design paints its hover as one
113
- * opaque colour, and a stronger overlay costs the tertiary text step contrast
114
- * on the rungs it lightens or darkens most. Left to that trade alone, the
115
- * cheapest answer for the text is an overlay nobody sees, and the light
116
- * palette's frame kept asking for it: the design's own light hover sits dE
117
- * 0.60 off the frame. This is the floor that stops it. A hovered row separates
124
+ * A translucent overlay is one alpha on four grounds, and a stronger overlay
125
+ * costs the tertiary text step contrast on the rungs it lightens or darkens
126
+ * most. Left to that trade alone, the cheapest answer for the text is an
127
+ * overlay nobody sees, and a light frame keeps asking for it: an overlay that
128
+ * clears the sheet and the card can still sit under dE 3 on the frame. This
129
+ * is the floor that stops it. A hovered row separates
118
130
  * from its rung by dE 3, and a pressed row from a hovered one by dE 3 more,
119
131
  * because dE 3 is the floor this file holds every "can a reader notice it"
120
132
  * difference to. The marketing root is exempt, as it is from the rungs.
@@ -143,6 +155,18 @@ export const TEXT_STEPS = [
143
155
  ['--color-text-tertiary', 4.5],
144
156
  ];
145
157
 
158
+ /**
159
+ * A DIMMED word against the word being read, as [read, dimmed, contrast]: the
160
+ * primary step and the tertiary one. A screen dims what is not its subject by
161
+ * dropping it from the first step to the third (a calendar's days from the
162
+ * neighbouring month, a resolved row under an open one), and both are facts,
163
+ * so each clears its own floor above. What this holds is that the two are a
164
+ * STEP apart rather than a rounding: 2:1 between the inks themselves. A ramp
165
+ * squeezed from both ends, a dimmer primary over a tertiary lifted for its
166
+ * floor on a pressed raised row, can keep every floor and lose the step.
167
+ */
168
+ export const DIMMED_TEXT = ['--color-text-primary', '--color-text-tertiary', 2];
169
+
146
170
  /** A hue used as TEXT on a surface. */
147
171
  export const INK_STEPS = [
148
172
  '--color-brand-accent-ink',
@@ -169,11 +193,11 @@ export const STATUS = [
169
193
  ];
170
194
 
171
195
  /**
172
- * The chart series, in the order a figure assigns them: blue, orange, aqua and
173
- * an ochre yellow, the four the approved design draws.
196
+ * The chart series, in the order a figure assigns them: blue, orange, green
197
+ * and yellow, the four the approved palette draws.
174
198
  *
175
- * FOUR, because four is what both floors below can hold at once. The design's
176
- * four clear them with room (its tightest neighbours sit dE 19.8 apart under
199
+ * FOUR, because four is what both floors below can hold at once. The shipped
200
+ * four clear them with room (their tightest neighbours sit dE 20.6 apart under
177
201
  * normal vision), and a fifth would have to sit between two of them in a hue
178
202
  * budget the reserved red, the accent and a dichromat's collapse of red onto
179
203
  * green have already spent.
@@ -219,13 +243,13 @@ export const LABEL_ON_FILL = [
219
243
  * the fill is the whole of the feedback, so every step clears two things. It
220
244
  * is SEEN: ACTION_DE, the floor this file holds every "can a reader notice
221
245
  * it" difference to. And it moves AWAY from its label, which is ACTION_LABEL
222
- * on every fill: a hover or a press only ever gains contrast. The approved
223
- * design brightened its hover instead (brightness(1.08)), which is the step
224
- * TOWARD a white label, and on the dark accent it took white from 4.53:1 to
225
- * 4.18:1, under the text floor at the moment a reader is about to press. The
226
- * label rule above catches that number in dark; this rule is what catches the
227
- * same move in light, where the brightened step still measured 5.01:1 and
228
- * only its direction was wrong.
246
+ * on every fill: a hover or a press only ever gains contrast. A hover drawn as
247
+ * brightness(1.08) is the step TOWARD a white label: on either accent it
248
+ * takes white under the text floor (4.25:1 in dark, 4.19:1 in light) at the
249
+ * moment a reader is about to press, which the label rule above catches. This
250
+ * rule is what catches a step toward the label that still clears it: the
251
+ * accent dulled at its own lightness reads as a step and keeps its label over
252
+ * 4.5:1, and only its direction is wrong.
229
253
  *
230
254
  * The DESTRUCTIVE action is the other filled action, and it is held the same
231
255
  * way: a toast's destructive action is the danger fill under the same white
@@ -284,12 +308,12 @@ export const RAIL_GROUND = '--color-surface-frame';
284
308
  * border drawn round it INSIDE the row, and the primary ink.
285
309
  *
286
310
  * It is found the way a card is found. Its fill is allowed to be near-flat on
287
- * the frame, RAIL_CURRENT_LIFT, because in light raised is only dE 1.84 off
311
+ * the frame, RAIL_CURRENT_LIFT, because in light raised is only dE 1.93 off
288
312
  * the frame (the design draws it there); what carries the row is the
289
313
  * hairline, held to HAIRLINE_DE against the fill it is drawn on. The hairline
290
- * is also what tells the current row from a HOVERED one, which draws none: in
291
- * dark the two fills land dE 1.56 apart, because the fitted hover overlay
292
- * lifts the frame almost to raised.
314
+ * is also what tells the current row from a HOVERED one, which draws none: a
315
+ * hover overlay strong enough to be seen on the frame can lift it close to
316
+ * raised, and nothing else keeps the two apart.
293
317
  *
294
318
  * It used to be the accent's own soft tint with the label in the accent ink.
295
319
  * The accent is the primary action's fill now, so a violet row in the rail
@@ -443,14 +467,13 @@ export const VEIL_BAND = [1.5, 3];
443
467
  * somewhere the ink steps were never measured against, and it does it in the
444
468
  * one state where a reader cannot tell a rendering fault from a design.
445
469
  *
446
- * 0.65 is the approved dark scrim's 0.62 moved the least distance into the
447
- * band for every root. At 0.62 the veiled dark page's own primary text still
448
- * measured 3.02:1 to 3.15:1 over the four rungs, over the 3:1 ceiling, and the
449
- * marketing root, near-white on black, needs 0.65 before its brightest
450
- * composite comes under it. At 0.65 dark measures 2.73:1 to 2.84:1, light
451
- * 2.18:1 to 2.30:1 and the marketing root 2.75:1 to 2.93:1. The approved light
452
- * scrim, a dark wash at 0.28, left the light page at 8.35:1 to 9.64:1: a page
453
- * nobody had faded at all.
470
+ * 0.65 is the least alpha that brings every root into the band: the marketing
471
+ * root, near-white on black, needs it before its brightest composite comes
472
+ * under the 3:1 ceiling, and the approved palette draws its veil at it in both
473
+ * themes. At 0.65 dark measures 2.56:1 to 2.76:1, light 2.18:1 to 2.29:1 and
474
+ * the marketing root 2.75:1 to 2.93:1. A light veil drawn as a dark wash
475
+ * instead leaves the light page's text readable through it: a page nobody has
476
+ * faded at all.
454
477
  */
455
478
  export const VEIL_GROUND = '--color-surface-frame';
456
479
  export const VEIL_ALPHA = 0.65;
@@ -550,12 +573,11 @@ const ACCENT_DICHROMAT = 8;
550
573
  // because it is the one hue that means the same thing everywhere in a product,
551
574
  // and the accent's dichromat floor. Red, orange and green fall onto one axis
552
575
  // for a dichromat, so this is the floor the chart's orange and aqua spend the
553
- // most on: the approved aqua sat dE 0.73 from the approved red under
554
- // protanopia, and the approved orange 9.08 from it under normal vision in dark
555
- // and 7.45 in light. The fit moves the red further than the orange or the
556
- // aqua, darker, because it had to darken for its white label anyway, and in
557
- // dark it turns toward crimson as well, which spares the orange more than it
558
- // costs the red.
576
+ // most on: the approved green sat dE 6.79 from the approved red under
577
+ // deuteranopia in dark and 2.30 in light, and the approved orange 11.39 from
578
+ // it under normal vision in dark. The fit moves the red darker, because it had
579
+ // to darken for its white label anyway, and turns the green toward aqua, which
580
+ // takes it off the axis the red falls on.
559
581
  const DANGER_NORMAL = 14;
560
582
  const DANGER_DICHROMAT = 8;
561
583
 
@@ -671,6 +693,12 @@ function checkState(state, values, profile, push) {
671
693
  const low = worst(colour(name), all);
672
694
  say('text step clears its floor', low.ratio >= floor, name, low.ratio, `${low.ratio.toFixed(2)}:1 >= ${floor} (worst on ${low.name})`);
673
695
  }
696
+ {
697
+ // A dimmed word against the word being read. See DIMMED_TEXT.
698
+ const [read, dimmed, floor] = DIMMED_TEXT;
699
+ const ratio = contrast(colour(read), colour(dimmed));
700
+ say('a dimmed word is a step from the word being read', ratio >= floor, `${read} vs ${dimmed}`, ratio, `${ratio.toFixed(2)}:1 >= ${floor}`);
701
+ }
674
702
 
675
703
  {
676
704
  // It exists for a hairline glyph and a disabled affordance. The assertion
@@ -1042,6 +1070,29 @@ function checkState(state, values, profile, push) {
1042
1070
  );
1043
1071
  }
1044
1072
  }
1073
+ {
1074
+ // A chip on every ground it can land on. See RAISED_CHIP.
1075
+ const [fill, edge] = RAISED_CHIP;
1076
+ const grounds = [
1077
+ ...opaque,
1078
+ ...OVERLAYS.flatMap((overlay) => opaque.map(([name, rgb]) => [`${overlay} on ${name}`, flatten(values.get(overlay), rgb)])),
1079
+ ];
1080
+ const low = grounds.reduce(
1081
+ (least, [name, ground]) => {
1082
+ const filled = flatten(values.get(fill), ground);
1083
+ const drawn = Math.max(deltaE(filled, ground), deltaE(flatten(values.get(edge), filled), ground));
1084
+ return drawn < least.drawn ? { drawn, name } : least;
1085
+ },
1086
+ { drawn: Infinity, name: '' },
1087
+ );
1088
+ say(
1089
+ 'the strong hairline finds a raised chip on every ground',
1090
+ low.drawn >= HAIRLINE_DE,
1091
+ `${edge} round ${fill}`,
1092
+ low.drawn,
1093
+ `dE ${low.drawn.toFixed(2)} >= ${HAIRLINE_DE} (worst on ${low.name})`,
1094
+ );
1095
+ }
1045
1096
  }
1046
1097
 
1047
1098
  const describe = (measured) => measured.map((value, i) => `${VISIONS[i].slice(0, 1)}${value.toFixed(1)}`).join('/');
@@ -22,6 +22,7 @@ import {
22
22
  DATA_ADJACENT_DE,
23
23
  DATA_ADJACENT_NORMAL_DE,
24
24
  describeFailure,
25
+ DIMMED_TEXT,
25
26
  fromOklab,
26
27
  hex,
27
28
  oklabToLinear,
@@ -30,6 +31,7 @@ import {
30
31
  paletteStates,
31
32
  parseHex,
32
33
  RAIL_CURRENT_ROW,
34
+ RAISED_CHIP,
33
35
  runPalette,
34
36
  RUNG_STEPS,
35
37
  tightest,
@@ -155,6 +157,17 @@ describe('the palette', () => {
155
157
  assert.ok(onBase.includes('text step clears its floor'));
156
158
  });
157
159
 
160
+ test('a dimmed word is measured in every state, and one a rounding from the word being read is caught', () => {
161
+ const DIMMED_RULE = 'a dimmed word is a step from the word being read';
162
+ assert.deepEqual(DIMMED_TEXT, ['--color-text-primary', '--color-text-tertiary', 2]);
163
+ for (const state of STATES) {
164
+ assert.equal(checks.filter((check) => check.state === state && check.rule === DIMMED_RULE).length, 1, state);
165
+ }
166
+ // The tertiary step lifted onto the secondary one: each clears its own
167
+ // floor, and the step between the first and the third is gone.
168
+ assert.deepEqual(failuresAfter('dark', '--color-text-tertiary', typed.dark.color.text.secondary), [DIMMED_RULE]);
169
+ });
170
+
158
171
  test('the theme layer is dark first, and wins over the token layer', () => {
159
172
  // themes.css is imported second on purpose. If that ever stopped being
160
173
  // true the dark state would silently be the marketing palette; and if the
@@ -257,6 +270,18 @@ describe('the four rungs', () => {
257
270
  assert.deepEqual(darkFailures(line, dark.surface.subtle), ['the hairline that finds a card is visible on both sides of it']);
258
271
  });
259
272
 
273
+ test('a raised chip is measured in every theme state, and one whose edge is its own fill is caught', () => {
274
+ const CHIP_RULE = 'the strong hairline finds a raised chip on every ground';
275
+ const [fill, edge] = RAISED_CHIP;
276
+ assert.deepEqual([fill, edge], ['--color-surface-elevated', '--color-border-strong']);
277
+ for (const state of THEME_STATES) {
278
+ assert.equal(checks.filter((check) => check.state === state && check.rule === CHIP_RULE).length, 1, state);
279
+ }
280
+ // The edge drawn in the chip's own fill: on a raised ground the chip is
281
+ // loose words, and nothing else the palette measures notices.
282
+ assert.deepEqual(darkFailures(edge, dark.surface.elevated), [CHIP_RULE]);
283
+ });
284
+
260
285
  test('raised at the card\'s own value is caught', () => {
261
286
  assert.deepEqual(darkFailures('--color-surface-elevated', dark.surface.subtle), ['raised separates from the card']);
262
287
  });
@@ -279,8 +304,8 @@ describe('the accent and the primary action', () => {
279
304
  const [HOVER_RULE, PRESS_RULE] = ACTION_STEPS.map(([rule]) => rule);
280
305
 
281
306
  /**
282
- * What `filter: brightness(1.08)` does to a colour, which is the hover the
283
- * approved design drew: each sRGB channel scaled and clipped.
307
+ * What `filter: brightness(1.08)` does to a colour, the usual way to light a
308
+ * button under the pointer: each sRGB channel scaled and clipped.
284
309
  */
285
310
  const brightened = (value) => {
286
311
  const { r, g, b } = parseHex(value);
@@ -303,18 +328,33 @@ describe('the accent and the primary action', () => {
303
328
  }
304
329
  });
305
330
 
306
- test("the approved design's brightening hover is caught in both palettes", () => {
307
- // In dark it takes the white label under the text floor (4.18:1), which
308
- // the label rule sees; in light it still clears 5.01:1, and only the step
309
- // rule sees that it moved toward the label rather than away from it.
310
- assert.equal(brightened(typed.dark.color.brand.accent), '#865dff');
311
- assert.deepEqual(failuresAfter('dark', '--color-brand-accent-hover', brightened(typed.dark.color.brand.accent)), [
312
- LABEL_RULE,
313
- HOVER_RULE,
314
- ]);
315
- assert.deepEqual(failuresAfter('light', '--color-brand-accent-hover', brightened(typed.light.color.brand.accent)), [
316
- HOVER_RULE,
317
- ]);
331
+ test('a hover that brightens the fill is caught in both palettes', () => {
332
+ // It takes the white label under the text floor (4.25:1 in dark, 4.19:1
333
+ // in light), which the label rule sees, and the step rule sees that it
334
+ // moved toward the label rather than away from it.
335
+ assert.equal(brightened(typed.dark.color.brand.accent), '#7b62f8');
336
+ for (const palette of ['dark', 'light']) {
337
+ assert.deepEqual(
338
+ failuresAfter(palette, '--color-brand-accent-hover', brightened(typed[palette].color.brand.accent)),
339
+ [LABEL_RULE, HOVER_RULE],
340
+ palette,
341
+ );
342
+ }
343
+ });
344
+
345
+ test('a visible step toward the label is caught while the label still clears', () => {
346
+ // The accent dulled to 0.8 of its chroma at its own lightness: a step a
347
+ // reader sees (dE 3.94 in dark), under which the white label still clears
348
+ // the floor (4.72:1), and which moves toward the label all the same. Only
349
+ // the step rule's direction stands between it and a hover that fades.
350
+ const dulled = (value) => {
351
+ const lab = toOklab(parseHex(value));
352
+ return hex(fromOklab({ L: lab.L, a: lab.a * 0.8, b: lab.b * 0.8 }));
353
+ };
354
+ assert.equal(dulled(typed.dark.color.brand.accent), '#7164d1');
355
+ for (const palette of ['dark', 'light']) {
356
+ assert.deepEqual(failuresAfter(palette, '--color-brand-accent-hover', dulled(typed[palette].color.brand.accent)), [HOVER_RULE], palette);
357
+ }
318
358
  });
319
359
 
320
360
  test('a step nobody can see is caught, even one in the right direction', () => {
@@ -357,10 +397,12 @@ describe('the accent and the primary action', () => {
357
397
  assert.deepEqual(failuresAfter('dark', line, typed.dark.color.surface.elevated), [
358
398
  "the hairline round the rail's current row is visible on it",
359
399
  ]);
360
- // Raised two steps off the light frame: every other rule raised is held
361
- // to still holds, and the row is no longer there.
362
- const { r, g, b } = parseHex(typed.light.color.surface.frame);
363
- assert.deepEqual(failuresAfter('light', fill, hex({ r: r + 2, g: g + 2, b: b + 2 })), [
400
+ // Raised two steps off the dark frame: every other rule raised is held
401
+ // to still holds, and the row is no longer there. In light the hairline
402
+ // sits dE 1.23 from the frame, so a row lowered onto the frame loses its
403
+ // line as well, and only dark shows the lift on its own.
404
+ const { r, g, b } = parseHex(typed.dark.color.surface.frame);
405
+ assert.deepEqual(failuresAfter('dark', fill, hex({ r: r + 2, g: g + 2, b: b + 2 })), [
364
406
  "the rail's current row lifts off the rail",
365
407
  ]);
366
408
  });
@@ -410,39 +452,37 @@ describe('the state and chart hues', () => {
410
452
  });
411
453
 
412
454
  test("the design's stopped red is caught by its label, by deuteranopia and by the chart hues", () => {
413
- // The approved #f26d6d carries a destructive action's white label at
414
- // 2.92:1, and a deuteranopic reader finds it dE 5.7 from done (5.59
415
- // against the design's own green, before either moved). It also sits
416
- // under the floors the reserved red keeps from the orange, the aqua
417
- // (protanopia 2.0) and the yellow.
418
- const failing = checksAfter('dark', '--color-feedback-danger', '#f26d6d').map(summary);
455
+ // The approved #f0506e carries a destructive action's white label at
456
+ // 3.46:1, and a deuteranopic reader finds it dE 5.9 from done. It also
457
+ // sits under the floors the reserved red keeps from the orange (11.4
458
+ // under normal vision, 6.8 under deuteranopia) and from the green
459
+ // (deuteranopia 4.9).
460
+ const failing = checksAfter('dark', '--color-feedback-danger', '#f0506e').map(summary);
419
461
  assert.deepEqual(failing, [
420
- `${LABEL_RULE}: --color-text-on-accent on --color-feedback-danger: 2.92:1 (worst on --color-feedback-danger on --color-surface-frame)`,
421
- `${STATUS_RULE}: --color-feedback-success vs --color-feedback-danger: dE n30.4/p15.2/d5.7 >= 10`,
422
- `${DANGER_RULE}: --color-data-2: dE n7.8/p8.3/d7.3 >= 14 normal, 8 dichromat`,
423
- `${DANGER_RULE}: --color-data-3: dE n28.7/p2.0/d14.0 >= 14 normal, 8 dichromat`,
424
- `${DANGER_RULE}: --color-data-4: dE n13.6/p8.9/d7.0 >= 14 normal, 8 dichromat`,
462
+ `${LABEL_RULE}: --color-text-on-accent on --color-feedback-danger: 3.46:1 (worst on --color-feedback-danger on --color-surface-frame)`,
463
+ `${STATUS_RULE}: --color-feedback-success vs --color-feedback-danger: dE n34.5/p18.6/d5.9 >= 10`,
464
+ `${DANGER_RULE}: --color-data-2: dE n11.4/p9.9/d6.8 >= 14 normal, 8 dichromat`,
465
+ `${DANGER_RULE}: --color-data-3: dE n32.5/p12.7/d4.9 >= 14 normal, 8 dichromat`,
425
466
  ]);
426
467
  });
427
468
 
428
469
  test("the design's light yellow is caught as a mark nobody can see on the frame", () => {
429
- // #eda100 on the light frame is 1.85:1, the palest mark the design draws,
430
- // and the only rule it breaks: a yellow dark enough to be seen on a light
431
- // page is an ochre.
432
- assert.deepEqual(checksAfter('light', '--color-data-4', '#eda100').map(summary), [
433
- `${MARK_RULE}: --color-data-4: 1.85:1 (worst on --color-surface-frame)`,
470
+ // #edb302 on the light frame is 1.61:1, and that is the only rule it
471
+ // breaks: a yellow dark enough to be seen on a light page is an ochre.
472
+ assert.deepEqual(checksAfter('light', '--color-data-4', '#edb302').map(summary), [
473
+ `${MARK_RULE}: --color-data-4: 1.61:1 (worst on --color-surface-frame)`,
434
474
  ]);
435
475
  });
436
476
 
437
477
  test('a neighbour that clears every dichromat floor is still caught under normal vision', () => {
438
- // An olive fourth series beside the aqua third: dE 12.1 apart under both
439
- // protanopia and deuteranopia, which the dichromat floor alone passes,
440
- // and 11.6 under normal vision, two shades of one hue to every reader who
441
- // sees colour. Nothing else it touches fails, so the normal-vision floor
442
- // is the only thing standing between it and a chart.
443
- const failing = checksAfter('dark', '--color-data-4', '#5a8100');
478
+ // A teal fourth series beside the aqua green third: dE 13.0 apart under
479
+ // protanopia and 12.3 under deuteranopia, which the dichromat floor alone
480
+ // passes, and 13.0 under normal vision, two shades of one hue to every
481
+ // reader who sees colour. Nothing else it touches fails, so the
482
+ // normal-vision floor is the only thing standing between it and a chart.
483
+ const failing = checksAfter('dark', '--color-data-4', '#008c87');
444
484
  assert.deepEqual(failing.map(summary), [
445
- `${ADJACENT_RULE}: --color-data-3 vs --color-data-4: dE n11.6/p12.1/d12.1 >= ${DATA_ADJACENT_NORMAL_DE} normal, ${DATA_ADJACENT_DE} every vision`,
485
+ `${ADJACENT_RULE}: --color-data-3 vs --color-data-4: dE n13.0/p13.0/d12.3 >= ${DATA_ADJACENT_NORMAL_DE} normal, ${DATA_ADJACENT_DE} every vision`,
446
486
  ]);
447
487
  assert.ok(failing[0].value >= DATA_ADJACENT_DE, 'the mutation has to clear the dichromat floor, or it proves nothing about the normal one');
448
488
  });