@obvi/blueprint 1.0.10 → 1.1.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.
@@ -182,8 +182,14 @@
182
182
  --bp-hatch: repeating-linear-gradient(
183
183
  -45deg,
184
184
  var(--bp-ink-faint) 0 var(--bp-stroke),
185
- transparent var(--bp-stroke) var(--bp-hatch-gap)
185
+ oklch(0 0 0 / 0) var(--bp-stroke) var(--bp-hatch-gap)
186
186
  );
187
+ /* Modal scrim wash — darker than --bp-ink-soft so floated panels (search
188
+ palette, lightbox, expanded mock) read clearly over the page. Always
189
+ pair with --bp-hatch:
190
+ background-color: var(--bp-scrim);
191
+ background-image: var(--bp-hatch); */
192
+ --bp-scrim: oklch(0 0 0 / 0.58);
187
193
  --bp-edge: var(--obvious-border-subtle, oklch(0 0 0 / 0.08));
188
194
  --bp-ink-line: var(--obvious-border-default, oklch(0 0 0 / 0.16));
189
195
  --bp-text: var(--obvious-text-primary, oklch(0 0 0 / 0.92));
@@ -322,6 +328,7 @@
322
328
  --bp-duration-normal: 200ms;
323
329
  --bp-duration-slow: 300ms;
324
330
  --bp-duration-deliberate: 450ms;
331
+ --bp-duration-spin: 1.4s; /* constant rotation (marquees, loading glyphs) */
325
332
 
326
333
  /* Easing — strong curves only; never ease-in for UI (its slow start delays
327
334
  feedback and feels sluggish).
@@ -358,6 +365,7 @@
358
365
  /* Neutral white-alpha panel fills over the dark paper. */
359
366
  --bp-fill-amb: oklch(1 0 0 / 0.04);
360
367
  --bp-fill-hi: oklch(1 0 0 / 0.08);
368
+ --bp-scrim: oklch(1 0 0 / 0.58);
361
369
  --bp-paper: var(--bp-gray-950);
362
370
  --bp-bg: oklch(0.15 0 0); /* the desk sits darker than the sheet, so the page lifts */
363
371
  --bp-edge: oklch(1 0 0 / 0.08);
@@ -430,6 +438,19 @@
430
438
  -moz-osx-font-smoothing: grayscale;
431
439
  counter-reset: bp-sec bp-fig bp-fn;
432
440
  }
441
+
442
+ /* Theme crossfade — View Transitions capture one old/new snapshot of the
443
+ document and the compositor crossfades them in a single pass. Per-element
444
+ color transitions on a token flip fight the cascade (thousands of
445
+ mismatched 150ms/200ms tweens, pseudo-elements snapping mid-fade, and
446
+ transitions cut off when an animate flag clears). blueprint.js wraps theme
447
+ flips in startViewTransition(); reduced motion is handled globally in
448
+ @layer reset. Pseudo-elements can't live in :where(); same rule here. */
449
+ ::view-transition-old(root),
450
+ ::view-transition-new(root) {
451
+ animation-duration: var(--bp-duration-normal);
452
+ animation-timing-function: var(--bp-ease-out);
453
+ }
433
454
  /* Pseudo-elements can't live inside :where()/:is() — Chromium (incl. Dia)
434
455
  and Firefox drop the whole rule. Keep these as plain selectors, and
435
456
  split ::selection / ::-moz-selection so one invalid token doesn't
@@ -451,8 +472,8 @@
451
472
  padding: var(--bp-space-6);
452
473
  }
453
474
 
454
- /* Section rhythm + automatic "01 / 02" eyebrow counter before each h2.
455
- The counter is keyed to <section>, so reordering renumbers for free. */
475
+ /* Section rhythm primary section eyebrows are injected by blueprint.js
476
+ on h2[data-sidebar]; nested sections without data-sidebar stay plain. */
456
477
  :where(section) {
457
478
  counter-increment: bp-sec;
458
479
  }
@@ -470,6 +491,38 @@
470
491
  margin-bottom: var(--bp-space-2);
471
492
  text-wrap: var(--bp-wrap-heading);
472
493
  }
494
+ /* Document masthead — the first <header> in <main>. No implicit, position-
495
+ based styling: author the eyebrow with <bp-eyebrow> and the lede with
496
+ <bp-subheader> explicitly. We only reset the title's top margin and tune
497
+ the gap before a following contents index. */
498
+ :where(main > header:first-child) {
499
+ margin-bottom: 0;
500
+ }
501
+ :where(main > header:first-child > h1) {
502
+ margin: 0 0 var(--bp-space-2);
503
+ }
504
+ :where(main > header:first-child:has(+ :is(.bp-contents, bp-toc))) {
505
+ margin-bottom: var(--bp-space-4);
506
+ }
507
+ :where(main > header:first-child + :is(.bp-contents, bp-toc)) {
508
+ margin-top: 0;
509
+ }
510
+ /* Navigation GROUP divider — every repeated <header> after the masthead.
511
+ It reuses the masthead structure (eyebrow + h1 + optional <bp-subheader>)
512
+ and reads as a calm "part" break: a hairline above and generous rhythm,
513
+ so the document splits into labelled groups that the runtime mirrors as
514
+ eyebrows in the contents rail. */
515
+ :where(main > header:not(:first-child)) {
516
+ margin: calc(var(--bp-space-7) + var(--bp-space-5)) 0 0;
517
+ padding-top: var(--bp-space-6);
518
+ border-top: 1px solid var(--bp-edge);
519
+ }
520
+ :where(main > header:not(:first-child) > h1) {
521
+ margin: 0 0 var(--bp-space-2);
522
+ }
523
+ :where(bp-toc) {
524
+ display: block;
525
+ }
473
526
  :where(h2) {
474
527
  font-family: var(--bp-sans);
475
528
  font-size: var(--bp-text-h2);
@@ -480,12 +533,10 @@
480
533
  scroll-margin-top: var(--bp-space-4);
481
534
  text-wrap: var(--bp-wrap-heading);
482
535
  }
483
- /* Section-scoped eyebrow: "01", "02"… rendered above each section's h2.
484
- A solid ink square leads the counter, set off by --bp-marker-gap. The
485
- square is a left-anchored background so it stays a single pseudo-element
486
- (no extra node) and centers vertically against the label line. */
487
- :where(section > h2:first-child)::before,
488
- :where(section > hgroup:first-child > h2)::before {
536
+ /* Legacy section-scoped eyebrow for pages without blueprint.js. Authored
537
+ h2[data-sidebar] headings get .bp-heading-eyebrow from the runtime. */
538
+ :where(section > h2:first-child:not([data-sidebar]):not(:has(.bp-heading-eyebrow)))::before,
539
+ :where(section > hgroup:first-child > h2:not([data-sidebar]):not(:has(.bp-heading-eyebrow)))::before {
489
540
  content: counter(bp-sec, decimal-leading-zero);
490
541
  display: block;
491
542
  width: max-content;
@@ -539,8 +590,8 @@
539
590
  text-wrap: var(--bp-wrap-body);
540
591
  }
541
592
  /* Lede-by-position was removed — a paragraph following a heading should
542
- render as ordinary body type. Use .bp-lede explicitly when you want
543
- the narrative lead-in treatment. */
593
+ render as ordinary body type. Use <bp-subheader> explicitly when you
594
+ want the narrative lead-in treatment. */
544
595
  :where(ul, ol) {
545
596
  margin: 0 0 1.2em;
546
597
  padding-left: 1.3em;
@@ -785,7 +836,7 @@
785
836
  background: var(--bp-fill-amb);
786
837
  padding: 2px 5px;
787
838
  color: var(--bp-ink);
788
- border-radius: var(--bp-radius-2);
839
+ border-radius: var(--bp-radius-0);
789
840
  }
790
841
  :where(pre) {
791
842
  font-family: var(--bp-mono);
@@ -810,7 +861,7 @@
810
861
  background: var(--bp-paper);
811
862
  border: 1px solid var(--bp-ink-line);
812
863
  border-bottom-width: 2px;
813
- border-radius: var(--bp-radius-2);
864
+ border-radius: var(--bp-radius-0);
814
865
  padding: 1px 5px;
815
866
  color: var(--bp-text);
816
867
  }
@@ -944,13 +995,22 @@
944
995
  :where(summary)::-webkit-details-marker {
945
996
  display: none;
946
997
  }
998
+ /* Disclosure caret — Iconoir nav-arrow-right, inlined as a mask so every
999
+ native <summary> stays JS-free and inherits ink via currentColor. */
947
1000
  :where(summary)::before {
948
1001
  content: "";
949
- width: 0;
950
- height: 0;
951
- border-left: 5px solid var(--bp-ink);
952
- border-top: 4px solid transparent;
953
- border-bottom: 4px solid transparent;
1002
+ flex-shrink: 0;
1003
+ width: 16px;
1004
+ height: 16px;
1005
+ background-color: var(--bp-ink);
1006
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 6L15 12L9 18'/%3E%3C/svg%3E");
1007
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 6L15 12L9 18'/%3E%3C/svg%3E");
1008
+ -webkit-mask-size: contain;
1009
+ mask-size: contain;
1010
+ -webkit-mask-repeat: no-repeat;
1011
+ mask-repeat: no-repeat;
1012
+ -webkit-mask-position: center;
1013
+ mask-position: center;
954
1014
  /* Motion: .bp-transition-transform .bp-ease (disclosure caret rotate) */
955
1015
  transition-property: transform;
956
1016
  transition-duration: var(--bp-duration-fast);
@@ -967,7 +1027,65 @@
967
1027
  :where(a, button, summary, [tabindex]):focus-visible {
968
1028
  outline: 2px solid var(--bp-ink);
969
1029
  outline-offset: 3px;
970
- border-radius: var(--bp-radius-2);
1030
+ border-radius: var(--bp-radius-0);
1031
+ }
1032
+
1033
+ /* Squared checkbox. Native checkboxes render with rounded corners that
1034
+ border-radius cannot override, so reset appearance and draw the box: a
1035
+ paper square with an Iconoir check tick (mask-inlined, same glyph as the
1036
+ heading permalink copied state). Paper fill + ink tick stay legible
1037
+ whether the control sits on paper or an inked chip. Radios keep their
1038
+ native round rendering — a square radio reads as a checkbox. */
1039
+ :where(input[type="checkbox"]) {
1040
+ appearance: none;
1041
+ -webkit-appearance: none;
1042
+ flex: none;
1043
+ display: inline-grid;
1044
+ place-items: center;
1045
+ inline-size: 1em;
1046
+ block-size: 1em;
1047
+ border: 1px solid var(--bp-ink-line);
1048
+ border-radius: var(--bp-radius-0);
1049
+ background: var(--bp-paper);
1050
+ cursor: pointer;
1051
+ /* Motion: .bp-transition-colors (border + fill settle on toggle) */
1052
+ transition-property: border-color, background;
1053
+ transition-duration: var(--bp-duration-fast);
1054
+ transition-timing-function: var(--bp-ease-out);
1055
+ }
1056
+ :where(input[type="checkbox"])::before {
1057
+ content: "";
1058
+ inline-size: 0.7em;
1059
+ block-size: 0.7em;
1060
+ background-color: var(--bp-ink);
1061
+ /* Iconoir check — same glyph as heading permalink copied state. */
1062
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 13L9 17L19 7'/%3E%3C/svg%3E");
1063
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 13L9 17L19 7'/%3E%3C/svg%3E");
1064
+ -webkit-mask-size: contain;
1065
+ mask-size: contain;
1066
+ -webkit-mask-repeat: no-repeat;
1067
+ mask-repeat: no-repeat;
1068
+ -webkit-mask-position: center;
1069
+ mask-position: center;
1070
+ transform: scale(0);
1071
+ transform-origin: center;
1072
+ /* Motion: tick draws in as the box is checked */
1073
+ transition: transform var(--bp-duration-fast) var(--bp-ease-out);
1074
+ }
1075
+ :where(input[type="checkbox"]:checked) {
1076
+ border-color: var(--bp-ink);
1077
+ }
1078
+ :where(input[type="checkbox"]:checked)::before {
1079
+ transform: scale(1);
1080
+ }
1081
+ :where(input[type="checkbox"]:disabled) {
1082
+ opacity: 0.4;
1083
+ cursor: not-allowed;
1084
+ pointer-events: none;
1085
+ }
1086
+ :where(input[type="checkbox"]:focus-visible) {
1087
+ outline: 2px solid var(--bp-ink);
1088
+ outline-offset: 3px;
971
1089
  }
972
1090
  }
973
1091
 
@@ -987,37 +1105,51 @@
987
1105
  ===================================================================== */
988
1106
  @layer components {
989
1107
 
990
- /* ---- Narrative lede (explicit opt-in) ---------------------------
991
- A lede must be asked for — apply .bp-lede to any paragraph
992
- that should read as the narrative lead-in. Lede-by-position (the
993
- old automatic `section > h2 + p` rule) was removed; a paragraph
994
- following a heading is now ordinary body type unless classed. */
995
- :where(.bp-lede) {
1108
+ /* ---- Narrative lede <bp-subheader> ----------------------------
1109
+ A lede must be asked for — wrap a lead-in paragraph in <bp-subheader>
1110
+ so it reads as the narrative opener. Lede-by-position (the old
1111
+ automatic `section > h2 + p` rule) was removed; a paragraph following
1112
+ a heading is ordinary body type unless it is a <bp-subheader>. */
1113
+ :where(bp-subheader) {
1114
+ display: block;
1115
+ margin: 0 0 1.2em;
996
1116
  font-size: var(--bp-text-lede);
997
1117
  line-height: var(--bp-lh-lede);
998
1118
  color: var(--bp-text);
999
1119
  }
1000
1120
 
1001
- /* ---- Section heading permalink ------------------------------------
1002
- Injected by blueprint.js: pixelarticons sit in the main-column gutter
1003
- (left of the title, no text shift). Link on heading hover, copy on icon
1004
- hover, check after a successful copy. */
1005
- :where(h2:has(.bp-heading-row)) {
1121
+ /* ---- Section heading permalink + eyebrow --------------------------------
1122
+ Injected by blueprint.js on headings with data-sidebar: a numbered eyebrow
1123
+ on primary (h2) sections, plus a gutter permalink on every opted-in level.
1124
+ Link on heading hover, copy on icon hover, check after a successful copy. */
1125
+ :where(.bp-heading-eyebrow) {
1126
+ display: block;
1127
+ width: max-content;
1128
+ font-family: var(--bp-mono);
1129
+ font-size: var(--bp-label-lg);
1130
+ font-weight: var(--bp-weight-medium);
1131
+ letter-spacing: 0.18em;
1132
+ color: var(--bp-ink);
1133
+ padding-left: calc(var(--bp-marker-size) + var(--bp-marker-gap));
1134
+ background: linear-gradient(var(--bp-ink), var(--bp-ink)) left center / var(--bp-marker-size) var(--bp-marker-size) no-repeat;
1135
+ margin-bottom: var(--bp-space-2);
1136
+ }
1137
+ :where(:is(h2, h3, h4, h5, h6):has(.bp-heading-row)) {
1006
1138
  position: relative;
1007
1139
  }
1008
1140
  /* Invisible bridge from the title into the gutter so the icon stays
1009
- reachable while the pointer crosses the gap outside h2's border box. */
1010
- :where(h2:has(.bp-heading-row))::after {
1141
+ reachable while the pointer crosses the gap outside the heading box. */
1142
+ :where(:is(h2, h3, h4, h5, h6):has(.bp-heading-row))::after {
1011
1143
  content: "";
1012
1144
  position: absolute;
1013
1145
  left: calc(-1 * (16px + var(--bp-space-2)));
1014
1146
  bottom: 0;
1015
1147
  z-index: 0;
1016
1148
  width: calc(16px + var(--bp-space-2) + var(--bp-space-3));
1017
- height: var(--bp-lh-h2);
1149
+ height: 1.2em;
1018
1150
  pointer-events: none;
1019
1151
  }
1020
- :where(h2:is(:hover, :focus-within, [data-heading-link="visible"]):has(.bp-heading-row))::after {
1152
+ :where(:is(h2, h3, h4, h5, h6):is(:hover, :focus-within, [data-heading-link="visible"]):has(.bp-heading-row))::after {
1021
1153
  pointer-events: auto;
1022
1154
  }
1023
1155
  :where(.bp-heading-row) {
@@ -1043,7 +1175,7 @@
1043
1175
  margin: 0;
1044
1176
  padding: 0;
1045
1177
  border: 0;
1046
- background: transparent;
1178
+ background: oklch(0 0 0 / 0);
1047
1179
  color: var(--bp-ink-soft);
1048
1180
  cursor: pointer;
1049
1181
  opacity: 0;
@@ -1069,21 +1201,20 @@
1069
1201
  inset: 0;
1070
1202
  display: block;
1071
1203
  opacity: 0;
1072
- image-rendering: pixelated;
1073
1204
  transition: opacity var(--bp-duration-fast) var(--bp-ease-out);
1074
1205
  }
1075
- :where(h2:hover .bp-heading-link),
1076
- :where(h2:focus-within .bp-heading-link),
1077
- :where(h2[data-heading-link="visible"] .bp-heading-link),
1206
+ :where(:is(h2, h3, h4, h5, h6):hover .bp-heading-link),
1207
+ :where(:is(h2, h3, h4, h5, h6):focus-within .bp-heading-link),
1208
+ :where(:is(h2, h3, h4, h5, h6)[data-heading-link="visible"] .bp-heading-link),
1078
1209
  :where(button.bp-heading-link:focus-visible),
1079
1210
  :where(button.bp-heading-link[data-bp-copy-state="copied"]),
1080
1211
  :where(button.bp-heading-link[data-bp-copy-state="error"]) {
1081
1212
  opacity: 1;
1082
1213
  pointer-events: auto;
1083
1214
  }
1084
- :where(h2:hover .bp-heading-link__icon--link),
1085
- :where(h2:focus-within .bp-heading-link__icon--link),
1086
- :where(h2[data-heading-link="visible"] .bp-heading-link__icon--link),
1215
+ :where(:is(h2, h3, h4, h5, h6):hover .bp-heading-link__icon--link),
1216
+ :where(:is(h2, h3, h4, h5, h6):focus-within .bp-heading-link__icon--link),
1217
+ :where(:is(h2, h3, h4, h5, h6)[data-heading-link="visible"] .bp-heading-link__icon--link),
1087
1218
  :where(button.bp-heading-link:focus-visible .bp-heading-link__icon--link) {
1088
1219
  opacity: 1;
1089
1220
  }
@@ -1110,14 +1241,19 @@
1110
1241
  :where(button.bp-heading-link:focus-visible) {
1111
1242
  outline: 2px solid var(--bp-ink);
1112
1243
  outline-offset: 2px;
1113
- border-radius: var(--bp-radius-2);
1244
+ border-radius: var(--bp-radius-0);
1114
1245
  }
1115
1246
  :where(button.bp-heading-link[data-bp-copy-state="error"]) {
1116
1247
  color: var(--bp-ink-soft);
1117
1248
  }
1118
1249
 
1119
- /* ---- Eyebrow / label / meta the mono-label family -------------- */
1120
- :where(.bp-eyebrow, .bp-label) {
1250
+ /* ---- Mono micro-label family<bp-eyebrow> + internal aliases -----
1251
+ <bp-eyebrow> is a short uppercase kicker line that heads the block
1252
+ beneath it. Default is ink; add the [muted] attribute for quiet
1253
+ metadata or provenance. The legacy .bp-eyebrow / .bp-label classes
1254
+ are the same ink role, and .bp-meta the muted role — all kept only
1255
+ for runtime-injected chrome and choice internals, not authoring. */
1256
+ :where(bp-eyebrow, .bp-eyebrow, .bp-label) {
1121
1257
  display: block;
1122
1258
  font-family: var(--bp-mono);
1123
1259
  font-size: var(--bp-label-md);
@@ -1126,212 +1262,28 @@
1126
1262
  color: var(--bp-ink);
1127
1263
  margin-bottom: var(--bp-space-2);
1128
1264
  }
1129
- :where(.bp-meta) {
1130
- font-family: var(--bp-mono);
1131
- font-size: var(--bp-label-md);
1132
- letter-spacing: var(--bp-label-md-ls);
1133
- text-transform: uppercase;
1134
- color: var(--bp-text-secondary);
1135
- }
1136
- :where(.bp-note) {
1265
+ :where(bp-eyebrow[muted]) {
1137
1266
  color: var(--bp-text-secondary);
1138
1267
  }
1139
-
1140
- /* ---- 1. Decision panel ------------------------------------------
1141
- The single load-bearing decision, given more weight than a routine
1142
- callout. An inverted masthead band carries the kicker, an optional
1143
- status pill, and optional provenance (decider + <time>); the headline
1144
- statement is the typographic hero; stacked tenets explain it; and a
1145
- distinct hatched footer states the condition for revisiting it. Every
1146
- part but the statement is optional. Markup:
1147
- <section class="bp-decision">
1148
- <header class="bp-decision__bar">
1149
- <span class="bp-label">Decision</span>
1150
- <span class="bp-decision__status">Locked</span>
1151
- <p class="bp-decision__meta">Obvious · <time datetime="2026-06-16">16 Jun 2026</time></p>
1152
- </header>
1153
- <div class="bp-decision__body">
1154
- <p class="bp-decision-stmt">…the headline…</p>
1155
- <dl class="bp-decision__tenets"> <dt>Holds because</dt><dd>…</dd> … </dl>
1156
- </div>
1157
- <p class="bp-decision__revisit"><b>Revisit when</b> …</p>
1158
- </section> */
1159
- :where(.bp-decision) {
1160
- border: 1px solid var(--bp-ink);
1161
- border-radius: var(--bp-radius-6);
1162
- background: var(--bp-paper);
1163
- overflow: hidden;
1164
- margin: var(--bp-space-4) 0;
1165
- }
1166
- /* Inverted masthead band: kicker left, status pill, provenance pushed
1167
- right. Paper-on-ink, so it flips with the theme like the doc band. */
1168
- :where(.bp-decision__bar) {
1169
- display: flex;
1170
- align-items: center;
1171
- gap: var(--bp-space-2);
1172
- padding: var(--bp-space-2) var(--bp-space-3);
1173
- background: var(--bp-ink);
1174
- color: var(--bp-paper);
1175
- }
1176
- :where(.bp-decision__bar) > :where(.bp-label) {
1177
- margin: 0;
1178
- color: var(--bp-paper);
1179
- font-size: var(--bp-label-lg);
1180
- letter-spacing: var(--bp-label-lg-ls);
1181
- font-weight: var(--bp-weight-medium);
1182
- }
1183
- :where(.bp-decision__status) {
1184
- font-family: var(--bp-mono);
1185
- font-size: var(--bp-label-sm);
1186
- letter-spacing: var(--bp-label-sm-ls);
1187
- text-transform: uppercase;
1188
- border: 1px solid color-mix(in oklch, var(--bp-paper) 45%, transparent);
1189
- border-radius: var(--bp-radius-pill);
1190
- padding: 1px 8px;
1191
- }
1192
- :where(.bp-decision__meta) {
1193
- margin: 0 0 0 auto;
1194
- font-family: var(--bp-mono);
1195
- font-size: var(--bp-label-md);
1196
- letter-spacing: var(--bp-label-md-ls);
1197
- text-transform: uppercase;
1198
- color: color-mix(in oklch, var(--bp-paper) 72%, transparent);
1199
- text-align: right;
1200
- }
1201
- :where(.bp-decision__meta) :where(time) {
1202
- color: color-mix(in oklch, var(--bp-paper) 90%, transparent);
1203
- }
1204
- :where(.bp-decision__body) {
1205
- padding: var(--bp-space-4) var(--bp-space-4) var(--bp-space-4);
1206
- }
1207
- :where(.bp-decision-stmt) {
1208
- font-family: var(--bp-sans);
1209
- font-size: var(--bp-text-title);
1210
- font-weight: var(--bp-weight-strong);
1211
- letter-spacing: -0.48px;
1212
- line-height: var(--bp-lh-title);
1213
- color: var(--bp-text);
1214
- margin: 0;
1215
- text-wrap: balance;
1216
- }
1217
- /* Stacked tenets — mono label ABOVE its value (no stranded label column),
1218
- so several points stay aligned to the left margin and read cleanly. */
1219
- :where(.bp-decision__tenets) {
1220
- display: block;
1221
- margin: var(--bp-space-3) 0 0;
1222
- padding: 0;
1223
- }
1224
- :where(.bp-decision__tenets) > :where(dt) {
1268
+ :where(.bp-meta) {
1225
1269
  font-family: var(--bp-mono);
1226
1270
  font-size: var(--bp-label-md);
1227
- font-weight: var(--bp-weight-body);
1228
1271
  letter-spacing: var(--bp-label-md-ls);
1229
1272
  text-transform: uppercase;
1230
1273
  color: var(--bp-text-secondary);
1231
- padding: 0;
1232
- margin: 0 0 var(--bp-space-1);
1233
- }
1234
- :where(.bp-decision__tenets) > :where(dt ~ dt) {
1235
- margin-top: var(--bp-space-3);
1236
1274
  }
1237
- :where(.bp-decision__tenets) > :where(dd) {
1238
- color: var(--bp-text);
1239
- font-size: var(--bp-text-body);
1240
- line-height: var(--bp-lh-body);
1241
- margin: 0;
1275
+ /* Quiet subordinate paragraph — <bp-note> for authored asides/caveats.
1276
+ .bp-note is the same role kept for runtime-injected chrome. */
1277
+ :where(bp-note) {
1278
+ display: block;
1279
+ margin: 0 0 1.2em;
1242
1280
  }
1243
- /* Change condition — a distinct hatched footer (drafting "subject to
1244
- revision" texture). Tight, flush to the panel's bottom edge. */
1245
- :where(.bp-decision__revisit) {
1246
- display: flex;
1247
- align-items: baseline;
1248
- gap: var(--bp-space-2);
1249
- margin: 0;
1250
- padding: var(--bp-space-2) var(--bp-space-4);
1251
- border-top: 1px solid var(--bp-edge);
1252
- background-image: var(--bp-hatch);
1253
- font-size: var(--bp-text-small);
1254
- line-height: var(--bp-lh-small);
1281
+ :where(bp-note, .bp-note) {
1255
1282
  color: var(--bp-text-secondary);
1256
1283
  }
1257
- :where(.bp-decision__revisit) > :where(b) {
1258
- font-family: var(--bp-mono);
1259
- font-size: var(--bp-label-md);
1260
- font-weight: var(--bp-weight-body);
1261
- letter-spacing: var(--bp-label-md-ls);
1262
- text-transform: uppercase;
1263
- color: var(--bp-ink);
1264
- white-space: nowrap;
1265
- }
1266
-
1267
- /* ---- 1b. Collapsible decision -----------------------------------
1268
- A <details> variant for pages that carry several decisions. Collapsed,
1269
- the whole panel is just the inverted bar — the decision reads on the
1270
- left (compact, not hero scale), provenance on the right. Expanding
1271
- drops the tenets and the revisit footer below the same bar. No JS —
1272
- native disclosure. Markup:
1273
- <details class="bp-decision bp-decision--collapsible">
1274
- <summary class="bp-decision__bar">
1275
- <span class="bp-label">Decision</span>
1276
- <span class="bp-decision__title">…the decision…</span>
1277
- <span class="bp-decision__meta">Obvious · <time …>…</time></span>
1278
- <span class="bp-decision__caret" aria-hidden="true"></span>
1279
- </summary>
1280
- <dl class="bp-decision__tenets"> … </dl>
1281
- <p class="bp-decision__revisit"> … </p>
1282
- </details> */
1283
- :where(.bp-decision--collapsible) {
1284
- padding: 0;
1285
- }
1286
- /* The summary IS the bar — clicking it toggles the disclosure. */
1287
- :where(.bp-decision--collapsible) > :where(summary) {
1288
- cursor: pointer;
1289
- list-style: none;
1290
- gap: var(--bp-space-3);
1291
- }
1292
- :where(.bp-decision--collapsible) > :where(summary)::-webkit-details-marker {
1293
- display: none;
1294
- }
1295
- /* Suppress the global <summary> caret; this variant carries its own. */
1296
- :where(.bp-decision--collapsible) > :where(summary)::before {
1297
- content: none;
1298
- }
1299
- /* The decision itself, compact (not hero scale), sits left of the meta. */
1300
- :where(.bp-decision__title) {
1301
- min-width: 0;
1302
- font-family: var(--bp-sans);
1303
- font-size: var(--bp-text-h4);
1304
- font-weight: var(--bp-weight-strong);
1305
- letter-spacing: -0.2px;
1306
- line-height: var(--bp-lh-h4);
1307
- color: var(--bp-paper);
1308
- }
1309
- :where(.bp-decision--collapsible) :where(.bp-decision__meta) {
1310
- margin-right: 0;
1311
- }
1312
- /* Caret rides at the far right and rotates open. */
1313
- :where(.bp-decision__caret) {
1314
- flex: 0 0 auto;
1315
- width: 0;
1316
- height: 0;
1317
- border-left: 5px solid currentColor;
1318
- border-top: 4px solid transparent;
1319
- border-bottom: 4px solid transparent;
1320
- transition: transform var(--bp-duration-fast) var(--bp-ease);
1321
- }
1322
- /* No meta? Caret takes the auto margin so it still parks on the right. */
1323
- :where(.bp-decision--collapsible) :where(.bp-decision__bar):not(:has(.bp-decision__meta)) :where(.bp-decision__caret) {
1324
- margin-left: auto;
1325
- }
1326
- :where(.bp-decision--collapsible[open]) :where(.bp-decision__caret) {
1327
- transform: rotate(90deg);
1328
- }
1329
- :where(.bp-decision--collapsible) > :where(.bp-decision__tenets) {
1330
- padding: var(--bp-space-4) var(--bp-space-4) var(--bp-space-3);
1331
- }
1332
1284
 
1333
1285
  /* ---- 2. Typed callout family ------------------------------------
1334
- A coherent callout family with legible roles, beneath the decision.
1286
+ A coherent callout family with legible roles.
1335
1287
  Drafting-style: instead of a box, four L-shaped registration ticks
1336
1288
  bracket the corners (drawn as layered background gradients — no extra
1337
1289
  DOM), with an icon + mono type label heading the body. The base is
@@ -1351,7 +1303,7 @@
1351
1303
  padding: var(--bp-space-3) var(--bp-space-4);
1352
1304
  margin: var(--bp-space-4) 0;
1353
1305
  color: var(--bp-text);
1354
- background-color: transparent;
1306
+ background-color: oklch(0 0 0 / 0);
1355
1307
  background-repeat: no-repeat;
1356
1308
  /* 8 solid slivers = 4 corner Ls, then an optional texture fill below. */
1357
1309
  background-image:
@@ -1380,10 +1332,7 @@
1380
1332
  /* Remove trailing margin from the last child of ANY boxed component so
1381
1333
  padding, not stray paragraph space, controls the inner bottom gap. */
1382
1334
  :where(
1383
- .bp-decision,
1384
1335
  .bp-callout,
1385
- .bp-option-grid > *,
1386
- .bp-state-grid > *,
1387
1336
  .bp-sequence > li,
1388
1337
  details
1389
1338
  ) > :last-child {
@@ -1451,41 +1400,10 @@
1451
1400
  grid-template-columns: minmax(10rem, 16rem) 1fr;
1452
1401
  }
1453
1402
 
1454
- /* ---- 4. Option / comparison grid --------------------------------
1455
- True parallel comparisons (A / B / C). Equal-column cards built
1456
- from <article>s, with a role label and an optional verdict pill.
1457
- Mark the chosen option with .bp-opt--rec. */
1458
- :where(.bp-option-grid) {
1459
- display: grid;
1460
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
1461
- gap: 0;
1462
- border: 1px solid var(--bp-edge);
1463
- margin: var(--bp-space-4) 0;
1464
- }
1465
- :where(.bp-option-grid > *) {
1466
- padding: var(--bp-space-3);
1467
- border-right: 1px solid var(--bp-edge);
1468
- background: var(--bp-paper);
1469
- }
1470
- :where(.bp-option-grid > :last-child) {
1471
- border-right: 0;
1472
- }
1473
- :where(.bp-opt--rec) {
1474
- background: var(--bp-fill-amb);
1475
- outline: 2px solid var(--bp-ink);
1476
- outline-offset: -2px;
1477
- }
1478
- :where(.bp-verdict) {
1479
- display: inline-block;
1480
- font-family: var(--bp-mono);
1481
- font-size: var(--bp-label-sm);
1482
- letter-spacing: var(--bp-label-sm-ls);
1483
- text-transform: uppercase;
1484
- color: var(--bp-paper);
1485
- background: var(--bp-ink);
1486
- padding: 2px 8px;
1487
- margin-top: var(--bp-space-2);
1488
- }
1403
+ /* Parallel comparisons and state models are author-composed: a small
1404
+ grid in the document's own <style>, colored from the core tokens (see
1405
+ SKILL "Visual grammar"). Recommendations live in <bp-choice resolved>,
1406
+ not a static grid modifier. */
1489
1407
 
1490
1408
  /* ---- 5. Numbered linear sequence --------------------------------
1491
1409
  A linear mechanism (pipeline, escalation). NOT a comparison grid.
@@ -1518,29 +1436,6 @@
1518
1436
  color: var(--bp-ink);
1519
1437
  }
1520
1438
 
1521
- /* ---- State grid -------------------------------------------------
1522
- Off-happy-path state model (empty / loading / ready / error …).
1523
- Built from <article>s; each gets a mono state name via .bp-label. */
1524
- :where(.bp-state-grid) {
1525
- display: grid;
1526
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1527
- gap: 0;
1528
- border: 1px solid var(--bp-edge);
1529
- margin: var(--bp-space-4) 0;
1530
- }
1531
- :where(.bp-state-grid > *) {
1532
- padding: var(--bp-space-3);
1533
- border-right: 1px solid var(--bp-edge);
1534
- border-bottom: 1px solid var(--bp-edge);
1535
- background: var(--bp-paper);
1536
- }
1537
- /* Void / fenced-off state — a dead cell (orphaned, reclaimed away).
1538
- The hatch layers over the paper set above; declared after so the
1539
- zero-specificity rules resolve by source order. */
1540
- :where(.bp-state--void) {
1541
- background-image: var(--bp-hatch);
1542
- }
1543
-
1544
1439
  /* ---- Hatch utility ----------------------------------------------
1545
1440
  Drop .bp-hatch on any block element to mark a fenced / held /
1546
1441
  out-of-bounds region with drafting section-lining. Layers the hatch
@@ -1573,7 +1468,7 @@
1573
1468
  padding: var(--bp-space-2) var(--bp-space-3);
1574
1469
  background: var(--bp-ink);
1575
1470
  color: var(--bp-paper);
1576
- border-radius: var(--bp-radius-4);
1471
+ border-radius: var(--bp-radius-0);
1577
1472
  margin-bottom: var(--bp-space-3);
1578
1473
  }
1579
1474
  :where(.bp-choice:has(.bp-choice__commit:checked)) .bp-choice__verdict,
@@ -1601,7 +1496,7 @@
1601
1496
  font-size: var(--bp-label-md);
1602
1497
  letter-spacing: var(--bp-label-md-ls);
1603
1498
  text-transform: uppercase;
1604
- color: color-mix(in oklch, var(--bp-paper) 72%, transparent);
1499
+ color: color-mix(in oklch, var(--bp-paper) 72%, oklch(0 0 0 / 0));
1605
1500
  }
1606
1501
 
1607
1502
  :where(.bp-choice-rationale) {
@@ -1672,7 +1567,7 @@
1672
1567
  flex-wrap: wrap;
1673
1568
  gap: 0;
1674
1569
  border: 1px solid var(--bp-ink-line);
1675
- border-radius: var(--bp-radius-4);
1570
+ border-radius: var(--bp-radius-0);
1676
1571
  overflow: hidden;
1677
1572
  margin: 0 0 var(--bp-space-3);
1678
1573
  width: fit-content;
@@ -1712,7 +1607,7 @@
1712
1607
  gap: 8px;
1713
1608
  cursor: pointer;
1714
1609
  border: 1px solid var(--bp-ink);
1715
- border-radius: var(--bp-radius-4);
1610
+ border-radius: var(--bp-radius-0);
1716
1611
  padding: 6px 14px;
1717
1612
  font-family: var(--bp-mono);
1718
1613
  font-size: var(--bp-label-md);
@@ -1735,7 +1630,7 @@
1735
1630
  padding: var(--bp-space-3);
1736
1631
  margin-left: calc(-1 * var(--bp-space-3));
1737
1632
  margin-right: calc(-1 * var(--bp-space-3));
1738
- border-radius: var(--bp-radius-4);
1633
+ border-radius: var(--bp-radius-0);
1739
1634
  }
1740
1635
  :where(.bp-choice__panel:has(.bp-choice__commit:checked)) :where(.bp-choice__adopt) {
1741
1636
  background: var(--bp-ink);
@@ -1751,7 +1646,7 @@
1751
1646
  display: block;
1752
1647
  position: relative;
1753
1648
  border: 1px solid var(--bp-edge);
1754
- border-radius: var(--bp-radius-4);
1649
+ border-radius: var(--bp-radius-0);
1755
1650
  padding: var(--bp-space-3);
1756
1651
  cursor: pointer;
1757
1652
  background: var(--bp-paper);
@@ -1796,6 +1691,38 @@
1796
1691
  display: inline-flex;
1797
1692
  }
1798
1693
 
1694
+ /* ---- mode: resolved (non-interactive — the decision is already made) -
1695
+ Same stack visuals, but driven by [data-resolved] instead of :checked:
1696
+ the verdict banner and chosen card are shown statically, the rest read
1697
+ as considered-and-set-aside. No inputs, no reset. */
1698
+ :where(.bp-choice--resolved) :where(.bp-choice__verdict) {
1699
+ display: flex;
1700
+ }
1701
+ :where(.bp-choice--resolved) :where(.bp-choice__verdict-pick[data-resolved]) {
1702
+ display: block;
1703
+ }
1704
+ :where(.bp-choice--resolved) :where(.bp-choice__stack) {
1705
+ display: grid;
1706
+ gap: var(--bp-space-3);
1707
+ }
1708
+ :where(.bp-choice--resolved) :where(.bp-choice__card) {
1709
+ cursor: default;
1710
+ }
1711
+ :where(.bp-choice__card[data-resolved]) {
1712
+ border: 2px solid var(--bp-ink);
1713
+ background: var(--bp-fill-amb);
1714
+ }
1715
+ :where(.bp-choice__card[data-resolved]) :where(.bp-choice__card-chosen) {
1716
+ display: inline-flex;
1717
+ }
1718
+ :where(.bp-choice--resolved) :where(.bp-choice__card:not([data-resolved])) {
1719
+ opacity: 0.62;
1720
+ background-image: var(--bp-hatch);
1721
+ }
1722
+ :where(.bp-choice--resolved) :where(.bp-choice__card:not([data-resolved])) :where(.bp-choice__card-rejected) {
1723
+ display: inline-flex;
1724
+ }
1725
+
1799
1726
  /* ---- layout: gallery (mockup pick) ------------------------------- */
1800
1727
  :where(.bp-choice--gallery) :where(.bp-choice__gallery) {
1801
1728
  display: grid;
@@ -1807,7 +1734,7 @@
1807
1734
  position: relative;
1808
1735
  cursor: pointer;
1809
1736
  border: 1px solid var(--bp-edge);
1810
- border-radius: var(--bp-radius-4);
1737
+ border-radius: var(--bp-radius-0);
1811
1738
  overflow: hidden;
1812
1739
  background: var(--bp-paper);
1813
1740
  }
@@ -1863,7 +1790,7 @@
1863
1790
  :where(.bp-choice__compare) {
1864
1791
  margin-top: var(--bp-space-3);
1865
1792
  border: 1px solid var(--bp-edge);
1866
- border-radius: var(--bp-radius-4);
1793
+ border-radius: var(--bp-radius-0);
1867
1794
  }
1868
1795
  :where(.bp-choice__compare > summary) {
1869
1796
  cursor: pointer;
@@ -1886,7 +1813,7 @@
1886
1813
  /* ---- Pre-flight (decisions before drafting) ---------------------- */
1887
1814
  :where(.bp-preflight) {
1888
1815
  border: 1px solid var(--bp-ink-line);
1889
- border-radius: var(--bp-radius-6);
1816
+ border-radius: var(--bp-radius-0);
1890
1817
  overflow: hidden;
1891
1818
  margin: var(--bp-space-4) 0;
1892
1819
  }
@@ -1908,7 +1835,7 @@
1908
1835
  font-size: var(--bp-label-md);
1909
1836
  letter-spacing: var(--bp-label-md-ls);
1910
1837
  text-transform: uppercase;
1911
- color: color-mix(in oklch, var(--bp-paper) 78%, transparent);
1838
+ color: color-mix(in oklch, var(--bp-paper) 78%, oklch(0 0 0 / 0));
1912
1839
  }
1913
1840
  :where(.bp-preflight__questions) {
1914
1841
  counter-reset: bp-preflight-q;
@@ -1968,7 +1895,7 @@
1968
1895
  gap: 7px;
1969
1896
  cursor: pointer;
1970
1897
  border: 1px solid var(--bp-ink-faint);
1971
- border-radius: var(--bp-radius-pill);
1898
+ border-radius: var(--bp-radius-0);
1972
1899
  padding: 5px 12px;
1973
1900
  font-size: var(--bp-text-small);
1974
1901
  background: var(--bp-paper);
@@ -1988,7 +1915,7 @@
1988
1915
  :where(.bp-preflight__gate) {
1989
1916
  margin: var(--bp-space-3);
1990
1917
  border: 1px dashed var(--bp-ink-line);
1991
- border-radius: var(--bp-radius-6);
1918
+ border-radius: var(--bp-radius-0);
1992
1919
  padding: var(--bp-space-4);
1993
1920
  text-align: center;
1994
1921
  background-image: var(--bp-hatch);
@@ -2013,7 +1940,7 @@
2013
1940
  align-items: center;
2014
1941
  gap: 8px;
2015
1942
  border: 1px solid var(--bp-ink);
2016
- border-radius: var(--bp-radius-4);
1943
+ border-radius: var(--bp-radius-0);
2017
1944
  padding: 8px 16px;
2018
1945
  font-family: var(--bp-mono);
2019
1946
  font-size: var(--bp-label-md);
@@ -2037,7 +1964,7 @@
2037
1964
  }
2038
1965
  :where(.bp-choice-record__row) {
2039
1966
  border: 1px solid var(--bp-edge);
2040
- border-radius: var(--bp-radius-4);
1967
+ border-radius: var(--bp-radius-0);
2041
1968
  padding: var(--bp-space-2) var(--bp-space-3);
2042
1969
  display: grid;
2043
1970
  grid-template-columns: minmax(8rem, 14rem) 1fr auto;
@@ -2080,30 +2007,161 @@
2080
2007
  margin-inline: max(calc(50% - 50vw), calc((100% - var(--bp-wide)) / 2));
2081
2008
  }
2082
2009
 
2083
- /* ---- Source disclosure -------------------------------------------
2084
- A reusable source-code panel. The semantic shell is a native <details>;
2085
- authors provide the explicit language/copy toolbar, blueprint-code.js
2086
- enables the copy button, and blueprint-code.css adds Prism color. */
2087
- :where(bp-source) {
2010
+ /* ---- <bp-table>: opt-in enhanced data table ----------------------
2011
+ The bare <table> stays the calm typographic primitive in @layer base.
2012
+ <bp-table> is the recommended wrapper when a table earns data-table
2013
+ behavior: the runtime wraps the author's <table> in a .bp-table-x scroll
2014
+ shell so horizontal overflow stays contained without making <bp-table> the
2015
+ sticky header's scroll ancestor. The host washes rows on hover, end-aligns
2016
+ numeric columns (runtime adds .bp-col-num), and opts into a sticky header /
2017
+ key column or a compact density. The inner <table> is author content and
2018
+ keeps its base styling untouched. All of it lives here in @layer components
2019
+ so a plain <table> is unchanged. */
2020
+ :where(bp-table) {
2088
2021
  display: block;
2022
+ max-width: 100%;
2023
+ margin: var(--bp-space-3) 0 var(--bp-space-4);
2089
2024
  }
2090
- :where(.bp-source) {
2091
- padding: 0;
2025
+ /* Horizontal scroll lives on the inner shell, not on <bp-table> itself, so a
2026
+ plain (non-sticky) table never makes the host an overflow ancestor. */
2027
+ :where(bp-table > .bp-table-x) {
2028
+ max-width: 100%;
2029
+ overflow-x: auto;
2092
2030
  }
2093
- :where(.bp-source[open]) {
2094
- padding-bottom: 0;
2031
+ /* [sticky] turns the shell into a self-contained scroll REGION: one element
2032
+ scrolls BOTH axes within an explicit block size, so the header sticks to
2033
+ the region's top edge and the header + body share a single horizontal
2034
+ scroll coordinate system (no desync), identically at every width and with
2035
+ `wide`. This is the only model that reconciles a sticky header with
2036
+ horizontal scroll — a pure-CSS page-scroll sticky header is impossible once
2037
+ an ancestor must scroll horizontally. Authors can retune the height with
2038
+ --bp-table-sticky-height. */
2039
+ :where(bp-table[sticky] > .bp-table-x) {
2040
+ max-height: var(--bp-table-sticky-height, min(70vh, 32rem));
2041
+ overflow: auto;
2042
+ }
2043
+ /* Sticky cells need border-collapse:separate — the base table rule uses
2044
+ collapse, which prevents position:sticky from engaging on th. */
2045
+ :where(bp-table[sticky] > .bp-table-x > table) {
2046
+ border-collapse: separate;
2047
+ border-spacing: 0;
2048
+ }
2049
+ /* The host owns the block rhythm; null the inner table's base margin so the
2050
+ scroll shell (a BFC) does not add a top/bottom gutter. The base
2051
+ `table { width: 100% }` makes the table shrink to exactly fill the shell,
2052
+ so it can never overflow and overflow-x:auto never scrolls - columns just
2053
+ crush. Size to content instead (min-width:100% still fills the band when
2054
+ the table is narrow), so a table wider than its band overflows and the
2055
+ shell scrolls horizontally. */
2056
+ :where(bp-table > .bp-table-x > table) {
2057
+ margin: 0;
2058
+ width: max-content;
2059
+ min-width: 100%;
2095
2060
  }
2096
- /* The summary IS the header bar: marker + intent label on the left, the
2097
- language tag + copy action pushed to the right. One strip, then the code
2098
- beneath it — no nested boxes. */
2099
- :where(.bp-source > summary) {
2100
- display: flex;
2101
- align-items: center;
2102
- gap: var(--bp-space-3);
2103
- padding: var(--bp-space-2) var(--bp-space-3);
2061
+ /* Opt-in breakout: reuse the .bp-table-wrap--wide measure math. */
2062
+ :where(bp-table[wide]) {
2063
+ width: 100vw;
2064
+ max-width: var(--bp-wide);
2065
+ margin-inline: calc(50% - 50vw);
2066
+ margin-inline: max(calc(50% - 50vw), calc((100% - var(--bp-wide)) / 2));
2104
2067
  }
2105
- :where(.bp-source-name) {
2106
- font-family: var(--bp-mono);
2068
+ /* Inside the document shell, a wide table is an edge-aware scroll band. The
2069
+ wrapper breaks out to fill <main> from its left border to the right frame
2070
+ rule, but carries scroll padding equal to the prose offset on both ends.
2071
+ At rest the first column lines up with the body text while the band runs
2072
+ out to the frame rule on the right; scrolled to the end the last column
2073
+ stops on the text's right line while the earlier columns run out to
2074
+ <main>'s left border. The pad is the centered measure's offset from the
2075
+ sheet edge, and collapses to 0 once the measure fills the sheet (narrow
2076
+ widths), so the breakout simply disappears there. */
2077
+ :where(html[data-bp-document] bp-table[wide]) {
2078
+ --bp-wt-pad: max(
2079
+ 0px,
2080
+ (
2081
+ 100vw - var(--bp-shell-inset-left) - var(--bp-shell-inset-right) -
2082
+ (var(--bp-content) - 2 * var(--bp-space-6))
2083
+ ) / 2
2084
+ );
2085
+ width: auto;
2086
+ max-width: none;
2087
+ margin-inline: calc(-1 * var(--bp-wt-pad));
2088
+ padding-inline: var(--bp-wt-pad);
2089
+ }
2090
+ /* Numeric columns — the runtime end-aligns a column's data cells AND its
2091
+ th[scope=col] by adding .bp-col-num (value-specific, so JS-driven). */
2092
+ :where(bp-table .bp-col-num) {
2093
+ text-align: end;
2094
+ }
2095
+ /* Row hover — wash the ambient fill across the row's cells. Transition on
2096
+ the resting cell so hover-in and hover-out both ease (tokens only). */
2097
+ :where(bp-table tbody tr :is(td, th)) {
2098
+ transition: background-color var(--bp-duration-fast) var(--bp-ease-out);
2099
+ }
2100
+ :where(bp-table tbody tr:hover :is(td, th)) {
2101
+ background: var(--bp-fill-amb);
2102
+ }
2103
+ /* Sticky header (opt-in [sticky]) — pins to the top of the scroll region as
2104
+ it scrolls vertically. Backgrounds are paper so scrolled rows never show
2105
+ through; the header out-stacks the sticky key column. */
2106
+ :where(bp-table[sticky] thead th) {
2107
+ position: sticky;
2108
+ top: 0;
2109
+ z-index: 3;
2110
+ background: var(--bp-paper);
2111
+ }
2112
+ /* Sticky key column — the row header pins to the left edge as the region
2113
+ scrolls horizontally, staying in sync with the header (one scroll box). */
2114
+ :where(bp-table[sticky] th[scope="row"]) {
2115
+ position: sticky;
2116
+ left: 0;
2117
+ z-index: 2;
2118
+ background: var(--bp-paper);
2119
+ }
2120
+ /* The pinned corner (column header above the key column) out-stacks both.
2121
+ Authors label the key column with scope="row" in tbody; the matching
2122
+ thead cell is scope="col", so pin the first header cell left. */
2123
+ :where(bp-table[sticky] thead th:first-child) {
2124
+ left: 0;
2125
+ z-index: 4;
2126
+ }
2127
+ /* Compact density — tighter cells; body cells drop to small, the mono
2128
+ column headers to the small label step. */
2129
+ :where(bp-table[density="compact"] :is(td, th)) {
2130
+ padding-top: var(--bp-space-1);
2131
+ padding-bottom: var(--bp-space-1);
2132
+ }
2133
+ :where(bp-table[density="compact"] :is(td, th[scope="row"])) {
2134
+ font-size: var(--bp-text-small);
2135
+ }
2136
+ :where(bp-table[density="compact"] :is(th[scope="col"], thead th)) {
2137
+ font-size: var(--bp-label-sm);
2138
+ letter-spacing: var(--bp-label-sm-ls);
2139
+ }
2140
+
2141
+ /* ---- Source disclosure -------------------------------------------
2142
+ A reusable source-code panel. The semantic shell is a native <details>;
2143
+ authors provide the explicit language/copy toolbar, blueprint-code.js
2144
+ enables the copy button, and blueprint-code.css adds Prism color. */
2145
+ :where(bp-source) {
2146
+ display: block;
2147
+ }
2148
+ :where(.bp-source) {
2149
+ padding: 0;
2150
+ }
2151
+ :where(.bp-source[open]) {
2152
+ padding-bottom: 0;
2153
+ }
2154
+ /* The summary IS the header bar: marker + intent label on the left, the
2155
+ language tag + copy action pushed to the right. One strip, then the code
2156
+ beneath it — no nested boxes. */
2157
+ :where(.bp-source > summary) {
2158
+ display: flex;
2159
+ align-items: center;
2160
+ gap: var(--bp-space-3);
2161
+ padding: var(--bp-space-2) var(--bp-space-3);
2162
+ }
2163
+ :where(.bp-source-name) {
2164
+ font-family: var(--bp-mono);
2107
2165
  font-size: var(--bp-label-md);
2108
2166
  letter-spacing: var(--bp-label-md-ls);
2109
2167
  text-transform: uppercase;
@@ -2125,9 +2183,9 @@
2125
2183
  :where(.bp-source-copy) {
2126
2184
  appearance: none;
2127
2185
  border: 1px solid var(--bp-ink-line);
2128
- border-radius: var(--bp-radius-2);
2186
+ border-radius: var(--bp-radius-0);
2129
2187
  padding: 4px var(--bp-space-2);
2130
- background: transparent;
2188
+ background: oklch(0 0 0 / 0);
2131
2189
  color: var(--bp-ink);
2132
2190
  font-family: var(--bp-mono);
2133
2191
  font-size: var(--bp-label-sm);
@@ -2222,7 +2280,7 @@
2222
2280
  width: min(460px, calc(100vw - 20px));
2223
2281
  padding: 0;
2224
2282
  border: 1px solid var(--bp-ink-line);
2225
- border-radius: var(--bp-radius-6);
2283
+ border-radius: var(--bp-radius-0);
2226
2284
  background: var(--bp-paper);
2227
2285
  color: var(--bp-text);
2228
2286
  box-shadow: var(--bp-shadow-pop);
@@ -2261,7 +2319,7 @@
2261
2319
  left: var(--bp-cite-caret, 50%);
2262
2320
  width: 0;
2263
2321
  height: 0;
2264
- border: 7px solid transparent;
2322
+ border: 7px solid oklch(0 0 0 / 0);
2265
2323
  transform: translateX(-50%);
2266
2324
  }
2267
2325
  :where(.bp-cite-pop[data-placement="top"])::before {
@@ -2312,88 +2370,63 @@
2312
2370
  max-height: 320px;
2313
2371
  margin: 0;
2314
2372
  border: 0;
2315
- border-radius: 0 0 var(--bp-radius-6) var(--bp-radius-6);
2316
- }
2317
-
2318
- /* ---- SVG figure marks (1px hairline default via --bp-stroke) -------
2319
- ILLUSTRATIONS are the one place blue lives. Node/edge line work draws
2320
- in --bp-illustration; only the in-diagram TEXT labels stay neutral ink
2321
- (text everywhere reads in the monochrome scale). */
2322
- :where(.bp-node) {
2323
- fill: var(--bp-paper);
2324
- stroke: var(--bp-illustration);
2325
- stroke-width: var(--bp-stroke);
2326
- }
2327
- :where(.bp-node--amb) {
2328
- fill: var(--bp-illustration-fill);
2329
- }
2330
- /* Fenced node — a barrier / held step. Filled with the diagonal hatch
2331
- paint server (define the <pattern> once in the figure's <defs>; see
2332
- the .bp-hatch-fill / .bp-hatch-line tiles below). The .bp-node stroke
2333
- keeps the illustration border. CSS gradients can't paint an SVG shape,
2334
- so SVG uses a <pattern> while HTML uses the --bp-hatch gradient token. */
2335
- :where(.bp-node--fenced) {
2336
- fill: url(#bp-hatch);
2337
- }
2338
- /* Tiles for the SVG hatch <pattern> — token-driven so both themes track.
2339
- The fill rect carries the illustration wash; the line is the drafting
2340
- section-lining, both in the blue illustration family. */
2341
- :where(.bp-hatch-fill) {
2342
- fill: var(--bp-illustration-fill);
2343
- }
2344
- :where(.bp-hatch-line) {
2345
- stroke: var(--bp-illustration-faint);
2346
- stroke-width: var(--bp-stroke);
2347
- }
2348
- :where(.bp-edge) {
2349
- fill: none;
2350
- stroke: var(--bp-illustration);
2351
- stroke-width: var(--bp-stroke);
2352
- }
2353
- :where(.bp-edge--skip) {
2354
- stroke: var(--bp-illustration-soft);
2355
- stroke-dasharray: 4 3;
2356
- }
2357
- :where(.bp-svg-label) {
2358
- font-family: var(--bp-mono);
2359
- font-size: 11px;
2360
- fill: var(--bp-text);
2361
- }
2362
- :where(.bp-svg-meta) {
2363
- font-family: var(--bp-mono);
2364
- font-size: 9px;
2365
- fill: var(--bp-text-secondary);
2366
- }
2367
- /* Callout terminator dot — illustration line work, so it draws in blue */
2368
- :where(.bp-svg-dot) {
2369
- fill: var(--bp-illustration);
2373
+ border-radius: 0 0 var(--bp-radius-0) var(--bp-radius-0);
2370
2374
  }
2371
2375
 
2372
- /* ---- Isometric solids (overview illustration) ---------------------
2373
- Generic isometric primitives for axis-aligned boxes: side faces read
2374
- as paper edges, top faces carry the illustration wash, and a faint
2375
- floor grid grounds the scene. Used for the overview factory line
2376
- (machines, conveyors, parts); blue is line work only, labels stay
2377
- ink. .bp-svg-dot terminates callout leaders in the illustration blue. */
2378
- :where(.bp-stack-face) {
2379
- fill: var(--bp-paper);
2380
- stroke: var(--bp-illustration);
2381
- stroke-width: var(--bp-stroke);
2382
- }
2383
- :where(.bp-stack-top) {
2384
- fill: var(--bp-illustration-fill);
2385
- stroke: var(--bp-illustration);
2386
- stroke-width: var(--bp-stroke);
2387
- }
2388
- :where(.bp-stack-top--fenced) {
2389
- fill: url(#overview-stack-hatch);
2390
- }
2391
- :where(.bp-stack-grid) {
2392
- fill: none;
2393
- stroke: var(--bp-illustration-faint);
2394
- stroke-width: var(--bp-stroke);
2395
- opacity: 0.45;
2376
+ /* ---- SVG paint utilities (diagrams live entirely in the blue ramp) -
2377
+ Color is the ONLY thing these set: line work AND labels are blue —
2378
+ there is no neutral/gray ink inside a figure. Everything else
2379
+ (stroke-width, stroke-dasharray, font-size, text-anchor, markers,
2380
+ geometry) is plain SVG the author writes as normal attributes; SVG's
2381
+ default stroke-width:1 already equals the hairline.
2382
+ svg-fill-{100..900} paints `fill` — interiors and label text
2383
+ svg-stroke-{100..900} paints `stroke` — outlines, edges, grids
2384
+ The number is ink STRENGTH, not absolute lightness: 900 is the
2385
+ strongest mark (label text, key strokes), 100 the faintest fill. So
2386
+ the ramp inverts under the dark theme (below) to stay legible while
2387
+ never leaving the blue family — authors only ever touch the classes.
2388
+ A class beats a fill=""/stroke="" presentation attribute; reach for
2389
+ style="" only to override a single element. */
2390
+ :where(:root) {
2391
+ --svg-blue-100: var(--bp-blue-100);
2392
+ --svg-blue-200: var(--bp-blue-200);
2393
+ --svg-blue-300: var(--bp-blue-300);
2394
+ --svg-blue-400: var(--bp-blue-400);
2395
+ --svg-blue-500: var(--bp-blue-500);
2396
+ --svg-blue-600: var(--bp-blue-600);
2397
+ --svg-blue-700: var(--bp-blue-700);
2398
+ --svg-blue-800: var(--bp-blue-800);
2399
+ --svg-blue-900: var(--bp-blue-900);
2396
2400
  }
2401
+ :where([data-obvious-theme="dark"]) {
2402
+ --svg-blue-100: var(--bp-blue-900);
2403
+ --svg-blue-200: var(--bp-blue-800);
2404
+ --svg-blue-300: var(--bp-blue-700);
2405
+ --svg-blue-400: var(--bp-blue-600);
2406
+ --svg-blue-500: var(--bp-blue-500);
2407
+ --svg-blue-600: var(--bp-blue-400);
2408
+ --svg-blue-700: var(--bp-blue-300);
2409
+ --svg-blue-800: var(--bp-blue-200);
2410
+ --svg-blue-900: var(--bp-blue-100);
2411
+ }
2412
+ :where(.svg-fill-100) { fill: var(--svg-blue-100); }
2413
+ :where(.svg-fill-200) { fill: var(--svg-blue-200); }
2414
+ :where(.svg-fill-300) { fill: var(--svg-blue-300); }
2415
+ :where(.svg-fill-400) { fill: var(--svg-blue-400); }
2416
+ :where(.svg-fill-500) { fill: var(--svg-blue-500); }
2417
+ :where(.svg-fill-600) { fill: var(--svg-blue-600); }
2418
+ :where(.svg-fill-700) { fill: var(--svg-blue-700); }
2419
+ :where(.svg-fill-800) { fill: var(--svg-blue-800); }
2420
+ :where(.svg-fill-900) { fill: var(--svg-blue-900); }
2421
+ :where(.svg-stroke-100) { stroke: var(--svg-blue-100); }
2422
+ :where(.svg-stroke-200) { stroke: var(--svg-blue-200); }
2423
+ :where(.svg-stroke-300) { stroke: var(--svg-blue-300); }
2424
+ :where(.svg-stroke-400) { stroke: var(--svg-blue-400); }
2425
+ :where(.svg-stroke-500) { stroke: var(--svg-blue-500); }
2426
+ :where(.svg-stroke-600) { stroke: var(--svg-blue-600); }
2427
+ :where(.svg-stroke-700) { stroke: var(--svg-blue-700); }
2428
+ :where(.svg-stroke-800) { stroke: var(--svg-blue-800); }
2429
+ :where(.svg-stroke-900) { stroke: var(--svg-blue-900); }
2397
2430
 
2398
2431
  /* ---- Masthead title-block: a metadata strip (author/date/status) - */
2399
2432
  :where(.bp-title-block) {
@@ -2549,6 +2582,10 @@
2549
2582
  margin: 0;
2550
2583
  padding: 0;
2551
2584
  list-style: none;
2585
+ /* Primary label text starts after pad + index tick + gap — sub-entries
2586
+ reuse this inset so their labels line up with the row above. */
2587
+ --bp-toc-index-w: 14px;
2588
+ --bp-toc-label-inset: calc(var(--bp-space-3) + var(--bp-toc-index-w) + var(--bp-space-2));
2552
2589
  }
2553
2590
  /* The gliding active pill, drawn behind the entries and travelling via
2554
2591
  --bp-toc-y / --bp-toc-h (set by blueprint.js). */
@@ -2591,45 +2628,72 @@
2591
2628
  text-decoration: none;
2592
2629
  padding: 6px var(--bp-space-3);
2593
2630
  border-radius: var(--bp-radius-6);
2594
- /* Motion: .bp-transition-colors .bp-ease (hover color shift) */
2595
- transition-property: color, background;
2596
- transition-duration: var(--bp-duration-fast);
2597
- transition-timing-function: var(--bp-ease);
2631
+ /* Idle entries snap on theme flip so the root crossfade isn't fighting
2632
+ per-row token tweens (labels + index numbers alike). */
2633
+ transition: none;
2598
2634
  }
2599
2635
  /* Auto-numbered index (01, 02…), tabular so the digits line up. */
2600
2636
  :where(.bp-sidebar > ul > li > a, .bp-sidebar__panel > ul > li > a, .bp-toc > ul > li > a)::before {
2601
2637
  content: counter(bp-toc, decimal-leading-zero);
2602
- flex: 0 0 auto;
2638
+ flex: 0 0 var(--bp-toc-index-w, 14px);
2639
+ min-width: var(--bp-toc-index-w, 14px);
2603
2640
  font-size: var(--bp-label-lg);
2604
2641
  font-variant-numeric: tabular-nums;
2605
2642
  letter-spacing: 0.02em;
2606
2643
  color: var(--bp-ink-soft);
2607
- /* Motion: .bp-transition-colors .bp-ease (hover color shift) */
2608
- transition-property: color;
2609
- transition-duration: var(--bp-duration-fast);
2610
- transition-timing-function: var(--bp-ease);
2644
+ transition: none;
2611
2645
  }
2612
- /* Nested sub-entries indent and drop the number. */
2646
+ /* Nested sub-entries align with primary label text and read tighter. */
2613
2647
  :where(.bp-sidebar ul ul, .bp-toc ul ul) {
2614
- margin: 0;
2615
- padding: 0 0 0 var(--bp-space-3);
2648
+ margin: 0 0 var(--bp-space-1);
2649
+ padding: 0;
2616
2650
  list-style: none;
2617
2651
  }
2618
2652
  :where(.bp-sidebar ul ul a, .bp-toc ul ul a) {
2653
+ display: block;
2619
2654
  font-size: var(--bp-text-small);
2655
+ line-height: 1.25;
2656
+ padding: 1px var(--bp-space-3) 1px var(--bp-toc-label-inset);
2657
+ }
2658
+ /* Group eyebrow row — a quiet mono kicker that heads a run of entries. It is
2659
+ a flat sibling of the entry items (so the gliding pill + scroll-spy are
2660
+ untouched) and is skipped by the auto-numbering counter, keeping the
2661
+ section numbers continuous with the document's own section eyebrows. */
2662
+ :where(.bp-sidebar .bp-nav-group, .bp-toc .bp-nav-group) {
2663
+ counter-increment: none;
2664
+ margin: var(--bp-space-4) 0 var(--bp-space-1);
2665
+ }
2666
+ :where(.bp-sidebar .bp-nav-group:first-child, .bp-toc .bp-nav-group:first-child) {
2667
+ margin-top: var(--bp-space-1);
2668
+ }
2669
+ :where(.bp-nav-group__label) {
2670
+ display: block;
2671
+ padding: 0 var(--bp-space-3);
2672
+ font-family: var(--bp-mono);
2673
+ font-size: var(--bp-label-sm);
2674
+ letter-spacing: var(--bp-label-sm-ls);
2675
+ text-transform: uppercase;
2676
+ color: var(--bp-text-secondary);
2620
2677
  }
2621
2678
  /* Hover shows its own subtle pill; the active entry rides the gliding pill
2622
2679
  above (so it keeps no own background) and reads in ink + medium weight. */
2623
2680
  :where(.bp-sidebar a:hover, .bp-toc a:hover) {
2624
2681
  color: var(--bp-ink);
2625
2682
  background: var(--bp-fill-amb);
2683
+ transition:
2684
+ color var(--bp-duration-fast) var(--bp-ease),
2685
+ background-color var(--bp-duration-fast) var(--bp-ease);
2626
2686
  }
2627
2687
  :where(.bp-sidebar a[aria-current="location"], .bp-toc a[aria-current="location"], .bp-sidebar a.active, .bp-sidebar a.is-active, .bp-toc a.active, .bp-toc a.is-active) {
2628
2688
  color: var(--bp-ink);
2629
2689
  background: none;
2630
2690
  font-weight: var(--bp-weight-medium);
2631
2691
  }
2632
- :where(.bp-sidebar a:hover::before, .bp-toc a:hover::before, .bp-sidebar a[aria-current="location"]::before, .bp-toc a[aria-current="location"]::before, .bp-sidebar a.active::before, .bp-toc a.active::before) {
2692
+ :where(.bp-sidebar a:hover::before, .bp-toc a:hover::before) {
2693
+ color: var(--bp-ink);
2694
+ transition: color var(--bp-duration-fast) var(--bp-ease);
2695
+ }
2696
+ :where(.bp-sidebar a[aria-current="location"]::before, .bp-toc a[aria-current="location"]::before, .bp-sidebar a.active::before, .bp-toc a.active::before) {
2633
2697
  color: var(--bp-ink);
2634
2698
  }
2635
2699
  /* ---- Full-width contents block -----------------------------------
@@ -2656,6 +2720,25 @@
2656
2720
  counter-increment: bp-contents;
2657
2721
  margin: 0;
2658
2722
  }
2723
+ /* Group eyebrow in the inline index: a full-width band that breaks the grid
2724
+ into labelled runs. Skipped by the numbering counter (it carries no link),
2725
+ so entry numbers stay continuous across groups. */
2726
+ :where(.bp-contents-group) {
2727
+ grid-column: 1 / -1;
2728
+ counter-increment: none;
2729
+ margin: var(--bp-space-4) 0 var(--bp-space-1);
2730
+ }
2731
+ :where(.bp-contents-group:first-child) {
2732
+ margin-top: 0;
2733
+ }
2734
+ :where(.bp-contents-group__label) {
2735
+ display: block;
2736
+ font-family: var(--bp-mono);
2737
+ font-size: var(--bp-label-sm);
2738
+ letter-spacing: var(--bp-label-sm-ls);
2739
+ text-transform: uppercase;
2740
+ color: var(--bp-text-secondary);
2741
+ }
2659
2742
  :where(.bp-contents a) {
2660
2743
  display: flex;
2661
2744
  align-items: baseline;
@@ -2708,13 +2791,10 @@
2708
2791
  transform: translateX(var(--bp-space-1));
2709
2792
  }
2710
2793
 
2711
- /* Column frame: the shell is pinned between fixed top/bottom insets so the
2712
- desk margin always shows while content scrolls inside; vertical rules at
2713
- the column edges run full viewport height; horizontal rules are one
2714
- continuous full-width line at the shell's top and bottom inset, with a
2715
- solid top band masking scroll bleed into the margin. Shadows off while
2716
- testing. */
2717
- :where(html:has(.bp-shell)) {
2794
+ /* Column frame: the PAGE scrolls, while fixed top/bottom bands + fixed vertical
2795
+ edge rules hold the desk margin on all four sides. <main> sits in normal
2796
+ flow (paper sheet); the sidebar rail stays fixed. */
2797
+ :where(html[data-bp-document]) {
2718
2798
  --bp-shell-inset-block: var(--bp-space-4);
2719
2799
  --bp-shell-inset-top: var(--bp-shell-inset-block);
2720
2800
  --bp-shell-inset-left: var(--bp-sidebar);
@@ -2727,29 +2807,40 @@
2727
2807
  --bp-frame-line-z: 210;
2728
2808
  --bp-scroll-margin: var(--bp-space-6);
2729
2809
  }
2730
- :where(body:has(.bp-shell)) {
2731
- overflow: hidden;
2732
- }
2733
- :where(.bp-shell) {
2734
- position: fixed;
2735
- inset: var(--bp-shell-inset-top) var(--bp-shell-inset-right)
2736
- var(--bp-shell-inset-block) var(--bp-shell-inset-left);
2737
- margin: 0;
2738
- overflow-x: hidden;
2739
- overflow-y: auto;
2740
- overscroll-behavior: contain;
2741
- -webkit-overflow-scrolling: touch;
2810
+ /* Top/bottom desk bands are body padding so the sheet clears the fixed
2811
+ frame rules and the page tail is not occluded by the bottom band. */
2812
+ :where(html[data-bp-document] body) {
2813
+ padding-top: var(--bp-shell-inset-top);
2814
+ padding-bottom: var(--bp-shell-inset-block);
2815
+ }
2816
+ /* <main> is the paper sheet spanning the full column between the vertical
2817
+ frame rules; direct children keep the prose measure from Tier 1b. */
2818
+ :where(html[data-bp-document] main) {
2819
+ max-width: none;
2820
+ margin: 0 var(--bp-shell-inset-right) 0 var(--bp-shell-inset-left);
2821
+ min-height: calc(
2822
+ 100vh - var(--bp-shell-inset-top) - var(--bp-shell-inset-block)
2823
+ );
2742
2824
  background: var(--bp-paper);
2743
2825
  border: 0;
2744
2826
  border-radius: var(--bp-radius-0);
2745
2827
  box-shadow: none;
2746
2828
  }
2747
- /* TOC / hash targets scroll inside the shell reserve top air so headings
2748
- don't land flush against the inner edge. */
2749
- :where(.bp-shell section[id]) {
2829
+ /* Same inner measure as max-width: var(--bp-content) on a padded main, and
2830
+ auto margins center that measure within the sheet so the reading column
2831
+ sits balanced between the frame rules. The sheet's own box shrinks when the
2832
+ rail collapses (margin-inline transition above), so the centered measure
2833
+ re-centers in step with the slide rather than hugging the left rule. */
2834
+ :where(html[data-bp-document] main > *) {
2835
+ max-width: min(100%, calc(var(--bp-content) - 2 * var(--bp-space-6)));
2836
+ margin-inline: auto;
2837
+ }
2838
+ /* TOC / hash targets — reserve top air so a heading clears the fixed top
2839
+ band instead of landing flush beneath it. */
2840
+ :where(html[data-bp-document] main :is(section[id], [id][data-sidebar])) {
2750
2841
  scroll-margin-top: var(--bp-scroll-margin);
2751
2842
  }
2752
- :where(html:has(.bp-shell))::before {
2843
+ :where(html[data-bp-document])::before {
2753
2844
  content: "";
2754
2845
  position: fixed;
2755
2846
  inset-inline: 0;
@@ -2759,7 +2850,7 @@
2759
2850
  pointer-events: none;
2760
2851
  z-index: var(--bp-frame-line-z);
2761
2852
  }
2762
- :where(html:has(.bp-shell))::after {
2853
+ :where(html[data-bp-document])::after {
2763
2854
  content: "";
2764
2855
  position: fixed;
2765
2856
  inset-block: 0;
@@ -2775,10 +2866,10 @@
2775
2866
  var(--bp-shell-inset-left) 0,
2776
2867
  calc(100% - var(--bp-shell-inset-right)) 0;
2777
2868
  }
2778
- /* Top inset: solid desk band masks scroll bleed; frame lines sit above rail
2779
- scrims so verticals run edge-to-edge through the horizontals. Bottom: line
2780
- only. */
2781
- :where(body:has(.bp-shell))::before {
2869
+ /* Top/bottom desk bands (fixed) mask the page scrolling beneath them; the
2870
+ bottom band draws the lower horizontal at its top edge. Frame lines sit
2871
+ above rail scrims so the verticals run edge-to-edge through horizontals. */
2872
+ :where(html[data-bp-document] body)::before {
2782
2873
  content: "";
2783
2874
  position: fixed;
2784
2875
  inset-inline: 0;
@@ -2788,15 +2879,16 @@
2788
2879
  pointer-events: none;
2789
2880
  z-index: var(--bp-shell-mask-z);
2790
2881
  }
2791
- :where(body:has(.bp-shell))::after {
2882
+ :where(html[data-bp-document] body)::after {
2792
2883
  content: "";
2793
2884
  position: fixed;
2794
2885
  inset-inline: 0;
2795
- height: 0;
2886
+ bottom: 0;
2887
+ height: var(--bp-shell-inset-block);
2796
2888
  border-top: var(--bp-stroke) solid var(--bp-edge);
2889
+ background: var(--bp-bg);
2797
2890
  pointer-events: none;
2798
2891
  z-index: var(--bp-frame-line-z);
2799
- bottom: var(--bp-shell-inset-block);
2800
2892
  }
2801
2893
  :where(.scroll-progress) {
2802
2894
  position: fixed;
@@ -2815,245 +2907,1101 @@
2815
2907
  transition-duration: var(--bp-duration-instant);
2816
2908
  transition-timing-function: var(--bp-ease-linear);
2817
2909
  }
2818
- }
2819
2910
 
2820
- /* Stored Blueprint shell contracts. New documents use bp-* components. */
2821
- @layer components {
2822
- :where(.sidebar) {
2823
- position: fixed;
2824
- inset: 0 auto 0 0;
2825
- width: var(--sidebar);
2826
- height: 100vh;
2827
- overflow-y: auto;
2828
- padding: 34px 20px 34px 26px;
2829
- z-index: 100;
2911
+ /* ---- <bp-mock> drafting browser frame for UX mockups ------------
2912
+ The frame is CHROME, so it is drawn in the neutral ink scale; the
2913
+ sketch inside keeps the illustration-blue ramp. `viewport` draws the
2914
+ canvas at a fixed pixel width and scales it to fit (crisp 1:1 when the
2915
+ frame is wide enough or expanded); the expand control lifts the frame
2916
+ into a near-fullscreen overlay over a drafting scrim. */
2917
+ :where(.bp-mock) {
2918
+ display: block;
2919
+ margin: var(--bp-space-4) 0;
2830
2920
  }
2831
- :where(.sidebar-head) {
2832
- padding-bottom: 16px;
2833
- border-bottom: 1px solid var(--edge);
2834
- margin-bottom: 16px;
2921
+ :where(.bp-mock__frame) {
2922
+ display: flex;
2923
+ flex-direction: column;
2924
+ border: var(--bp-stroke) solid var(--bp-ink-line);
2925
+ border-radius: var(--bp-radius-0);
2926
+ background: var(--bp-paper);
2927
+ overflow: hidden;
2835
2928
  }
2836
- :where(.sidebar-title) {
2837
- font: 500 11px/1.4 var(--mono);
2838
- letter-spacing: 0.12em;
2839
- text-transform: uppercase;
2840
- color: var(--ink);
2929
+ :where(.bp-mock__bar) {
2930
+ display: flex;
2931
+ align-items: center;
2932
+ gap: var(--bp-space-2);
2933
+ min-height: 34px;
2934
+ padding: var(--bp-space-2) var(--bp-space-3);
2935
+ border-bottom: var(--bp-stroke) solid var(--bp-edge);
2936
+ background: var(--bp-fill-amb);
2841
2937
  }
2842
- :where(.sidebar-doc) {
2843
- font: 400 10px/1.4 var(--mono);
2844
- letter-spacing: 0.08em;
2845
- text-transform: uppercase;
2846
- color: var(--text-secondary);
2847
- margin-top: 6px;
2938
+ :where(.bp-mock__ticks) {
2939
+ display: inline-flex;
2940
+ gap: 5px;
2941
+ flex: 0 0 auto;
2848
2942
  }
2849
- :where(.sidebar ul) {
2850
- list-style: none;
2851
- margin: 0;
2852
- padding: 0;
2853
- counter-reset: navsec;
2854
- display: flex;
2855
- flex-direction: column;
2943
+ :where(.bp-mock__ticks i) {
2944
+ width: 8px;
2945
+ height: 8px;
2946
+ border-radius: var(--bp-radius-0);
2947
+ border: var(--bp-stroke) solid var(--bp-ink-faint);
2856
2948
  }
2857
- :where(.sidebar li) {
2858
- margin: 0;
2949
+ :where(.bp-mock__label) {
2950
+ flex: 1 1 auto;
2951
+ min-width: 0;
2952
+ font-family: var(--bp-mono);
2953
+ font-size: var(--bp-label-md);
2954
+ letter-spacing: var(--bp-label-md-ls);
2955
+ color: var(--bp-text-secondary);
2956
+ white-space: nowrap;
2957
+ overflow: hidden;
2958
+ text-overflow: ellipsis;
2959
+ }
2960
+ :where(.bp-mock__btn) {
2961
+ display: inline-flex;
2962
+ align-items: center;
2963
+ justify-content: center;
2964
+ flex: 0 0 auto;
2965
+ width: 24px;
2966
+ height: 24px;
2859
2967
  padding: 0;
2968
+ border: var(--bp-stroke) solid var(--bp-edge);
2969
+ border-radius: var(--bp-radius-0);
2970
+ background: var(--bp-paper);
2971
+ color: var(--bp-text-secondary);
2972
+ cursor: pointer;
2860
2973
  }
2861
- :where(.sidebar a) {
2862
- position: relative;
2863
- display: block;
2864
- font: 400 12px/1.35 var(--sans);
2865
- color: var(--text-secondary);
2866
- text-decoration: none;
2867
- padding: 7px 10px 7px 36px;
2974
+ :where(.bp-mock__btn svg) {
2975
+ width: 14px;
2976
+ height: 14px;
2868
2977
  }
2869
- :where(.sidebar a)::before {
2870
- counter-increment: navsec;
2871
- content: counter(navsec, decimal-leading-zero);
2872
- position: absolute;
2873
- left: 10px;
2874
- top: 8px;
2875
- font: 400 10px/1 var(--mono);
2876
- letter-spacing: 0.06em;
2877
- color: var(--ink-faint);
2978
+ :where(.bp-mock__btn:hover) {
2979
+ color: var(--bp-text);
2980
+ border-color: var(--bp-ink-line);
2878
2981
  }
2879
- :where(.sidebar a:hover, .sidebar a[aria-current="location"]) {
2880
- color: var(--ink);
2881
- background: var(--fill-amb);
2982
+ :where(.bp-mock__btn:focus-visible) {
2983
+ outline: 2px solid var(--bp-ink);
2984
+ outline-offset: 2px;
2882
2985
  }
2883
- :where(.sidebar a[aria-current="location"])::after {
2884
- content: "";
2885
- position: absolute;
2886
- inset: 0 auto 0 0;
2887
- width: 2px;
2888
- background: var(--ink);
2986
+ :where(.bp-mock__stage) {
2987
+ position: relative;
2988
+ overflow: hidden;
2989
+ background: var(--bp-bg);
2889
2990
  }
2890
- :where(.page-body) {
2891
- margin-left: var(--sidebar);
2991
+ :where(.bp-mock__canvas) {
2992
+ transform-origin: top left;
2993
+ transform: scale(var(--bp-mock-scale, 1));
2892
2994
  }
2893
- :where(.sheet) {
2995
+
2996
+ /* Expanded → near-fullscreen overlay; the frame floats over a drafting
2997
+ scrim (ink wash + hatch). The frame itself stays the bordered window. */
2998
+ :where(.bp-mock.is-expanded) {
2999
+ position: fixed;
3000
+ inset: 0;
3001
+ /* Above the fixed rail (100), scroll-progress (200), and frame rules
3002
+ (210) so the overlay fully covers the document chrome. */
3003
+ z-index: 300;
2894
3004
  margin: 0;
2895
- min-height: 100vh;
2896
- background: var(--paper);
2897
- border-left: 1px solid var(--edge);
3005
+ display: grid;
3006
+ place-items: center;
3007
+ padding: clamp(var(--bp-space-4), 4vw, var(--bp-space-7));
3008
+ background-color: var(--bp-scrim);
3009
+ background-image: var(--bp-hatch);
2898
3010
  }
2899
- :where(.hero) {
2900
- padding: 48px 56px 36px;
2901
- border-bottom: 1px solid var(--ink-line);
2902
- font-family: var(--sans);
3011
+ :where(.bp-mock.is-expanded) :where(.bp-mock__frame) {
3012
+ width: min(100%, 1120px);
3013
+ max-height: 100%;
3014
+ box-shadow: var(--bp-shadow-pop);
2903
3015
  }
2904
- :where(.hero-meta) {
2905
- font: 400 11px/1.4 var(--mono);
2906
- letter-spacing: 0.14em;
2907
- text-transform: uppercase;
2908
- color: var(--ink-soft);
2909
- margin-bottom: 16px;
3016
+ :where(.bp-mock.is-expanded) :where(.bp-mock__stage) {
3017
+ overflow: auto;
2910
3018
  }
2911
- :where(.hero h1) {
2912
- font: 600 24px/32px var(--sans);
2913
- letter-spacing: -0.48px;
2914
- margin: 10px 0 14px;
3019
+
3020
+ /* ---- <bp-gallery> — image grid with a lightbox viewer -------------
3021
+ A tidy responsive grid of images (screenshots, mockups, uploads).
3022
+ Each thumbnail wears a BLUESCALE treatment by default — the raster
3023
+ is desaturated and a blue veil (.bp-gallery__wash) color-blends over
3024
+ it — and reveals its true color on hover/focus. Activating a tile
3025
+ opens .bp-lightbox, a near-fullscreen viewer over a drafting scrim. */
3026
+ :where(.bp-gallery) {
3027
+ display: block;
3028
+ margin: var(--bp-space-5) 0;
2915
3029
  }
2916
- :where(.hero-sub) {
2917
- font: 400 14px/20px var(--serif);
2918
- text-align: justify;
2919
- hyphens: auto;
3030
+ :where(.bp-gallery__grid) {
3031
+ display: grid;
3032
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
3033
+ gap: var(--bp-space-3);
2920
3034
  }
2921
- :where(.title-block) {
2922
- margin-top: 34px;
2923
- border: 1px solid var(--ink-line);
3035
+ :where(.bp-gallery__item) {
2924
3036
  display: flex;
2925
- flex-wrap: wrap;
3037
+ flex-direction: column;
3038
+ gap: var(--bp-space-2);
3039
+ min-width: 0;
3040
+ margin: 0;
3041
+ padding: 0;
3042
+ border: 0;
3043
+ background: none;
3044
+ text-align: left;
3045
+ cursor: pointer;
3046
+ font: inherit;
3047
+ color: inherit;
2926
3048
  }
2927
- :where(.tb-cell) {
2928
- flex: 0 0 auto;
3049
+ :where(.bp-gallery__thumb) {
3050
+ position: relative;
3051
+ display: block;
3052
+ overflow: hidden;
3053
+ aspect-ratio: 4 / 3;
3054
+ border: var(--bp-stroke) solid var(--bp-ink-line);
3055
+ border-radius: var(--bp-radius-0);
3056
+ background: var(--bp-bg);
3057
+ }
3058
+ :where(.bp-gallery__img) {
3059
+ display: block;
3060
+ width: 100%;
3061
+ height: 100%;
3062
+ object-fit: cover;
3063
+ /* Bluescale: drop saturation; the wash supplies the blue cast. */
3064
+ filter: grayscale(1) contrast(1.02);
3065
+ transition:
3066
+ filter var(--bp-duration-slow) var(--bp-ease-out),
3067
+ transform var(--bp-duration-slow) var(--bp-ease-out);
3068
+ }
3069
+ :where(.bp-gallery__wash) {
3070
+ position: absolute;
3071
+ inset: 0;
3072
+ background: var(--bp-illustration);
3073
+ mix-blend-mode: color;
3074
+ opacity: 0.85;
3075
+ transition: opacity var(--bp-duration-slow) var(--bp-ease-out);
3076
+ pointer-events: none;
3077
+ }
3078
+ :where(.bp-gallery__zoom) {
3079
+ position: absolute;
3080
+ inset: auto var(--bp-space-2) var(--bp-space-2) auto;
3081
+ display: inline-flex;
3082
+ align-items: center;
3083
+ justify-content: center;
3084
+ width: 28px;
3085
+ height: 28px;
3086
+ border-radius: var(--bp-radius-0);
3087
+ background: var(--bp-paper);
3088
+ color: var(--bp-text-secondary);
3089
+ box-shadow: var(--bp-shadow-pop);
3090
+ opacity: 0;
3091
+ transform: translateY(4px);
3092
+ transition:
3093
+ opacity var(--bp-duration-fast) var(--bp-ease-out),
3094
+ transform var(--bp-duration-fast) var(--bp-ease-out);
3095
+ pointer-events: none;
3096
+ }
3097
+ :where(.bp-gallery__zoom svg) {
3098
+ width: 16px;
3099
+ height: 16px;
3100
+ }
3101
+ /* Reveal true color on hover or keyboard focus. */
3102
+ :where(.bp-gallery__item:hover, .bp-gallery__item:focus-visible)
3103
+ :where(.bp-gallery__img) {
3104
+ filter: none;
3105
+ transform: scale(1.03);
3106
+ }
3107
+ :where(.bp-gallery__item:hover, .bp-gallery__item:focus-visible)
3108
+ :where(.bp-gallery__wash) {
3109
+ opacity: 0;
3110
+ }
3111
+ :where(.bp-gallery__item:hover, .bp-gallery__item:focus-visible)
3112
+ :where(.bp-gallery__zoom) {
3113
+ opacity: 1;
3114
+ transform: translateY(0);
3115
+ }
3116
+ :where(.bp-gallery__item:focus-visible) :where(.bp-gallery__thumb) {
3117
+ outline: 2px solid var(--bp-ink);
3118
+ outline-offset: 2px;
3119
+ }
3120
+ :where(.bp-gallery__caption) {
3121
+ font-family: var(--bp-mono);
3122
+ font-size: var(--bp-label-md);
3123
+ letter-spacing: var(--bp-label-md-ls);
3124
+ color: var(--bp-text-secondary);
3125
+ overflow: hidden;
3126
+ text-overflow: ellipsis;
3127
+ white-space: nowrap;
3128
+ }
3129
+
3130
+ /* The lightbox is appended to <body> by the runtime; hidden until open. */
3131
+ :where(.bp-lightbox) {
3132
+ /* Scrim inset and the room reserved for the caption row below the
3133
+ image, so a portrait image taller than the viewport scales down
3134
+ instead of pushing its caption off-screen. */
3135
+ --bp-lightbox-pad: clamp(var(--bp-space-4), 4vw, var(--bp-space-7));
3136
+ --bp-lightbox-meta: calc(var(--bp-space-3) + var(--bp-space-5));
3137
+ position: fixed;
3138
+ inset: 0;
3139
+ z-index: 300;
3140
+ display: none;
3141
+ /* Side columns reserve the prev/next controls; the figure rides the
3142
+ centered middle column and the single 1fr row fills the height so
3143
+ the image is centered on both axes. */
3144
+ grid-template-columns: auto minmax(0, 1fr) auto;
3145
+ grid-template-rows: minmax(0, 1fr);
3146
+ align-items: center;
3147
+ justify-items: center;
3148
+ gap: var(--bp-space-3);
3149
+ padding: var(--bp-lightbox-pad);
3150
+ background-color: var(--bp-scrim);
3151
+ background-image: var(--bp-hatch);
3152
+ }
3153
+ :where(.bp-lightbox.is-open) {
3154
+ display: grid;
3155
+ }
3156
+ :where(.bp-lightbox__figure) {
3157
+ grid-column: 2;
3158
+ display: flex;
3159
+ flex-direction: column;
3160
+ align-items: center;
3161
+ justify-content: center;
3162
+ gap: var(--bp-space-3);
3163
+ margin: 0;
3164
+ min-width: 0;
3165
+ min-height: 0;
3166
+ max-width: 100%;
3167
+ max-height: 100%;
3168
+ }
3169
+ :where(.bp-lightbox__img) {
3170
+ display: block;
3171
+ /* Width is bounded by the centered column; height is bounded by the
3172
+ viewport minus the scrim inset and the reserved caption row. With
3173
+ both maxes set and intrinsic sizing left auto, the browser scales
3174
+ the image down while preserving its aspect ratio. */
3175
+ width: auto;
3176
+ height: auto;
3177
+ max-width: 100%;
3178
+ max-height: calc(100dvh - 2 * var(--bp-lightbox-pad) - var(--bp-lightbox-meta));
3179
+ object-fit: contain;
3180
+ border: var(--bp-stroke) solid var(--bp-ink-line);
3181
+ border-radius: var(--bp-radius-0);
3182
+ background: var(--bp-paper);
3183
+ box-shadow: var(--bp-shadow-pop);
3184
+ }
3185
+ :where(.bp-lightbox__meta) {
3186
+ display: flex;
3187
+ align-items: baseline;
3188
+ gap: var(--bp-space-3);
3189
+ max-width: 100%;
3190
+ font-family: var(--bp-mono);
3191
+ font-size: var(--bp-label-md);
3192
+ letter-spacing: var(--bp-label-md-ls);
3193
+ color: var(--bp-paper);
3194
+ }
3195
+ :where(.bp-lightbox__kicker) {
3196
+ text-transform: uppercase;
3197
+ opacity: 0.7;
3198
+ }
3199
+ :where(.bp-lightbox__counter) {
3200
+ opacity: 0.7;
3201
+ }
3202
+ :where(.bp-lightbox__caption) {
3203
+ min-width: 0;
3204
+ overflow: hidden;
3205
+ text-overflow: ellipsis;
3206
+ white-space: nowrap;
3207
+ }
3208
+ :where(.bp-lightbox__close) {
3209
+ position: absolute;
3210
+ inset: var(--bp-space-4) var(--bp-space-4) auto auto;
3211
+ display: inline-flex;
3212
+ align-items: center;
3213
+ justify-content: center;
3214
+ width: 40px;
3215
+ height: 40px;
3216
+ padding: 0;
3217
+ border: var(--bp-stroke) solid var(--bp-edge);
3218
+ border-radius: var(--bp-radius-0);
3219
+ background: var(--bp-paper);
3220
+ color: var(--bp-text-secondary);
3221
+ cursor: pointer;
3222
+ }
3223
+ :where(.bp-lightbox__nav) {
3224
+ display: inline-flex;
3225
+ align-items: center;
3226
+ justify-content: center;
3227
+ width: 44px;
3228
+ height: 44px;
3229
+ padding: 0;
3230
+ border: var(--bp-stroke) solid var(--bp-edge);
3231
+ border-radius: var(--bp-radius-0);
3232
+ background: var(--bp-paper);
3233
+ color: var(--bp-text-secondary);
3234
+ cursor: pointer;
3235
+ }
3236
+ :where(.bp-lightbox__nav--prev) {
3237
+ grid-column: 1;
3238
+ }
3239
+ :where(.bp-lightbox__nav--next) {
3240
+ grid-column: 3;
3241
+ }
3242
+ :where(.bp-lightbox.is-single) :where(.bp-lightbox__nav) {
3243
+ visibility: hidden;
3244
+ }
3245
+ :where(.bp-lightbox__close:hover, .bp-lightbox__nav:hover) {
3246
+ color: var(--bp-text);
3247
+ border-color: var(--bp-ink-line);
3248
+ }
3249
+ :where(.bp-lightbox__close:focus-visible, .bp-lightbox__nav:focus-visible) {
3250
+ outline: 2px solid var(--bp-paper);
3251
+ outline-offset: 2px;
3252
+ }
3253
+ :where(.bp-lightbox__close svg) {
3254
+ width: 18px;
3255
+ height: 18px;
3256
+ }
3257
+ :where(.bp-lightbox__nav svg) {
3258
+ width: 22px;
3259
+ height: 22px;
3260
+ }
3261
+ }
3262
+
3263
+ /* Stored Blueprint shell contracts. New documents use bp-* components. */
3264
+ @layer components {
3265
+ :where(.sidebar) {
3266
+ position: fixed;
3267
+ inset: 0 auto 0 0;
3268
+ width: var(--sidebar);
3269
+ height: 100vh;
3270
+ overflow-y: auto;
3271
+ padding: 34px 20px 34px 26px;
3272
+ z-index: 100;
3273
+ }
3274
+ :where(.sidebar-head) {
3275
+ padding-bottom: 16px;
3276
+ border-bottom: 1px solid var(--edge);
3277
+ margin-bottom: 16px;
3278
+ }
3279
+ :where(.sidebar-title) {
3280
+ font: 500 11px/1.4 var(--mono);
3281
+ letter-spacing: 0.12em;
3282
+ text-transform: uppercase;
3283
+ color: var(--ink);
3284
+ }
3285
+ :where(.sidebar-doc) {
3286
+ font: 400 10px/1.4 var(--mono);
3287
+ letter-spacing: 0.08em;
3288
+ text-transform: uppercase;
3289
+ color: var(--text-secondary);
3290
+ margin-top: 6px;
3291
+ }
3292
+ :where(.sidebar ul) {
3293
+ list-style: none;
3294
+ margin: 0;
3295
+ padding: 0;
3296
+ counter-reset: navsec;
3297
+ display: flex;
3298
+ flex-direction: column;
3299
+ }
3300
+ :where(.sidebar li) {
3301
+ margin: 0;
3302
+ padding: 0;
3303
+ }
3304
+ :where(.sidebar a) {
3305
+ position: relative;
3306
+ display: block;
3307
+ font: 400 12px/1.35 var(--sans);
3308
+ color: var(--text-secondary);
3309
+ text-decoration: none;
3310
+ padding: 7px 10px 7px 36px;
3311
+ }
3312
+ :where(.sidebar a)::before {
3313
+ counter-increment: navsec;
3314
+ content: counter(navsec, decimal-leading-zero);
3315
+ position: absolute;
3316
+ left: 10px;
3317
+ top: 8px;
3318
+ font: 400 10px/1 var(--mono);
3319
+ letter-spacing: 0.06em;
3320
+ color: var(--ink-faint);
3321
+ }
3322
+ :where(.sidebar a:hover, .sidebar a[aria-current="location"]) {
3323
+ color: var(--ink);
3324
+ background: var(--fill-amb);
3325
+ }
3326
+ :where(.sidebar a[aria-current="location"])::after {
3327
+ content: "";
3328
+ position: absolute;
3329
+ inset: 0 auto 0 0;
3330
+ width: 2px;
3331
+ background: var(--ink);
3332
+ }
3333
+ :where(.page-body) {
3334
+ margin-left: var(--sidebar);
3335
+ }
3336
+ :where(.sheet) {
3337
+ margin: 0;
3338
+ min-height: 100vh;
3339
+ background: var(--paper);
3340
+ border-left: 1px solid var(--edge);
3341
+ }
3342
+ :where(.hero) {
3343
+ padding: 48px 56px 36px;
3344
+ border-bottom: 1px solid var(--ink-line);
3345
+ font-family: var(--sans);
3346
+ }
3347
+ :where(.hero-meta) {
3348
+ font: 400 11px/1.4 var(--mono);
3349
+ letter-spacing: 0.14em;
3350
+ text-transform: uppercase;
3351
+ color: var(--ink-soft);
3352
+ margin-bottom: 16px;
3353
+ }
3354
+ :where(.hero h1) {
3355
+ font: 600 24px/32px var(--sans);
3356
+ letter-spacing: -0.48px;
3357
+ margin: 10px 0 14px;
3358
+ }
3359
+ :where(.hero-sub) {
3360
+ font: 400 14px/20px var(--serif);
3361
+ text-align: justify;
3362
+ hyphens: auto;
3363
+ }
3364
+ :where(.title-block) {
3365
+ margin-top: 34px;
3366
+ border: 1px solid var(--ink-line);
3367
+ display: flex;
3368
+ flex-wrap: wrap;
3369
+ }
3370
+ :where(.tb-cell) {
3371
+ flex: 0 0 auto;
2929
3372
  padding: 10px 20px 11px 14px;
2930
3373
  border-right: 1px solid var(--ink-line);
2931
3374
  }
2932
- :where(.tb-cell--grow) {
2933
- flex: 1 1 auto;
3375
+ :where(.tb-cell--grow) {
3376
+ flex: 1 1 auto;
3377
+ }
3378
+ :where(.tb-cell:last-child) {
3379
+ border-right: 0;
3380
+ }
3381
+ :where(.tb-label) {
3382
+ font: 400 10px/1.4 var(--mono);
3383
+ letter-spacing: 0.1em;
3384
+ text-transform: uppercase;
3385
+ color: var(--ink-soft);
3386
+ margin-bottom: 3px;
3387
+ }
3388
+ :where(.tb-value) {
3389
+ font: 400 12px/1.4 var(--sans);
3390
+ color: var(--text);
3391
+ }
3392
+ :where(.body) {
3393
+ padding: 8px 56px 56px;
3394
+ }
3395
+ :where(.prose) {
3396
+ max-width: var(--content);
3397
+ margin-inline: auto;
3398
+ }
3399
+ :where(.oq) {
3400
+ display: flex;
3401
+ gap: 12px;
3402
+ padding: 10px 0;
3403
+ border-bottom: 1px solid var(--edge);
3404
+ }
3405
+ :where(.oq-tag) {
3406
+ font: 400 10px/1.4 var(--mono);
3407
+ letter-spacing: 0.08em;
3408
+ text-transform: uppercase;
3409
+ color: var(--ink);
3410
+ border: 1px solid var(--ink-faint);
3411
+ padding: 2px 7px;
3412
+ height: max-content;
3413
+ white-space: nowrap;
3414
+ }
3415
+ :where(.assumption .oq-tag) {
3416
+ color: var(--text-secondary);
3417
+ border-color: var(--edge);
3418
+ }
3419
+ :where(.states) {
3420
+ display: grid;
3421
+ grid-template-columns: repeat(2, minmax(0, 1fr));
3422
+ border: 1px solid var(--edge);
3423
+ margin: 20px 0;
3424
+ }
3425
+ :where(.state) {
3426
+ padding: 14px 16px;
3427
+ border-right: 1px solid var(--edge);
3428
+ border-bottom: 1px solid var(--edge);
3429
+ }
3430
+ :where(.state:nth-child(2n)) {
3431
+ border-right: 0;
3432
+ }
3433
+ :where(.state-name, .states-label) {
3434
+ font: 400 10px/1.4 var(--mono);
3435
+ letter-spacing: 0.08em;
3436
+ text-transform: uppercase;
3437
+ color: var(--ink);
3438
+ }
3439
+ :where(.state-name) {
3440
+ margin-bottom: 6px;
3441
+ }
3442
+ :where(.state-desc) {
3443
+ font-size: 13px;
3444
+ line-height: 18px;
3445
+ color: var(--text-secondary);
3446
+ }
3447
+ :where(.states-label) {
3448
+ margin-top: 26px;
3449
+ }
3450
+ :where(.states-label + .states) {
3451
+ margin-top: 10px;
3452
+ }
3453
+ :where(.figure) {
3454
+ width: fit-content;
3455
+ max-width: 100%;
3456
+ margin: 32px auto;
3457
+ }
3458
+ :where(.figure svg[viewBox]) {
3459
+ display: block;
3460
+ width: auto;
3461
+ max-width: min(100%, var(--bp-diagram-w));
3462
+ height: auto;
3463
+ }
3464
+ :where(.figure svg:not([viewBox])) {
3465
+ display: block;
3466
+ width: auto;
3467
+ max-width: 100%;
3468
+ height: auto;
3469
+ }
3470
+ :where(.fig-cap) {
3471
+ font: 400 10px/1.4 var(--mono);
3472
+ letter-spacing: 0.06em;
3473
+ text-transform: uppercase;
3474
+ color: var(--text-secondary);
3475
+ text-align: center;
3476
+ margin-top: 14px;
3477
+ }
3478
+ }
3479
+
3480
+ /* =====================================================================
3481
+ @layer components — <bp-workplan>
3482
+
3483
+ A live "work plan": typed, dependent tasks that resolve into waves,
3484
+ rendered four ways (list · gantt · kanban · swimlanes). Light-DOM,
3485
+ token-only, theme-aware. Chrome is ink; the only non-ink token is
3486
+ --bp-positive for a passing/approved PR. The element renders only
3487
+ <div>/<span> for structure, so the base layer's semantic-element
3488
+ styling never applies — no resets needed.
3489
+ ===================================================================== */
3490
+ @layer components {
3491
+ :where(.bp-workplan) {
3492
+ font-family: var(--bp-sans);
3493
+ color: var(--bp-text);
3494
+ container-type: inline-size;
3495
+ }
3496
+
3497
+ /* ---- plan header + summary stats ---------------------------- */
3498
+ :where(.wp-head) {
3499
+ display: flex;
3500
+ flex-wrap: wrap;
3501
+ gap: var(--bp-space-3);
3502
+ align-items: flex-end;
3503
+ justify-content: space-between;
3504
+ padding-bottom: var(--bp-space-3);
3505
+ margin-bottom: var(--bp-space-3);
3506
+ border-bottom: 1px solid var(--bp-ink-line);
3507
+ }
3508
+ :where(.wp-head__name) {
3509
+ font-size: var(--bp-text-h3);
3510
+ line-height: var(--bp-lh-h3);
3511
+ font-weight: var(--bp-weight-strong);
3512
+ margin: 0;
3513
+ }
3514
+ :where(.wp-head__sub) {
3515
+ font-size: var(--bp-text-small);
3516
+ color: var(--bp-text-secondary);
3517
+ margin: 2px 0 0;
3518
+ }
3519
+ :where(.wp-stats) {
3520
+ display: flex;
3521
+ gap: var(--bp-space-4);
3522
+ margin: 0;
3523
+ }
3524
+ :where(.wp-stat) { text-align: right; }
3525
+ :where(.wp-stat__k) {
3526
+ display: block;
3527
+ font-family: var(--bp-mono);
3528
+ font-size: var(--bp-label-sm);
3529
+ letter-spacing: var(--bp-label-sm-ls);
3530
+ text-transform: uppercase;
3531
+ color: var(--bp-text-secondary);
3532
+ }
3533
+ :where(.wp-stat__v) {
3534
+ display: block;
3535
+ margin-top: 2px;
3536
+ font-variant-numeric: tabular-nums;
3537
+ font-size: var(--bp-text-h4);
3538
+ font-weight: var(--bp-weight-medium);
3539
+ }
3540
+
3541
+ /* ---- toolbar: built-in view selector + gantt unit toggle ---- */
3542
+ :where(.wp-toolbar) {
3543
+ display: flex;
3544
+ flex-wrap: wrap;
3545
+ gap: var(--bp-space-3);
3546
+ align-items: center;
3547
+ justify-content: space-between;
3548
+ margin-bottom: var(--bp-space-3);
3549
+ }
3550
+ :where(.wp-modes) { display: flex; flex-wrap: wrap; gap: var(--bp-space-1); }
3551
+ :where(.wp-modes--unit) { margin-left: auto; }
3552
+ :where(.wp-modes__btn) {
3553
+ font: inherit;
3554
+ font-size: var(--bp-text-small);
3555
+ line-height: 1;
3556
+ padding: var(--bp-space-1) var(--bp-space-3);
3557
+ border: 1px solid var(--bp-edge);
3558
+ border-radius: var(--bp-radius-0);
3559
+ background: var(--bp-paper);
3560
+ color: var(--bp-text-secondary);
3561
+ cursor: pointer;
3562
+ }
3563
+ :where(.wp-modes__btn):hover { background: var(--bp-fill-amb); }
3564
+ :where(.wp-modes__btn[aria-selected="true"]) {
3565
+ border-color: var(--bp-ink);
3566
+ color: var(--bp-text);
3567
+ background: var(--bp-fill-amb);
3568
+ }
3569
+
3570
+ /* ---- type badge (inline mono, not a pill) ------------------- */
3571
+ :where(.wp-type) {
3572
+ display: inline-flex;
3573
+ align-items: center;
3574
+ gap: 5px;
3575
+ font-family: var(--bp-mono);
3576
+ font-size: var(--bp-label-md);
3577
+ letter-spacing: var(--bp-label-md-ls);
3578
+ text-transform: uppercase;
3579
+ color: var(--bp-text-secondary);
3580
+ white-space: nowrap;
3581
+ }
3582
+ :where(.wp-type__glyph) { color: var(--bp-ink-soft); flex: none; }
3583
+ :where(.wp-type__label) { line-height: 1; }
3584
+
3585
+ /* ---- phase dot (status without color) ----------------------- */
3586
+ :where(.wp-dot) {
3587
+ --d: 10px;
3588
+ width: var(--d);
3589
+ height: var(--d);
3590
+ border-radius: var(--bp-radius-0);
3591
+ flex: none;
3592
+ box-sizing: border-box;
3593
+ }
3594
+ :where(.wp-dot--done) { background: var(--bp-ink); }
3595
+ :where(.wp-dot--active) {
3596
+ background: var(--bp-ink);
3597
+ box-shadow: 0 0 0 2px var(--bp-paper), 0 0 0 3px var(--bp-ink);
3598
+ }
3599
+ :where(.wp-dot--ready) {
3600
+ background: oklch(0 0 0 / 0);
3601
+ border: 1.5px solid var(--bp-ink-soft);
3602
+ }
3603
+ :where(.wp-dot--blocked) {
3604
+ background: oklch(0 0 0 / 0);
3605
+ border: 1.5px dashed var(--bp-ink-faint);
3606
+ }
3607
+
3608
+ /* ---- PR status — inline mono text, NO pill ------------------ */
3609
+ :where(.wp-pr) {
3610
+ display: inline-flex;
3611
+ align-items: center;
3612
+ gap: 6px;
3613
+ font-family: var(--bp-mono);
3614
+ font-size: var(--bp-label-md);
3615
+ letter-spacing: 0.01em;
3616
+ color: var(--bp-text-secondary);
3617
+ white-space: nowrap;
3618
+ min-width: 0;
3619
+ }
3620
+ :where(.wp-pr__glyph) { flex: none; width: 13px; height: 13px; }
3621
+ :where(.wp-pr__num) { color: var(--bp-text); font-weight: var(--bp-weight-medium); }
3622
+ :where(.wp-pr__ci) { color: var(--bp-text-secondary); }
3623
+ :where(.wp-pr__diff) { color: var(--bp-text-secondary); }
3624
+ :where(.wp-pr__add) { color: var(--bp-positive); }
3625
+
3626
+ :where(.wp-pr--passed .wp-pr__ci),
3627
+ :where(.wp-pr--approved .wp-pr__ci) { color: var(--bp-positive); }
3628
+ :where(.wp-pr--failed .wp-pr__glyph) { color: var(--bp-text); stroke-width: 1.8; }
3629
+ :where(.wp-pr--running .wp-pr__glyph) {
3630
+ color: var(--bp-ink-soft);
3631
+ animation: wp-spin var(--bp-duration-spin) var(--bp-ease-linear) infinite;
3632
+ }
3633
+ @keyframes wp-spin { to { transform: rotate(360deg); } }
3634
+ @media (prefers-reduced-motion: reduce) {
3635
+ :where(.wp-pr--running .wp-pr__glyph) { animation: none; }
3636
+ }
3637
+
3638
+ /* ---- owner mark — agent (square + spark) vs human (circle) -- */
3639
+ :where(.wp-owner) {
3640
+ display: inline-grid;
3641
+ place-items: center;
3642
+ width: 22px;
3643
+ height: 22px;
3644
+ flex: none;
3645
+ font-family: var(--bp-mono);
3646
+ font-size: 9px;
3647
+ letter-spacing: 0.02em;
3648
+ }
3649
+ :where(.wp-owner--human) {
3650
+ border-radius: var(--bp-radius-0);
3651
+ background: var(--bp-fill-hi);
3652
+ border: 1px solid var(--bp-edge);
3653
+ color: var(--bp-text-secondary);
3654
+ }
3655
+ :where(.wp-owner--agent) {
3656
+ border-radius: var(--bp-radius-0);
3657
+ border: 1px solid var(--bp-ink-line);
3658
+ color: var(--bp-text-secondary);
3659
+ }
3660
+ :where(.wp-owner__glyph) { width: 12px; height: 12px; }
3661
+
3662
+ /* ---- waiting note — quiet inline text ----------------------- */
3663
+ :where(.wp-wait) {
3664
+ display: block;
3665
+ font-size: var(--bp-text-small);
3666
+ color: var(--bp-text-secondary);
3667
+ white-space: nowrap;
3668
+ overflow: hidden;
3669
+ text-overflow: ellipsis;
3670
+ }
3671
+
3672
+ /* greyed-out-until-ready treatment, shared across views */
3673
+ :where([data-locked="true"]) { opacity: 0.66; }
3674
+
3675
+ /* ---- LIST view ---------------------------------------------- */
3676
+ :where(.wp-view--list) { display: grid; gap: var(--bp-space-3); }
3677
+ :where(.wp-wave__head) {
3678
+ display: flex;
3679
+ align-items: baseline;
3680
+ justify-content: space-between;
3681
+ gap: var(--bp-space-2);
3682
+ margin-bottom: 6px;
3683
+ padding: 0 2px;
3684
+ }
3685
+ :where(.wp-wave__no),
3686
+ :where(.wp-wave__count) {
3687
+ font-family: var(--bp-mono);
3688
+ font-size: var(--bp-label-md);
3689
+ letter-spacing: var(--bp-label-md-ls);
3690
+ text-transform: uppercase;
3691
+ }
3692
+ :where(.wp-wave__no) { color: var(--bp-text); }
3693
+ :where(.wp-wave__count) { color: var(--bp-text-secondary); }
3694
+ :where(.wp-row) {
3695
+ display: grid;
3696
+ grid-template-columns: auto 84px minmax(0, 1fr) auto auto;
3697
+ align-items: center;
3698
+ gap: var(--bp-space-3);
3699
+ padding: 11px var(--bp-space-3);
3700
+ border: 1px solid var(--bp-edge);
3701
+ border-top: none;
3702
+ background: var(--bp-paper);
2934
3703
  }
2935
- :where(.tb-cell:last-child) {
2936
- border-right: 0;
3704
+ :where(.wp-row:first-child) {
3705
+ border-top: 1px solid var(--bp-edge);
3706
+ border-radius: var(--bp-radius-0) var(--bp-radius-0) 0 0;
2937
3707
  }
2938
- :where(.tb-label) {
2939
- font: 400 10px/1.4 var(--mono);
2940
- letter-spacing: 0.1em;
2941
- text-transform: uppercase;
2942
- color: var(--ink-soft);
2943
- margin-bottom: 3px;
3708
+ :where(.wp-row:last-child) { border-radius: 0 0 var(--bp-radius-0) var(--bp-radius-0); }
3709
+ :where(.wp-row__main) { position: relative; min-width: 0; }
3710
+ :where(.wp-row__name) {
3711
+ display: block;
3712
+ font-size: var(--bp-text-body);
3713
+ font-weight: var(--bp-weight-medium);
3714
+ white-space: nowrap;
3715
+ overflow: hidden;
3716
+ text-overflow: ellipsis;
2944
3717
  }
2945
- :where(.tb-value) {
2946
- font: 400 12px/1.4 var(--sans);
2947
- color: var(--text);
3718
+ /* Waiting note overlays the name and is revealed on hover, so a locked
3719
+ row reads as its title at rest and its blockers on demand. */
3720
+ :where(.wp-row__main .wp-wait) {
3721
+ position: absolute;
3722
+ inset: 0;
3723
+ display: flex;
3724
+ align-items: center;
3725
+ margin: 0;
3726
+ background: var(--bp-paper);
3727
+ opacity: 0;
3728
+ pointer-events: none;
3729
+ transition: opacity var(--bp-duration-fast) var(--bp-ease-out);
2948
3730
  }
2949
- :where(.body) {
2950
- padding: 8px 56px 56px;
3731
+ :where(.wp-row[data-locked="true"]:hover .wp-row__name) { opacity: 0; }
3732
+ :where(.wp-row[data-locked="true"]:hover .wp-row__main .wp-wait) { opacity: 1; }
3733
+ :where(.wp-row--done .wp-row__name) {
3734
+ color: var(--bp-text-secondary);
3735
+ text-decoration: line-through;
3736
+ text-decoration-color: var(--bp-ink-faint);
2951
3737
  }
2952
- :where(.prose) {
2953
- max-width: var(--content);
2954
- margin-inline: auto;
3738
+ :where(.wp-row__pr) { justify-self: end; }
3739
+
3740
+ /* ---- KANBAN view — min-width columns, horizontal scroll ----- */
3741
+ :where(.wp-view--kanban) {
3742
+ display: grid;
3743
+ grid-auto-flow: column;
3744
+ grid-auto-columns: minmax(216px, 1fr);
3745
+ gap: var(--bp-space-3);
3746
+ align-items: start;
3747
+ overflow-x: auto;
3748
+ padding-bottom: var(--bp-space-2);
2955
3749
  }
2956
- :where(.oq) {
3750
+ :where(.wp-col) {
3751
+ background: var(--bp-bg);
3752
+ border: 1px solid var(--bp-edge);
3753
+ border-radius: var(--bp-radius-0);
3754
+ padding: var(--bp-space-2);
2957
3755
  display: flex;
2958
- gap: 12px;
2959
- padding: 10px 0;
2960
- border-bottom: 1px solid var(--edge);
3756
+ flex-direction: column;
3757
+ min-width: 0;
2961
3758
  }
2962
- :where(.oq-tag) {
2963
- font: 400 10px/1.4 var(--mono);
2964
- letter-spacing: 0.08em;
3759
+ :where(.wp-col__head) {
3760
+ display: flex;
3761
+ align-items: center;
3762
+ justify-content: space-between;
3763
+ padding: 4px var(--bp-space-1) var(--bp-space-2);
3764
+ }
3765
+ :where(.wp-col__title) {
3766
+ font-family: var(--bp-mono);
3767
+ font-size: var(--bp-label-md);
3768
+ letter-spacing: var(--bp-label-md-ls);
2965
3769
  text-transform: uppercase;
2966
- color: var(--ink);
2967
- border: 1px solid var(--ink-faint);
2968
- padding: 2px 7px;
2969
- height: max-content;
2970
- white-space: nowrap;
3770
+ color: var(--bp-text);
2971
3771
  }
2972
- :where(.assumption .oq-tag) {
2973
- color: var(--text-secondary);
2974
- border-color: var(--edge);
3772
+ :where(.wp-col__count) {
3773
+ font-family: var(--bp-mono);
3774
+ font-size: var(--bp-label-sm);
3775
+ color: var(--bp-text-secondary);
3776
+ font-variant-numeric: tabular-nums;
2975
3777
  }
2976
- :where(.states) {
3778
+ /* One full-width track — minmax(0,1fr) keeps cards at the column width
3779
+ so nowrap text truncates instead of stretching the card. */
3780
+ :where(.wp-col__body) {
2977
3781
  display: grid;
2978
- grid-template-columns: repeat(2, minmax(0, 1fr));
2979
- border: 1px solid var(--edge);
2980
- margin: 20px 0;
3782
+ grid-template-columns: minmax(0, 1fr);
3783
+ gap: var(--bp-space-2);
3784
+ align-content: start;
3785
+ min-width: 0;
2981
3786
  }
2982
- :where(.state) {
2983
- padding: 14px 16px;
2984
- border-right: 1px solid var(--edge);
2985
- border-bottom: 1px solid var(--edge);
3787
+ :where(.wp-col__empty) {
3788
+ font-family: var(--bp-mono);
3789
+ font-size: var(--bp-label-sm);
3790
+ letter-spacing: var(--bp-label-sm-ls);
3791
+ text-transform: uppercase;
3792
+ color: var(--bp-ink-faint);
3793
+ margin: var(--bp-space-1) 0 var(--bp-space-2);
3794
+ padding-left: var(--bp-space-1);
2986
3795
  }
2987
- :where(.state:nth-child(2n)) {
2988
- border-right: 0;
3796
+ :where(.wp-card) {
3797
+ background: var(--bp-paper);
3798
+ border: 1px solid var(--bp-edge);
3799
+ border-radius: var(--bp-radius-0);
3800
+ padding: 10px var(--bp-space-2);
3801
+ display: grid;
3802
+ grid-template-columns: minmax(0, 1fr);
3803
+ gap: 7px;
3804
+ min-width: 0;
2989
3805
  }
2990
- :where(.state-name, .states-label) {
2991
- font: 400 10px/1.4 var(--mono);
2992
- letter-spacing: 0.08em;
3806
+ :where(.wp-card__top) {
3807
+ display: flex;
3808
+ align-items: center;
3809
+ justify-content: space-between;
3810
+ }
3811
+ :where(.wp-card__name) {
3812
+ margin: 0;
3813
+ font-size: var(--bp-text-small);
3814
+ font-weight: var(--bp-weight-medium);
3815
+ line-height: 1.35;
3816
+ }
3817
+ :where(.wp-card--done .wp-card__name) { color: var(--bp-text-secondary); }
3818
+ :where(.wp-card__pr) { min-width: 0; overflow: hidden; }
3819
+ :where(.wp-card__pr .wp-pr) { font-size: var(--bp-label-sm); max-width: 100%; }
3820
+ :where(.wp-card__pr .wp-pr__ci) {
3821
+ overflow: hidden;
3822
+ text-overflow: ellipsis;
3823
+ }
3824
+ :where(.wp-card__foot) {
3825
+ display: flex;
3826
+ align-items: center;
3827
+ justify-content: space-between;
3828
+ gap: var(--bp-space-2);
3829
+ margin-top: 1px;
3830
+ }
3831
+ :where(.wp-card__wave) {
3832
+ font-family: var(--bp-mono);
3833
+ font-size: var(--bp-label-sm);
3834
+ letter-spacing: var(--bp-label-sm-ls);
2993
3835
  text-transform: uppercase;
2994
- color: var(--ink);
3836
+ color: var(--bp-text-secondary);
2995
3837
  }
2996
- :where(.state-name) {
2997
- margin-bottom: 6px;
3838
+
3839
+ /* ---- SWIMLANES view (type × wave grid) — single-line chips -- */
3840
+ :where(.wp-view--swim) {
3841
+ display: grid;
3842
+ grid-template-columns: 120px repeat(var(--wp-waves), minmax(168px, 1fr));
3843
+ gap: 1px;
3844
+ background: var(--bp-edge);
3845
+ border: 1px solid var(--bp-edge);
3846
+ border-radius: var(--bp-radius-0);
3847
+ overflow: hidden;
2998
3848
  }
2999
- :where(.state-desc) {
3000
- font-size: 13px;
3001
- line-height: 18px;
3002
- color: var(--text-secondary);
3849
+ :where(.wp-swim__corner),
3850
+ :where(.wp-swim__whead),
3851
+ :where(.wp-swim__lanehead),
3852
+ :where(.wp-swim__cell) { background: var(--bp-paper); padding: var(--bp-space-2); min-width: 0; }
3853
+ :where(.wp-swim__corner),
3854
+ :where(.wp-swim__whead) {
3855
+ font-family: var(--bp-mono);
3856
+ font-size: var(--bp-label-sm);
3857
+ letter-spacing: var(--bp-label-sm-ls);
3858
+ text-transform: uppercase;
3859
+ color: var(--bp-text-secondary);
3860
+ background: var(--bp-bg);
3003
3861
  }
3004
- :where(.states-label) {
3005
- margin-top: 26px;
3862
+ :where(.wp-swim__lanehead) { background: var(--bp-bg); display: flex; align-items: center; }
3863
+ :where(.wp-swim__cell) { display: grid; gap: 6px; align-content: start; }
3864
+ :where(.wp-chip) {
3865
+ display: grid;
3866
+ gap: 5px;
3867
+ padding: 7px 9px;
3868
+ border: 1px solid var(--bp-edge);
3869
+ border-radius: var(--bp-radius-0);
3870
+ background: var(--bp-paper);
3871
+ min-width: 0;
3006
3872
  }
3007
- :where(.states-label + .states) {
3008
- margin-top: 10px;
3873
+ :where(.wp-chip__head) {
3874
+ display: grid;
3875
+ grid-template-columns: auto minmax(0, 1fr) auto;
3876
+ align-items: center;
3877
+ gap: 7px;
3009
3878
  }
3010
- :where(.figure) {
3011
- width: fit-content;
3012
- max-width: 100%;
3013
- margin: 32px auto;
3879
+ :where(.wp-chip__name) {
3880
+ font-size: var(--bp-text-small);
3881
+ font-weight: var(--bp-weight-medium);
3882
+ line-height: 1.2;
3883
+ white-space: nowrap;
3884
+ overflow: hidden;
3885
+ text-overflow: ellipsis;
3014
3886
  }
3015
- :where(.figure svg[viewBox]) {
3016
- display: block;
3017
- width: auto;
3018
- max-width: min(100%, var(--bp-diagram-w));
3019
- height: auto;
3887
+ :where(.wp-chip--done .wp-chip__name) { color: var(--bp-text-secondary); }
3888
+ :where(.wp-chip__pr) { min-width: 0; overflow: hidden; }
3889
+ :where(.wp-chip__pr .wp-pr) { font-size: var(--bp-label-sm); max-width: 100%; }
3890
+ :where(.wp-chip[data-locked="true"]) { border-style: dashed; }
3891
+
3892
+ /* ---- GANTT view (longest-path schedule) --------------------- */
3893
+ :where(.wp-view--gantt) { display: grid; gap: 4px; }
3894
+ :where(.wp-gantt__row) {
3895
+ display: grid;
3896
+ grid-template-columns: 264px 1fr;
3897
+ gap: var(--bp-space-3);
3898
+ align-items: center;
3020
3899
  }
3021
- :where(.figure svg:not([viewBox])) {
3022
- display: block;
3023
- width: auto;
3024
- max-width: 100%;
3025
- height: auto;
3900
+ :where(.wp-gantt__label) {
3901
+ display: grid;
3902
+ grid-template-columns: 84px minmax(0, 1fr) auto;
3903
+ gap: var(--bp-space-2);
3904
+ align-items: center;
3905
+ min-width: 0;
3026
3906
  }
3027
- :where(.fig-node) {
3028
- fill: var(--paper);
3029
- stroke: var(--bp-illustration);
3030
- stroke-width: 1.5;
3907
+ :where(.wp-gantt__name) {
3908
+ font-size: var(--bp-text-small);
3909
+ font-weight: var(--bp-weight-medium);
3910
+ white-space: nowrap;
3911
+ overflow: hidden;
3912
+ text-overflow: ellipsis;
3031
3913
  }
3032
- :where(.fig-node-amb) {
3033
- fill: var(--bp-illustration-fill);
3034
- stroke: var(--bp-illustration);
3035
- stroke-width: 1.5;
3914
+ :where(.wp-gantt__row--done .wp-gantt__name) { color: var(--bp-text-secondary); }
3915
+ :where(.wp-gantt__track) {
3916
+ position: relative;
3917
+ height: 26px;
3918
+ background: var(--bp-fill-amb);
3919
+ border-radius: var(--bp-radius-0);
3920
+ background-image: repeating-linear-gradient(
3921
+ to right,
3922
+ oklch(0 0 0 / 0) 0 calc(100% / var(--span, 10) - 1px),
3923
+ var(--bp-edge) calc(100% / var(--span, 10) - 1px) calc(100% / var(--span, 10))
3924
+ );
3036
3925
  }
3037
- :where(.fig-edge) {
3038
- fill: none;
3039
- stroke: var(--bp-illustration);
3040
- stroke-width: 1.5;
3926
+ :where(.wp-gantt__bar) {
3927
+ position: absolute;
3928
+ top: 3px;
3929
+ bottom: 3px;
3930
+ display: flex;
3931
+ align-items: center;
3932
+ gap: 6px;
3933
+ padding: 0 8px;
3934
+ border-radius: var(--bp-radius-0);
3935
+ border: 1px solid var(--bp-ink-line);
3936
+ background: var(--bp-paper);
3937
+ min-width: 28px;
3938
+ }
3939
+ :where(.wp-gantt__bar--done) { background: var(--bp-ink); border-color: var(--bp-ink); }
3940
+ :where(.wp-gantt__bar--done .wp-gantt__bar-label),
3941
+ :where(.wp-gantt__bar--done .wp-dot) { color: var(--bp-paper); }
3942
+ :where(.wp-gantt__bar--done .wp-dot--done) { background: var(--bp-paper); }
3943
+ :where(.wp-gantt__bar--active) { background: var(--bp-fill-hi); border-color: var(--bp-ink); }
3944
+ :where(.wp-gantt__bar--blocked) { border-style: dashed; }
3945
+ :where(.wp-gantt__bar-label) {
3946
+ font-family: var(--bp-mono);
3947
+ font-size: var(--bp-label-sm);
3948
+ letter-spacing: 0.02em;
3949
+ color: var(--bp-text-secondary);
3041
3950
  }
3042
- :where(.fig-label) {
3043
- font: 400 11px/1 var(--mono);
3044
- fill: var(--text);
3951
+ :where(.wp-gantt__track--axis) {
3952
+ position: relative;
3953
+ height: 18px;
3954
+ background: none;
3955
+ background-image: none;
3045
3956
  }
3046
- :where(.fig-label-soft) {
3047
- font: 400 9px/1 var(--mono);
3048
- fill: var(--text-secondary);
3957
+ :where(.wp-gantt__tick) {
3958
+ position: absolute;
3959
+ left: calc(var(--at) / var(--span) * 100%);
3960
+ transform: translateX(-50%);
3961
+ font-family: var(--bp-mono);
3962
+ font-size: var(--bp-label-sm);
3963
+ color: var(--bp-text-secondary);
3964
+ font-variant-numeric: tabular-nums;
3049
3965
  }
3050
- :where(.fig-cap) {
3051
- font: 400 10px/1.4 var(--mono);
3052
- letter-spacing: 0.06em;
3966
+ :where(.wp-gantt__label--axis) {
3967
+ display: block;
3968
+ font-family: var(--bp-mono);
3969
+ font-size: var(--bp-label-sm);
3970
+ letter-spacing: var(--bp-label-sm-ls);
3053
3971
  text-transform: uppercase;
3054
- color: var(--text-secondary);
3972
+ color: var(--bp-text-secondary);
3973
+ }
3974
+ :where(.wp-gantt__row--axis) { border-bottom: 1px solid var(--bp-edge); padding-bottom: 4px; }
3975
+
3976
+ /* ---- EMPTY / no-data state ---------------------------------- */
3977
+ :where(.wp-empty) {
3978
+ display: grid;
3979
+ place-items: center;
3980
+ gap: var(--bp-space-2);
3055
3981
  text-align: center;
3056
- margin-top: 14px;
3982
+ padding: var(--bp-space-6) var(--bp-space-4);
3983
+ border: 1px dashed var(--bp-ink-line);
3984
+ border-radius: var(--bp-radius-0);
3985
+ background-color: var(--bp-fill-amb);
3986
+ background-image: var(--bp-hatch);
3987
+ color: var(--bp-text-secondary);
3988
+ }
3989
+ :where(.wp-empty__art) { color: var(--bp-ink-soft); margin-bottom: var(--bp-space-1); }
3990
+ :where(.wp-empty__title) {
3991
+ margin: 0;
3992
+ font-size: var(--bp-text-h4);
3993
+ font-weight: var(--bp-weight-strong);
3994
+ color: var(--bp-text);
3995
+ }
3996
+ :where(.wp-empty__hint) { margin: 0; max-width: 42ch; font-size: var(--bp-text-small); }
3997
+ :where(.wp-mono) { font-family: var(--bp-mono); font-size: 0.92em; }
3998
+
3999
+ /* ---- responsive: collapse the dense grids on narrow containers */
4000
+ @container (max-width: 560px) {
4001
+ :where(.wp-row) { grid-template-columns: auto minmax(0, 1fr) auto; }
4002
+ :where(.wp-row .wp-type) { display: none; }
4003
+ :where(.wp-row__pr) { grid-column: 2 / -1; justify-self: start; }
4004
+ :where(.wp-gantt__row) { grid-template-columns: 1fr; gap: 4px; }
3057
4005
  }
3058
4006
  }
3059
4007
 
@@ -3107,6 +4055,29 @@
3107
4055
  :where(.bp-ease-overshoot) { transition-timing-function: var(--bp-ease-overshoot); }
3108
4056
  :where(.bp-ease-linear) { transition-timing-function: var(--bp-ease-linear); }
3109
4057
 
4058
+ /* Theme flip — mute per-element transitions while the root crossfade runs.
4059
+ TOC links, inputs, and other token-bound surfaces bake color motion into
4060
+ @layer components; that fights the view transition the same way
4061
+ .bp-transition-colors utilities do. blueprint.js sets
4062
+ data-bp-theme-switching for the duration of startViewTransition().
4063
+ Keep the data-attribute and typed selectors in separate rules — an
4064
+ unknown :active-view-transition-type() in a comma list invalidates the
4065
+ whole block in engines that lack typed View Transition selector support. */
4066
+ html[data-bp-theme-switching] :where(*) {
4067
+ transition-property: none;
4068
+ }
4069
+ html:active-view-transition-type(bp-theme) :where(*) {
4070
+ transition-property: none;
4071
+ }
4072
+ html[data-bp-theme-switching] *::before,
4073
+ html[data-bp-theme-switching] *::after {
4074
+ transition-property: none;
4075
+ }
4076
+ html:active-view-transition-type(bp-theme) *::before,
4077
+ html:active-view-transition-type(bp-theme) *::after {
4078
+ transition-property: none;
4079
+ }
4080
+
3110
4081
  /* Responsive: keep section navigation reachable while collapsing the rail. */
3111
4082
  @media (max-width: 860px) {
3112
4083
  :where(.bp-sidebar, .bp-toc, .sidebar) {
@@ -3126,6 +4097,16 @@
3126
4097
  padding-bottom: var(--bp-space-1);
3127
4098
  }
3128
4099
  :where(.bp-sidebar li, .bp-toc li, .sidebar li) { flex: 0 0 auto; }
4100
+ /* In the collapsed horizontal nav the group eyebrow rides inline as a
4101
+ quiet separator ahead of its entries rather than a stacked header. */
4102
+ :where(.bp-sidebar .bp-nav-group, .bp-toc .bp-nav-group) {
4103
+ display: flex;
4104
+ align-items: center;
4105
+ margin: 0;
4106
+ }
4107
+ :where(.bp-nav-group__label) {
4108
+ padding: var(--bp-space-1) var(--bp-space-2);
4109
+ }
3129
4110
  /* The gliding pill only makes sense in the fixed column; in the collapsed
3130
4111
  horizontal nav the active entry just shows its own static pill. */
3131
4112
  :where(.bp-sidebar > ul, .bp-sidebar__panel > ul, .bp-toc > ul)::before { content: none; }
@@ -3133,7 +4114,7 @@
3133
4114
  background: var(--bp-fill-hi);
3134
4115
  }
3135
4116
  /* Rail is now on top; the sheet floats with a uniform margin. */
3136
- :where(html:has(.bp-shell)) {
4117
+ :where(html[data-bp-document]) {
3137
4118
  --bp-shell-inset-block: var(--bp-space-3);
3138
4119
  --bp-shell-inset-top: var(--bp-shell-inset-block);
3139
4120
  --bp-shell-inset-left: var(--bp-space-3);
@@ -3145,26 +4126,29 @@
3145
4126
  :where(main, article) {
3146
4127
  padding: var(--bp-space-4) var(--bp-space-3);
3147
4128
  }
3148
- /* Edge-to-edge on phones — the float costs too much width here. */
3149
- :where(body:has(.bp-shell)) {
3150
- overflow: auto;
4129
+ /* Edge-to-edge on phones — the desk float costs too much width here. */
4130
+ :where(html[data-bp-document] body) {
4131
+ padding-top: 0;
4132
+ padding-bottom: 0;
3151
4133
  }
3152
- :where(.bp-shell) {
3153
- position: static;
3154
- inset: auto;
4134
+ :where(html[data-bp-document] main) {
3155
4135
  margin: 0;
3156
- overflow: visible;
4136
+ max-width: var(--bp-content);
4137
+ min-height: 0;
3157
4138
  border: 0;
3158
4139
  border-radius: var(--bp-radius-0);
3159
4140
  box-shadow: none;
3160
4141
  }
3161
- :where(html:has(.bp-shell))::before,
3162
- :where(html:has(.bp-shell))::after,
3163
- :where(body:has(.bp-shell))::before,
3164
- :where(body:has(.bp-shell))::after {
4142
+ :where(html[data-bp-document] main > *) {
4143
+ max-width: none;
4144
+ }
4145
+ :where(html[data-bp-document])::before,
4146
+ :where(html[data-bp-document])::after,
4147
+ :where(html[data-bp-document] body)::before,
4148
+ :where(html[data-bp-document] body)::after {
3165
4149
  content: none;
3166
4150
  }
3167
- :where(.bp-option-grid, .bp-state-grid, .bp-card-grid, .bp-layout) {
4151
+ :where(.bp-card-grid, .bp-layout) {
3168
4152
  grid-template-columns: 1fr;
3169
4153
  }
3170
4154
  :where(.bp-content) {
@@ -3185,22 +4169,25 @@
3185
4169
  :where(.bp-sidebar, .bp-toc, .sidebar, .scroll-progress, .bp-no-print) {
3186
4170
  display: none !important;
3187
4171
  }
3188
- :where(.bp-shell, .page-body, .sheet) { margin: 0; border: 0; border-radius: var(--bp-radius-0); box-shadow: none; }
3189
- :where(body:has(.bp-shell)) {
3190
- overflow: visible;
4172
+ :where(html[data-bp-document] main, .page-body, .sheet) { margin: 0; border: 0; border-radius: var(--bp-radius-0); box-shadow: none; }
4173
+ :where(html[data-bp-document] body) {
4174
+ padding-top: 0;
4175
+ padding-bottom: 0;
3191
4176
  }
3192
- :where(.bp-shell) {
3193
- position: static;
3194
- inset: auto;
4177
+ :where(html[data-bp-document] main) {
4178
+ min-height: 0;
3195
4179
  overflow: visible;
3196
4180
  }
3197
- :where(html:has(.bp-shell))::before,
3198
- :where(html:has(.bp-shell))::after,
3199
- :where(body:has(.bp-shell))::before,
3200
- :where(body:has(.bp-shell))::after {
4181
+ :where(html[data-bp-document] main > *) {
4182
+ max-width: none;
4183
+ }
4184
+ :where(html[data-bp-document])::before,
4185
+ :where(html[data-bp-document])::after,
4186
+ :where(html[data-bp-document] body)::before,
4187
+ :where(html[data-bp-document] body)::after {
3201
4188
  content: none;
3202
4189
  }
3203
- :where(.bp-decision, .bp-callout, .bp-card, .bp-section, .bp-figure, figure, .bp-option-grid, details) {
4190
+ :where(.bp-callout, .bp-card, .bp-section, .bp-figure, figure, details) {
3204
4191
  break-inside: avoid;
3205
4192
  }
3206
4193
  }