@obvi/blueprint 1.3.0 → 1.3.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.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "markupSchema": 2,
3
- "frameworkVersion": "1.3.0",
4
- "source": "github:FlatFilers/blueprint-framework@v1.3.0",
3
+ "frameworkVersion": "1.3.1",
4
+ "source": "github:FlatFilers/blueprint-framework@v1.3.1",
5
5
  "assets": {
6
6
  "vendor/blueprint/blueprint.css": {
7
- "sha256": "7ba1864176042524b1bacdeea0d1af2445fe55860c8bf0b857bca878c45c1cbf"
7
+ "sha256": "eaf4ca1936c6231709fdafea34f47c5e919e31b9a5d3d69398542af7ba30bb83"
8
8
  },
9
9
  "vendor/blueprint/blueprint.js": {
10
10
  "sha256": "55f1b677d5302bddba2edb840d26bbe409252fce20345a3d4b6368fd04526954"
@@ -181,9 +181,21 @@
181
181
  --bp-hatch-gap: 7px;
182
182
  --bp-hatch: repeating-linear-gradient(
183
183
  -45deg,
184
- var(--bp-ink-faint) 0 var(--bp-stroke),
185
- oklch(0 0 0 / 0) var(--bp-stroke) var(--bp-hatch-gap)
184
+ var(--bp-ink-faint) 0,
185
+ var(--bp-ink-faint) var(--bp-stroke),
186
+ oklch(0 0 0 / 0) var(--bp-stroke),
187
+ oklch(0 0 0 / 0) var(--bp-hatch-gap)
186
188
  );
189
+ /* Fixed fullscreen scrims in WebKit rasterize more predictably when the
190
+ hatch is tiled through a mask on a pseudo-element instead of painted
191
+ directly as a repeating gradient on the fixed layer itself. */
192
+ --bp-hatch-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 7 7'%3E%3Cpath d='M-1 8L8 -1' stroke='%23000' stroke-width='1' stroke-linecap='square' shape-rendering='crispEdges'/%3E%3C/svg%3E");
193
+ /* Hatch line color for the masked ::before treatment used on non-fixed
194
+ hatch surfaces (preflight gate, empty state, rejected choice cards).
195
+ Tracks the --bp-hatch gradient line color per theme (ink-faint in
196
+ light, edge in dark) so swapping the soft diagonal gradient for the
197
+ crisp deterministic mask never shifts the rendered color. */
198
+ --bp-hatch-ink: var(--bp-ink-faint);
187
199
  /* Drafting board — a faint dot-grid backdrop that marks INTERACTIVE
188
200
  component surfaces (choice, preflight, choice-record, source, mock,
189
201
  gallery): a second axis distinct from the diagonal --bp-hatch (which
@@ -216,11 +228,16 @@
216
228
  --bp-board-card-pad: var(--bp-space-4);
217
229
  --bp-board-card-radius: var(--bp-radius-8);
218
230
  /* Modal scrim wash — darker than --bp-ink-soft so floated panels (search
219
- palette, lightbox, expanded mock) read clearly over the page. Always
220
- pair with --bp-hatch:
221
- background-color: var(--bp-scrim);
222
- background-image: var(--bp-hatch); */
231
+ palette, lightbox, expanded mock) read clearly over the page. The wash
232
+ is painted as a SOLID background-image layer (--bp-scrim-fill), not as
233
+ background-color: WebKit/Safari can drop a semi-transparent oklch
234
+ background-color while still painting gradient background-image layers,
235
+ which left the wash faint/absent there while the hatch texture kept
236
+ drawing. Fixed fullscreen overlays pair this fill on the host with the
237
+ diagonal hatch on a masked pseudo-element (see --bp-hatch-mask) so the
238
+ repeating gradient itself is not rasterized on the fixed layer. */
223
239
  --bp-scrim: oklch(0 0 0 / 0.58);
240
+ --bp-scrim-fill: linear-gradient(var(--bp-scrim), var(--bp-scrim));
224
241
  --bp-edge: var(--obvious-border-subtle, oklch(0 0 0 / 0.08));
225
242
  --bp-ink-line: var(--obvious-border-default, oklch(0 0 0 / 0.16));
226
243
  --bp-text: var(--obvious-text-primary, oklch(0 0 0 / 0.92));
@@ -437,9 +454,14 @@
437
454
  rejected-choice fills keep texture without brightening the wash. */
438
455
  --bp-hatch: repeating-linear-gradient(
439
456
  -45deg,
440
- var(--bp-edge) 0 var(--bp-stroke),
441
- oklch(0 0 0 / 0) var(--bp-stroke) var(--bp-hatch-gap)
457
+ var(--bp-edge) 0,
458
+ var(--bp-edge) var(--bp-stroke),
459
+ oklch(0 0 0 / 0) var(--bp-stroke),
460
+ oklch(0 0 0 / 0) var(--bp-hatch-gap)
442
461
  );
462
+ /* Dark hatch line color = --bp-edge (matches the dark --bp-hatch gradient
463
+ above), keeping the masked ::before faithful to the gradient in dark. */
464
+ --bp-hatch-ink: var(--bp-edge);
443
465
  --bp-paper: var(--bp-gray-950);
444
466
  --bp-bg: oklch(0.15 0 0); /* the desk sits darker than the sheet, so the page lifts */
445
467
  --bp-edge: oklch(1 0 0 / 0.08);
@@ -1481,7 +1503,10 @@
1481
1503
  --bp-tick: var(--bp-ink);
1482
1504
  --bp-tick-len: 16px;
1483
1505
  --bp-tick-wt: 2.5px;
1484
- --bp-callout-fill: var(--bp-hatch);
1506
+ /* Hatch is painted by the crisp masked ::before (shared hatch-mask rule),
1507
+ leaving the corner tick stack on the host. */
1508
+ --bp-callout-fill: none;
1509
+ isolation: isolate;
1485
1510
  background-color: var(--bp-fill-hi);
1486
1511
  }
1487
1512
  /* Reference = enumerated values (subdued, soft ticks). */
@@ -1807,7 +1832,10 @@
1807
1832
  }
1808
1833
  :where(.bp-choice--stack:has(.bp-choice__pick:checked)) :where(.bp-choice__card:not(:has(.bp-choice__pick:checked))) {
1809
1834
  opacity: 0.62;
1810
- background-image: var(--bp-hatch);
1835
+ /* Hatch is painted by the crisp masked ::before (shared hatch-mask rule),
1836
+ not a diagonal gradient — the gradient anti-aliases differently in
1837
+ WebKit vs Chromium. The card is already position:relative. */
1838
+ isolation: isolate;
1811
1839
  }
1812
1840
  :where(.bp-choice--stack:has(.bp-choice__pick:checked)) :where(.bp-choice__card:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__card-rejected) {
1813
1841
  display: inline-flex;
@@ -1839,7 +1867,8 @@
1839
1867
  }
1840
1868
  :where(.bp-choice--resolved) :where(.bp-choice__card:not([data-resolved])) {
1841
1869
  opacity: 0.62;
1842
- background-image: var(--bp-hatch);
1870
+ /* Crisp masked ::before hatch (cross-engine parity), see hatch-mask rule. */
1871
+ isolation: isolate;
1843
1872
  }
1844
1873
  :where(.bp-choice--resolved) :where(.bp-choice__card:not([data-resolved])) :where(.bp-choice__card-rejected) {
1845
1874
  display: inline-flex;
@@ -1903,7 +1932,9 @@
1903
1932
  opacity: 0.5;
1904
1933
  }
1905
1934
  :where(.bp-choice--gallery:has(.bp-choice__pick:checked)) :where(.bp-choice__mock:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__mock-frame) {
1906
- background-image: var(--bp-hatch);
1935
+ /* Crisp masked ::before hatch (cross-engine parity), see hatch-mask rule. */
1936
+ position: relative;
1937
+ isolation: isolate;
1907
1938
  }
1908
1939
  :where(.bp-choice--gallery:has(.bp-choice__pick:checked)) :where(.bp-choice__mock:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__mock-rejected) {
1909
1940
  display: inline-flex;
@@ -2061,12 +2092,17 @@
2061
2092
  accent-color: var(--bp-button-primary-fg);
2062
2093
  }
2063
2094
  :where(.bp-preflight__gate) {
2095
+ /* position + isolation host the masked hatch ::before (see the shared
2096
+ hatch-mask rule near the overlay scrims) without leaking its negative
2097
+ z-index behind ancestors. The crisp mask replaces the diagonal
2098
+ repeating-gradient, which WebKit and Chromium anti-alias differently. */
2099
+ position: relative;
2100
+ isolation: isolate;
2064
2101
  margin: var(--bp-space-3);
2065
2102
  border: 1px dashed var(--bp-ink-line);
2066
2103
  border-radius: var(--bp-radius-0);
2067
2104
  padding: var(--bp-space-4);
2068
2105
  text-align: center;
2069
- background-image: var(--bp-hatch);
2070
2106
  color: var(--bp-text-secondary);
2071
2107
  }
2072
2108
  :where(.bp-preflight__gate-ready) {
@@ -3243,9 +3279,75 @@
3243
3279
  display: grid;
3244
3280
  place-items: center;
3245
3281
  padding: clamp(var(--bp-space-4), 4vw, var(--bp-space-7));
3246
- background-color: var(--bp-scrim);
3247
- background-image: var(--bp-hatch);
3282
+ /* Fixed fullscreen overlays need BOTH Safari mitigations:
3283
+ 1. the wash stays on the host as --bp-scrim-fill so WebKit paints it;
3284
+ 2. the hatch moves to a masked pseudo-element so the repeating gradient
3285
+ itself is not rasterized on the fixed layer. */
3286
+ background-image: var(--bp-scrim-fill);
3287
+ isolation: isolate;
3288
+ }
3289
+ :where(.bp-mock.is-expanded, .bp-lightbox)::before {
3290
+ content: "";
3291
+ position: absolute;
3292
+ inset: 0;
3293
+ z-index: -1;
3294
+ pointer-events: none;
3295
+ background-color: var(--bp-ink-faint);
3296
+ -webkit-mask-image: var(--bp-hatch-mask);
3297
+ mask-image: var(--bp-hatch-mask);
3298
+ -webkit-mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
3299
+ mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
3300
+ -webkit-mask-repeat: repeat;
3301
+ mask-repeat: repeat;
3302
+ -webkit-mask-position: 0 0;
3303
+ mask-position: 0 0;
3304
+ }
3305
+
3306
+ /* ---- Non-fixed hatch surfaces: crisp masked hatch -----------------
3307
+ The invariant callout, preflight gate, empty state, and rejected choice
3308
+ cards/frames used to paint the diagonal hatch as
3309
+ `background-image: var(--bp-hatch)`.
3310
+ WebKit and Chromium anti-alias that 45deg hard-edged repeating gradient
3311
+ differently — the cross-engine pixel diff showed thousands of low-delta
3312
+ fringe pixels along every stroke. The same crisp SVG mask used on the
3313
+ fixed overlay scrims fixes it deterministically: a 7x7 crispEdges stripe
3314
+ (--bp-hatch-mask) tiled over a flat --bp-hatch-ink fill, so both engines
3315
+ rasterize identical on/off pixels. --bp-hatch-ink tracks the gradient's
3316
+ per-theme line color (ink-faint light / edge dark) so the swap is color
3317
+ faithful. Each host sets position + isolation (see those rules) so this
3318
+ negative-z-index ::before paints above the host fill and below content
3319
+ without escaping the card. */
3320
+ :where(
3321
+ .bp-callout--invariant,
3322
+ .bp-preflight__gate,
3323
+ .wp-empty,
3324
+ .bp-choice--stack:has(.bp-choice__pick:checked)
3325
+ .bp-choice__card:not(:has(.bp-choice__pick:checked)),
3326
+ .bp-choice--resolved .bp-choice__card:not([data-resolved]),
3327
+ .bp-choice--gallery:has(.bp-choice__pick:checked)
3328
+ .bp-choice__mock:not(:has(.bp-choice__pick:checked))
3329
+ .bp-choice__mock-frame
3330
+ )::before {
3331
+ content: "";
3332
+ position: absolute;
3333
+ inset: 0;
3334
+ z-index: -1;
3335
+ pointer-events: none;
3336
+ background-color: var(--bp-hatch-ink);
3337
+ -webkit-mask-image: var(--bp-hatch-mask);
3338
+ mask-image: var(--bp-hatch-mask);
3339
+ -webkit-mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
3340
+ mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
3341
+ -webkit-mask-repeat: repeat;
3342
+ mask-repeat: repeat;
3343
+ -webkit-mask-position: 0 0;
3344
+ mask-position: 0 0;
3248
3345
  }
3346
+ /* Resolved preflight clears its gate hatch (gate-ready copy replaces it). */
3347
+ :where(.bp-preflight--ready) :where(.bp-preflight__gate)::before {
3348
+ content: none;
3349
+ }
3350
+
3249
3351
  :where(.bp-mock.is-expanded) :where(.bp-mock__frame) {
3250
3352
  width: min(100%, 1120px);
3251
3353
  max-height: 100%;
@@ -3385,8 +3487,8 @@
3385
3487
  justify-items: center;
3386
3488
  gap: var(--bp-space-3);
3387
3489
  padding: var(--bp-lightbox-pad);
3388
- background-color: var(--bp-scrim);
3389
- background-image: var(--bp-hatch);
3490
+ background-image: var(--bp-scrim-fill);
3491
+ isolation: isolate;
3390
3492
  }
3391
3493
  :where(.bp-lightbox.is-open) {
3392
3494
  display: grid;
@@ -4155,10 +4257,18 @@
4155
4257
  height: 26px;
4156
4258
  background: var(--bp-fill-amb);
4157
4259
  border-radius: var(--bp-radius-0);
4260
+ /* Explicit four-stop syntax: WebKit mis-resolves the compact
4261
+ multi-position stop shorthand (`color A B`) when A/B are calc()
4262
+ expressions over a custom property (--span), producing sparse or
4263
+ bunched column lines. Splitting each compact stop into two explicit
4264
+ stops removes the shorthand and renders identically in both engines.
4265
+ Same expansion the --bp-hatch token uses. */
4158
4266
  background-image: repeating-linear-gradient(
4159
4267
  to right,
4160
- oklch(0 0 0 / 0) 0 calc(100% / var(--span, 10) - 1px),
4161
- var(--bp-edge) calc(100% / var(--span, 10) - 1px) calc(100% / var(--span, 10))
4268
+ oklch(0 0 0 / 0) 0,
4269
+ oklch(0 0 0 / 0) calc(100% / var(--span, 10) - 1px),
4270
+ var(--bp-edge) calc(100% / var(--span, 10) - 1px),
4271
+ var(--bp-edge) calc(100% / var(--span, 10))
4162
4272
  );
4163
4273
  }
4164
4274
  :where(.wp-gantt__bar) {
@@ -4221,7 +4331,10 @@
4221
4331
  border: 1px dashed var(--bp-ink-line);
4222
4332
  border-radius: var(--bp-radius-0);
4223
4333
  background-color: var(--bp-fill-amb);
4224
- background-image: var(--bp-hatch);
4334
+ /* Hatch via the crisp masked ::before instead of the diagonal gradient
4335
+ (cross-engine AA parity). position + isolation host it. */
4336
+ position: relative;
4337
+ isolation: isolate;
4225
4338
  color: var(--bp-text-secondary);
4226
4339
  }
4227
4340
  :where(.wp-empty__art) { color: var(--bp-ink-soft); margin-bottom: var(--bp-space-1); }
@@ -4314,17 +4427,19 @@
4314
4427
  background-color: var(--bp-paper);
4315
4428
  background-image: linear-gradient(var(--bp-fill-amb), var(--bp-fill-amb));
4316
4429
  }
4317
- /* Rejected / set-aside cards use opacity, so the whole card (incl. its
4318
- hatch) goes translucent and the board bleeds through. Keep the set-aside
4319
- read via hatch + tag, not element opacity. */
4430
+ /* Rejected / set-aside cards stay fully opaque on the board (opacity:1),
4431
+ so the set-aside read comes from the hatch + tag, not element opacity.
4432
+ The hatch is painted by the crisp masked ::before (shared hatch-mask
4433
+ rule) rather than the diagonal gradient — isolation keeps that ::before's
4434
+ negative z-index inside the card. */
4320
4435
  :where(
4321
4436
  .bp-choice--stack:has(.bp-choice__pick:checked)
4322
4437
  .bp-choice__card:not(:has(.bp-choice__pick:checked)),
4323
4438
  .bp-choice--resolved .bp-choice__card:not([data-resolved])
4324
4439
  ) {
4325
4440
  opacity: 1;
4441
+ isolation: isolate;
4326
4442
  background-color: var(--bp-paper);
4327
- background-image: var(--bp-hatch);
4328
4443
  }
4329
4444
  :where(
4330
4445
  .bp-choice--gallery:has(.bp-choice__pick:checked)
@@ -4337,8 +4452,9 @@
4337
4452
  .bp-choice__mock:not(:has(.bp-choice__pick:checked))
4338
4453
  .bp-choice__mock-frame
4339
4454
  ) {
4455
+ position: relative;
4456
+ isolation: isolate;
4340
4457
  background-color: var(--bp-paper);
4341
- background-image: var(--bp-hatch);
4342
4458
  }
4343
4459
 
4344
4460
  /* The compare disclosure sits between transparent siblings (gallery /
@@ -5075,8 +5191,11 @@ body > .site-nav .site-nav__theme:hover {
5075
5191
  ===================================================================== */
5076
5192
 
5077
5193
  /* Modal scrim. Hidden until .is-open; centers the panel near the top of the
5078
- viewport, the way command palettes sit. Sits above all docs chrome. Uses
5079
- the same drafting scrim as .bp-lightbox (--bp-scrim + --bp-hatch). */
5194
+ viewport, the way command palettes sit. Sits above all docs chrome. Uses the
5195
+ same drafting scrim as .bp-lightbox: the wash paints on the host as
5196
+ --bp-scrim-fill while the diagonal hatch sits on a masked pseudo-element.
5197
+ This keeps both Safari fixes alive: WebKit paints the solid wash reliably,
5198
+ and the repeating hatch is not rasterized directly on the fixed layer. */
5080
5199
  .docs-search-overlay {
5081
5200
  position: fixed;
5082
5201
  inset: 0;
@@ -5086,8 +5205,24 @@ body > .site-nav .site-nav__theme:hover {
5086
5205
  align-items: flex-start;
5087
5206
  padding: clamp(var(--bp-space-5), 12vh, var(--bp-space-7)) var(--bp-space-4)
5088
5207
  var(--bp-space-4);
5089
- background-color: var(--bp-scrim);
5090
- background-image: var(--bp-hatch);
5208
+ background-image: var(--bp-scrim-fill);
5209
+ isolation: isolate;
5210
+ }
5211
+ .docs-search-overlay::before {
5212
+ content: "";
5213
+ position: absolute;
5214
+ inset: 0;
5215
+ z-index: -1;
5216
+ pointer-events: none;
5217
+ background-color: var(--bp-ink-faint);
5218
+ -webkit-mask-image: var(--bp-hatch-mask);
5219
+ mask-image: var(--bp-hatch-mask);
5220
+ -webkit-mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
5221
+ mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
5222
+ -webkit-mask-repeat: repeat;
5223
+ mask-repeat: repeat;
5224
+ -webkit-mask-position: 0 0;
5225
+ mask-position: 0 0;
5091
5226
  }
5092
5227
  .docs-search-overlay.is-open {
5093
5228
  display: flex;
@@ -181,9 +181,21 @@
181
181
  --bp-hatch-gap: 7px;
182
182
  --bp-hatch: repeating-linear-gradient(
183
183
  -45deg,
184
- var(--bp-ink-faint) 0 var(--bp-stroke),
185
- oklch(0 0 0 / 0) var(--bp-stroke) var(--bp-hatch-gap)
184
+ var(--bp-ink-faint) 0,
185
+ var(--bp-ink-faint) var(--bp-stroke),
186
+ oklch(0 0 0 / 0) var(--bp-stroke),
187
+ oklch(0 0 0 / 0) var(--bp-hatch-gap)
186
188
  );
189
+ /* Fixed fullscreen scrims in WebKit rasterize more predictably when the
190
+ hatch is tiled through a mask on a pseudo-element instead of painted
191
+ directly as a repeating gradient on the fixed layer itself. */
192
+ --bp-hatch-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 7 7'%3E%3Cpath d='M-1 8L8 -1' stroke='%23000' stroke-width='1' stroke-linecap='square' shape-rendering='crispEdges'/%3E%3C/svg%3E");
193
+ /* Hatch line color for the masked ::before treatment used on non-fixed
194
+ hatch surfaces (preflight gate, empty state, rejected choice cards).
195
+ Tracks the --bp-hatch gradient line color per theme (ink-faint in
196
+ light, edge in dark) so swapping the soft diagonal gradient for the
197
+ crisp deterministic mask never shifts the rendered color. */
198
+ --bp-hatch-ink: var(--bp-ink-faint);
187
199
  /* Drafting board — a faint dot-grid backdrop that marks INTERACTIVE
188
200
  component surfaces (choice, preflight, choice-record, source, mock,
189
201
  gallery): a second axis distinct from the diagonal --bp-hatch (which
@@ -216,11 +228,16 @@
216
228
  --bp-board-card-pad: var(--bp-space-4);
217
229
  --bp-board-card-radius: var(--bp-radius-8);
218
230
  /* Modal scrim wash — darker than --bp-ink-soft so floated panels (search
219
- palette, lightbox, expanded mock) read clearly over the page. Always
220
- pair with --bp-hatch:
221
- background-color: var(--bp-scrim);
222
- background-image: var(--bp-hatch); */
231
+ palette, lightbox, expanded mock) read clearly over the page. The wash
232
+ is painted as a SOLID background-image layer (--bp-scrim-fill), not as
233
+ background-color: WebKit/Safari can drop a semi-transparent oklch
234
+ background-color while still painting gradient background-image layers,
235
+ which left the wash faint/absent there while the hatch texture kept
236
+ drawing. Fixed fullscreen overlays pair this fill on the host with the
237
+ diagonal hatch on a masked pseudo-element (see --bp-hatch-mask) so the
238
+ repeating gradient itself is not rasterized on the fixed layer. */
223
239
  --bp-scrim: oklch(0 0 0 / 0.58);
240
+ --bp-scrim-fill: linear-gradient(var(--bp-scrim), var(--bp-scrim));
224
241
  --bp-edge: var(--obvious-border-subtle, oklch(0 0 0 / 0.08));
225
242
  --bp-ink-line: var(--obvious-border-default, oklch(0 0 0 / 0.16));
226
243
  --bp-text: var(--obvious-text-primary, oklch(0 0 0 / 0.92));
@@ -437,9 +454,14 @@
437
454
  rejected-choice fills keep texture without brightening the wash. */
438
455
  --bp-hatch: repeating-linear-gradient(
439
456
  -45deg,
440
- var(--bp-edge) 0 var(--bp-stroke),
441
- oklch(0 0 0 / 0) var(--bp-stroke) var(--bp-hatch-gap)
457
+ var(--bp-edge) 0,
458
+ var(--bp-edge) var(--bp-stroke),
459
+ oklch(0 0 0 / 0) var(--bp-stroke),
460
+ oklch(0 0 0 / 0) var(--bp-hatch-gap)
442
461
  );
462
+ /* Dark hatch line color = --bp-edge (matches the dark --bp-hatch gradient
463
+ above), keeping the masked ::before faithful to the gradient in dark. */
464
+ --bp-hatch-ink: var(--bp-edge);
443
465
  --bp-paper: var(--bp-gray-950);
444
466
  --bp-bg: oklch(0.15 0 0); /* the desk sits darker than the sheet, so the page lifts */
445
467
  --bp-edge: oklch(1 0 0 / 0.08);
@@ -1481,7 +1503,10 @@
1481
1503
  --bp-tick: var(--bp-ink);
1482
1504
  --bp-tick-len: 16px;
1483
1505
  --bp-tick-wt: 2.5px;
1484
- --bp-callout-fill: var(--bp-hatch);
1506
+ /* Hatch is painted by the crisp masked ::before (shared hatch-mask rule),
1507
+ leaving the corner tick stack on the host. */
1508
+ --bp-callout-fill: none;
1509
+ isolation: isolate;
1485
1510
  background-color: var(--bp-fill-hi);
1486
1511
  }
1487
1512
  /* Reference = enumerated values (subdued, soft ticks). */
@@ -1807,7 +1832,10 @@
1807
1832
  }
1808
1833
  :where(.bp-choice--stack:has(.bp-choice__pick:checked)) :where(.bp-choice__card:not(:has(.bp-choice__pick:checked))) {
1809
1834
  opacity: 0.62;
1810
- background-image: var(--bp-hatch);
1835
+ /* Hatch is painted by the crisp masked ::before (shared hatch-mask rule),
1836
+ not a diagonal gradient — the gradient anti-aliases differently in
1837
+ WebKit vs Chromium. The card is already position:relative. */
1838
+ isolation: isolate;
1811
1839
  }
1812
1840
  :where(.bp-choice--stack:has(.bp-choice__pick:checked)) :where(.bp-choice__card:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__card-rejected) {
1813
1841
  display: inline-flex;
@@ -1839,7 +1867,8 @@
1839
1867
  }
1840
1868
  :where(.bp-choice--resolved) :where(.bp-choice__card:not([data-resolved])) {
1841
1869
  opacity: 0.62;
1842
- background-image: var(--bp-hatch);
1870
+ /* Crisp masked ::before hatch (cross-engine parity), see hatch-mask rule. */
1871
+ isolation: isolate;
1843
1872
  }
1844
1873
  :where(.bp-choice--resolved) :where(.bp-choice__card:not([data-resolved])) :where(.bp-choice__card-rejected) {
1845
1874
  display: inline-flex;
@@ -1903,7 +1932,9 @@
1903
1932
  opacity: 0.5;
1904
1933
  }
1905
1934
  :where(.bp-choice--gallery:has(.bp-choice__pick:checked)) :where(.bp-choice__mock:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__mock-frame) {
1906
- background-image: var(--bp-hatch);
1935
+ /* Crisp masked ::before hatch (cross-engine parity), see hatch-mask rule. */
1936
+ position: relative;
1937
+ isolation: isolate;
1907
1938
  }
1908
1939
  :where(.bp-choice--gallery:has(.bp-choice__pick:checked)) :where(.bp-choice__mock:not(:has(.bp-choice__pick:checked))) :where(.bp-choice__mock-rejected) {
1909
1940
  display: inline-flex;
@@ -2061,12 +2092,17 @@
2061
2092
  accent-color: var(--bp-button-primary-fg);
2062
2093
  }
2063
2094
  :where(.bp-preflight__gate) {
2095
+ /* position + isolation host the masked hatch ::before (see the shared
2096
+ hatch-mask rule near the overlay scrims) without leaking its negative
2097
+ z-index behind ancestors. The crisp mask replaces the diagonal
2098
+ repeating-gradient, which WebKit and Chromium anti-alias differently. */
2099
+ position: relative;
2100
+ isolation: isolate;
2064
2101
  margin: var(--bp-space-3);
2065
2102
  border: 1px dashed var(--bp-ink-line);
2066
2103
  border-radius: var(--bp-radius-0);
2067
2104
  padding: var(--bp-space-4);
2068
2105
  text-align: center;
2069
- background-image: var(--bp-hatch);
2070
2106
  color: var(--bp-text-secondary);
2071
2107
  }
2072
2108
  :where(.bp-preflight__gate-ready) {
@@ -3243,9 +3279,75 @@
3243
3279
  display: grid;
3244
3280
  place-items: center;
3245
3281
  padding: clamp(var(--bp-space-4), 4vw, var(--bp-space-7));
3246
- background-color: var(--bp-scrim);
3247
- background-image: var(--bp-hatch);
3282
+ /* Fixed fullscreen overlays need BOTH Safari mitigations:
3283
+ 1. the wash stays on the host as --bp-scrim-fill so WebKit paints it;
3284
+ 2. the hatch moves to a masked pseudo-element so the repeating gradient
3285
+ itself is not rasterized on the fixed layer. */
3286
+ background-image: var(--bp-scrim-fill);
3287
+ isolation: isolate;
3288
+ }
3289
+ :where(.bp-mock.is-expanded, .bp-lightbox)::before {
3290
+ content: "";
3291
+ position: absolute;
3292
+ inset: 0;
3293
+ z-index: -1;
3294
+ pointer-events: none;
3295
+ background-color: var(--bp-ink-faint);
3296
+ -webkit-mask-image: var(--bp-hatch-mask);
3297
+ mask-image: var(--bp-hatch-mask);
3298
+ -webkit-mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
3299
+ mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
3300
+ -webkit-mask-repeat: repeat;
3301
+ mask-repeat: repeat;
3302
+ -webkit-mask-position: 0 0;
3303
+ mask-position: 0 0;
3304
+ }
3305
+
3306
+ /* ---- Non-fixed hatch surfaces: crisp masked hatch -----------------
3307
+ The invariant callout, preflight gate, empty state, and rejected choice
3308
+ cards/frames used to paint the diagonal hatch as
3309
+ `background-image: var(--bp-hatch)`.
3310
+ WebKit and Chromium anti-alias that 45deg hard-edged repeating gradient
3311
+ differently — the cross-engine pixel diff showed thousands of low-delta
3312
+ fringe pixels along every stroke. The same crisp SVG mask used on the
3313
+ fixed overlay scrims fixes it deterministically: a 7x7 crispEdges stripe
3314
+ (--bp-hatch-mask) tiled over a flat --bp-hatch-ink fill, so both engines
3315
+ rasterize identical on/off pixels. --bp-hatch-ink tracks the gradient's
3316
+ per-theme line color (ink-faint light / edge dark) so the swap is color
3317
+ faithful. Each host sets position + isolation (see those rules) so this
3318
+ negative-z-index ::before paints above the host fill and below content
3319
+ without escaping the card. */
3320
+ :where(
3321
+ .bp-callout--invariant,
3322
+ .bp-preflight__gate,
3323
+ .wp-empty,
3324
+ .bp-choice--stack:has(.bp-choice__pick:checked)
3325
+ .bp-choice__card:not(:has(.bp-choice__pick:checked)),
3326
+ .bp-choice--resolved .bp-choice__card:not([data-resolved]),
3327
+ .bp-choice--gallery:has(.bp-choice__pick:checked)
3328
+ .bp-choice__mock:not(:has(.bp-choice__pick:checked))
3329
+ .bp-choice__mock-frame
3330
+ )::before {
3331
+ content: "";
3332
+ position: absolute;
3333
+ inset: 0;
3334
+ z-index: -1;
3335
+ pointer-events: none;
3336
+ background-color: var(--bp-hatch-ink);
3337
+ -webkit-mask-image: var(--bp-hatch-mask);
3338
+ mask-image: var(--bp-hatch-mask);
3339
+ -webkit-mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
3340
+ mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
3341
+ -webkit-mask-repeat: repeat;
3342
+ mask-repeat: repeat;
3343
+ -webkit-mask-position: 0 0;
3344
+ mask-position: 0 0;
3248
3345
  }
3346
+ /* Resolved preflight clears its gate hatch (gate-ready copy replaces it). */
3347
+ :where(.bp-preflight--ready) :where(.bp-preflight__gate)::before {
3348
+ content: none;
3349
+ }
3350
+
3249
3351
  :where(.bp-mock.is-expanded) :where(.bp-mock__frame) {
3250
3352
  width: min(100%, 1120px);
3251
3353
  max-height: 100%;
@@ -3385,8 +3487,8 @@
3385
3487
  justify-items: center;
3386
3488
  gap: var(--bp-space-3);
3387
3489
  padding: var(--bp-lightbox-pad);
3388
- background-color: var(--bp-scrim);
3389
- background-image: var(--bp-hatch);
3490
+ background-image: var(--bp-scrim-fill);
3491
+ isolation: isolate;
3390
3492
  }
3391
3493
  :where(.bp-lightbox.is-open) {
3392
3494
  display: grid;
@@ -4155,10 +4257,18 @@
4155
4257
  height: 26px;
4156
4258
  background: var(--bp-fill-amb);
4157
4259
  border-radius: var(--bp-radius-0);
4260
+ /* Explicit four-stop syntax: WebKit mis-resolves the compact
4261
+ multi-position stop shorthand (`color A B`) when A/B are calc()
4262
+ expressions over a custom property (--span), producing sparse or
4263
+ bunched column lines. Splitting each compact stop into two explicit
4264
+ stops removes the shorthand and renders identically in both engines.
4265
+ Same expansion the --bp-hatch token uses. */
4158
4266
  background-image: repeating-linear-gradient(
4159
4267
  to right,
4160
- oklch(0 0 0 / 0) 0 calc(100% / var(--span, 10) - 1px),
4161
- var(--bp-edge) calc(100% / var(--span, 10) - 1px) calc(100% / var(--span, 10))
4268
+ oklch(0 0 0 / 0) 0,
4269
+ oklch(0 0 0 / 0) calc(100% / var(--span, 10) - 1px),
4270
+ var(--bp-edge) calc(100% / var(--span, 10) - 1px),
4271
+ var(--bp-edge) calc(100% / var(--span, 10))
4162
4272
  );
4163
4273
  }
4164
4274
  :where(.wp-gantt__bar) {
@@ -4221,7 +4331,10 @@
4221
4331
  border: 1px dashed var(--bp-ink-line);
4222
4332
  border-radius: var(--bp-radius-0);
4223
4333
  background-color: var(--bp-fill-amb);
4224
- background-image: var(--bp-hatch);
4334
+ /* Hatch via the crisp masked ::before instead of the diagonal gradient
4335
+ (cross-engine AA parity). position + isolation host it. */
4336
+ position: relative;
4337
+ isolation: isolate;
4225
4338
  color: var(--bp-text-secondary);
4226
4339
  }
4227
4340
  :where(.wp-empty__art) { color: var(--bp-ink-soft); margin-bottom: var(--bp-space-1); }
@@ -4314,17 +4427,19 @@
4314
4427
  background-color: var(--bp-paper);
4315
4428
  background-image: linear-gradient(var(--bp-fill-amb), var(--bp-fill-amb));
4316
4429
  }
4317
- /* Rejected / set-aside cards use opacity, so the whole card (incl. its
4318
- hatch) goes translucent and the board bleeds through. Keep the set-aside
4319
- read via hatch + tag, not element opacity. */
4430
+ /* Rejected / set-aside cards stay fully opaque on the board (opacity:1),
4431
+ so the set-aside read comes from the hatch + tag, not element opacity.
4432
+ The hatch is painted by the crisp masked ::before (shared hatch-mask
4433
+ rule) rather than the diagonal gradient — isolation keeps that ::before's
4434
+ negative z-index inside the card. */
4320
4435
  :where(
4321
4436
  .bp-choice--stack:has(.bp-choice__pick:checked)
4322
4437
  .bp-choice__card:not(:has(.bp-choice__pick:checked)),
4323
4438
  .bp-choice--resolved .bp-choice__card:not([data-resolved])
4324
4439
  ) {
4325
4440
  opacity: 1;
4441
+ isolation: isolate;
4326
4442
  background-color: var(--bp-paper);
4327
- background-image: var(--bp-hatch);
4328
4443
  }
4329
4444
  :where(
4330
4445
  .bp-choice--gallery:has(.bp-choice__pick:checked)
@@ -4337,8 +4452,9 @@
4337
4452
  .bp-choice__mock:not(:has(.bp-choice__pick:checked))
4338
4453
  .bp-choice__mock-frame
4339
4454
  ) {
4455
+ position: relative;
4456
+ isolation: isolate;
4340
4457
  background-color: var(--bp-paper);
4341
- background-image: var(--bp-hatch);
4342
4458
  }
4343
4459
 
4344
4460
  /* The compare disclosure sits between transparent siblings (gallery /
@@ -5075,8 +5191,11 @@ body > .site-nav .site-nav__theme:hover {
5075
5191
  ===================================================================== */
5076
5192
 
5077
5193
  /* Modal scrim. Hidden until .is-open; centers the panel near the top of the
5078
- viewport, the way command palettes sit. Sits above all docs chrome. Uses
5079
- the same drafting scrim as .bp-lightbox (--bp-scrim + --bp-hatch). */
5194
+ viewport, the way command palettes sit. Sits above all docs chrome. Uses the
5195
+ same drafting scrim as .bp-lightbox: the wash paints on the host as
5196
+ --bp-scrim-fill while the diagonal hatch sits on a masked pseudo-element.
5197
+ This keeps both Safari fixes alive: WebKit paints the solid wash reliably,
5198
+ and the repeating hatch is not rasterized directly on the fixed layer. */
5080
5199
  .docs-search-overlay {
5081
5200
  position: fixed;
5082
5201
  inset: 0;
@@ -5086,8 +5205,24 @@ body > .site-nav .site-nav__theme:hover {
5086
5205
  align-items: flex-start;
5087
5206
  padding: clamp(var(--bp-space-5), 12vh, var(--bp-space-7)) var(--bp-space-4)
5088
5207
  var(--bp-space-4);
5089
- background-color: var(--bp-scrim);
5090
- background-image: var(--bp-hatch);
5208
+ background-image: var(--bp-scrim-fill);
5209
+ isolation: isolate;
5210
+ }
5211
+ .docs-search-overlay::before {
5212
+ content: "";
5213
+ position: absolute;
5214
+ inset: 0;
5215
+ z-index: -1;
5216
+ pointer-events: none;
5217
+ background-color: var(--bp-ink-faint);
5218
+ -webkit-mask-image: var(--bp-hatch-mask);
5219
+ mask-image: var(--bp-hatch-mask);
5220
+ -webkit-mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
5221
+ mask-size: var(--bp-hatch-gap) var(--bp-hatch-gap);
5222
+ -webkit-mask-repeat: repeat;
5223
+ mask-repeat: repeat;
5224
+ -webkit-mask-position: 0 0;
5225
+ mask-position: 0 0;
5091
5226
  }
5092
5227
  .docs-search-overlay.is-open {
5093
5228
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obvi/blueprint",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A classless-first CSS design system for beautiful technical blueprint documents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,7 +45,11 @@
45
45
  "build": "npm run build:code && node scripts/build-package.mjs",
46
46
  "build:code": "node code-highlighting/scripts/build.mjs",
47
47
  "render": "node harness/render.mjs",
48
- "measure": "node harness/measure.mjs && node harness/behavior.mjs && node harness/navigation-clipping.mjs",
48
+ "measure": "node harness/measure.mjs && node harness/behavior.mjs && node harness/navigation-clipping.mjs && node harness/scrim.mjs && node harness/hatch.mjs && node harness/browser-diff.mjs",
49
+ "measure:visual": "node harness/browser-diff.mjs",
50
+ "measure:docs": "node harness/docs-diff.mjs",
51
+ "scrim": "node harness/scrim.mjs",
52
+ "hatch": "node harness/hatch.mjs",
49
53
  "verify:package": "node scripts/verify-package.mjs",
50
54
  "verify:release-evidence": "node --test harness/release-evidence.test.mjs scripts/verify-published-package.test.mjs",
51
55
  "verify:published": "node scripts/verify-published-package.mjs",
@@ -57,7 +61,10 @@
57
61
  "devDependencies": {
58
62
  "@types/prismjs": "^1.26.5",
59
63
  "esbuild": "^0.28.1",
64
+ "pixelmatch": "^7.1.0",
60
65
  "prismjs": "^1.30.0",
66
+ "pngjs": "^7.0.0",
67
+ "playwright": "^1.61.1",
61
68
  "puppeteer": "^22.0.0"
62
69
  }
63
70
  }