@obvi/blueprint 1.1.3 → 1.3.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.
@@ -289,14 +289,19 @@
289
289
  /* Corner radius — an even 2px-step scale; each token names its pixel
290
290
  value directly (corners read as absolute, so px is the honest unit).
291
291
  -6 is the document default for sheets, panels, and chrome; -2 dresses
292
- inline marks, -4 small code blocks, -8 is the soft top rung. */
292
+ inline marks, -4 small code blocks, -8 is decision containers, -12 is
293
+ the Obvious shell panel rung (dropdowns), -16 is the search command menu. */
293
294
  --bp-radius-0: 0;
294
295
  --bp-radius-2: 2px;
295
296
  --bp-radius-4: 4px;
296
297
  --bp-radius-6: 6px;
297
298
  --bp-radius-8: 8px;
299
+ --bp-radius-12: 12px;
300
+ --bp-radius-16: 16px;
298
301
  --bp-radius-pill: 9999px;
299
302
  --bp-radius-round: 50%;
303
+ /* Runtime shell overlays (doc switcher, contents dropdown, search palette). */
304
+ --bp-shell-panel-radius: var(--bp-radius-12);
300
305
 
301
306
  /* Obvious button palette — the interactive-control vocabulary. Buttons are
302
307
  the one chrome surface that carries fill: primary (committed / CTA),
@@ -511,6 +516,8 @@
511
516
  /* ---- Tier 1a: page + body ---------------------------------------- */
512
517
  :where(html) {
513
518
  font-size: 16px;
519
+ background: var(--bp-bg);
520
+ overscroll-behavior: none;
514
521
  }
515
522
  :where(body) {
516
523
  font-family: var(--bp-serif);
@@ -579,19 +586,11 @@
579
586
  margin-bottom: var(--bp-space-2);
580
587
  text-wrap: var(--bp-wrap-heading);
581
588
  }
582
- /* Document masthead — the first <header> in <main>. No implicit, position-
583
- based styling: author the eyebrow with <bp-eyebrow> and the lede with
584
- <bp-subheader> explicitly. We only reset the title's top margin and tune
585
- the gap before a following contents index. */
586
- :where(main > header:first-child) {
587
- margin-bottom: 0;
588
- }
589
+ /* Document masthead — the first <header> in <main>. Its title starts flush,
590
+ and a following contents index keeps the masthead's own bottom gap. */
589
591
  :where(main > header:first-child > h1) {
590
592
  margin: 0 0 var(--bp-space-2);
591
593
  }
592
- :where(main > header:first-child:has(+ :is(.bp-contents, bp-toc))) {
593
- margin-bottom: var(--bp-space-4);
594
- }
595
594
  :where(main > header:first-child + :is(.bp-contents, bp-toc)) {
596
595
  margin-top: 0;
597
596
  }
@@ -1023,6 +1022,14 @@
1023
1022
  margin-bottom: var(--bp-space-5);
1024
1023
  border-bottom: 0;
1025
1024
  }
1025
+ /* The first main header is the canonical hero band; generic body/article
1026
+ headers remain compact metadata landmarks. */
1027
+ :where(main > header:first-child) {
1028
+ padding-top: var(--bp-space-6);
1029
+ padding-bottom: var(--bp-space-4);
1030
+ margin-bottom: var(--bp-space-5);
1031
+ border-bottom: 1px solid var(--bp-ink-line);
1032
+ }
1026
1033
  /* Subordinate sources / status region */
1027
1034
  :where(body > footer, main > footer, article > footer) {
1028
1035
  margin-top: var(--bp-space-7);
@@ -1205,6 +1212,10 @@
1205
1212
  line-height: var(--bp-lh-lede);
1206
1213
  color: var(--bp-text);
1207
1214
  }
1215
+ :where(main > header:first-child > bp-subheader) {
1216
+ text-align: justify;
1217
+ hyphens: auto;
1218
+ }
1208
1219
 
1209
1220
  /* ---- Section heading permalink + eyebrow --------------------------------
1210
1221
  Injected by blueprint.js on headings with data-sidebar: a numbered eyebrow
@@ -1353,6 +1364,12 @@
1353
1364
  :where(bp-eyebrow[muted]) {
1354
1365
  color: var(--bp-text-secondary);
1355
1366
  }
1367
+ :where(main > header:first-child > bp-eyebrow) {
1368
+ font-size: var(--bp-label-lg);
1369
+ letter-spacing: var(--bp-label-lg-ls);
1370
+ color: var(--bp-ink-soft);
1371
+ margin-bottom: var(--bp-space-3);
1372
+ }
1356
1373
  :where(.bp-meta) {
1357
1374
  font-family: var(--bp-mono);
1358
1375
  font-size: var(--bp-label-md);
@@ -1680,7 +1697,8 @@
1680
1697
  border-radius: var(--bp-radius-pill);
1681
1698
  transition: background var(--bp-duration-fast) var(--bp-ease-out),
1682
1699
  color var(--bp-duration-fast) var(--bp-ease-out),
1683
- box-shadow var(--bp-duration-fast) var(--bp-ease-out);
1700
+ box-shadow var(--bp-duration-fast) var(--bp-ease-out),
1701
+ transform var(--bp-duration-fast) var(--bp-ease-out);
1684
1702
  }
1685
1703
  :where(.bp-choice__seg-opt:hover) {
1686
1704
  color: var(--bp-button-ghost-fg-hover);
@@ -1716,7 +1734,8 @@
1716
1734
  color: var(--bp-button-secondary-fg);
1717
1735
  background: var(--bp-button-secondary-bg);
1718
1736
  transition: background var(--bp-duration-fast) var(--bp-ease-out),
1719
- color var(--bp-duration-fast) var(--bp-ease-out);
1737
+ color var(--bp-duration-fast) var(--bp-ease-out),
1738
+ transform var(--bp-duration-fast) var(--bp-ease-out);
1720
1739
  }
1721
1740
  :where(.bp-choice__adopt:hover) {
1722
1741
  background: var(--bp-button-secondary-bg-hover);
@@ -1913,7 +1932,8 @@
1913
1932
  color: var(--bp-button-ghost-fg);
1914
1933
  background: var(--bp-button-tertiary-bg);
1915
1934
  transition: background var(--bp-duration-fast) var(--bp-ease-out),
1916
- color var(--bp-duration-fast) var(--bp-ease-out);
1935
+ color var(--bp-duration-fast) var(--bp-ease-out),
1936
+ transform var(--bp-duration-fast) var(--bp-ease-out);
1917
1937
  }
1918
1938
  :where(.bp-choice__compare > summary:hover) {
1919
1939
  background: var(--bp-button-tertiary-bg-hover);
@@ -2021,7 +2041,8 @@
2021
2041
  background: oklch(0 0 0 / 0);
2022
2042
  transition: background var(--bp-duration-fast) var(--bp-ease-out),
2023
2043
  color var(--bp-duration-fast) var(--bp-ease-out),
2024
- border-color var(--bp-duration-fast) var(--bp-ease-out);
2044
+ border-color var(--bp-duration-fast) var(--bp-ease-out),
2045
+ transform var(--bp-duration-fast) var(--bp-ease-out);
2025
2046
  }
2026
2047
  :where(.bp-preflight__chip:hover) {
2027
2048
  border-color: var(--bp-button-secondary-bd);
@@ -2077,12 +2098,27 @@
2077
2098
  cursor: pointer;
2078
2099
  margin-top: var(--bp-space-2);
2079
2100
  transition: background var(--bp-duration-fast) var(--bp-ease-out),
2080
- border-color var(--bp-duration-fast) var(--bp-ease-out);
2101
+ border-color var(--bp-duration-fast) var(--bp-ease-out),
2102
+ transform var(--bp-duration-fast) var(--bp-ease-out);
2081
2103
  }
2082
2104
  :where(.bp-preflight__draft:hover) {
2083
2105
  background: var(--bp-button-primary-bg-hover);
2084
2106
  border-color: var(--bp-button-primary-bg-hover);
2085
2107
  }
2108
+
2109
+ /* Press feedback — the decision controls nudge 1px down on :active to match
2110
+ the Obvious app's button press. Motion only: the squared/pill split, the
2111
+ --bp-button-* fills, and the radius contract are all unchanged. Reduced
2112
+ motion is handled globally, so no per-rule query here. */
2113
+ :where(
2114
+ .bp-choice__seg-opt,
2115
+ .bp-choice__adopt,
2116
+ .bp-choice__compare > summary,
2117
+ .bp-preflight__chip,
2118
+ .bp-preflight__draft
2119
+ ):active {
2120
+ transform: translateY(1px);
2121
+ }
2086
2122
  :where(.bp-preflight__footer) {
2087
2123
  display: flex;
2088
2124
  align-items: center;
@@ -2635,6 +2671,9 @@
2635
2671
  border: 1px solid var(--bp-ink-line);
2636
2672
  margin-top: var(--bp-space-4);
2637
2673
  }
2674
+ :where(main > header:first-child > .bp-title-block) {
2675
+ margin-top: var(--bp-space-5);
2676
+ }
2638
2677
  :where(.bp-tb-cell) {
2639
2678
  flex: 0 0 auto;
2640
2679
  padding: var(--bp-space-2) var(--bp-space-3);
@@ -2776,7 +2815,7 @@
2776
2815
  }
2777
2816
  /* The contents list hosts a single rounded ACTIVE PILL that glides between
2778
2817
  entries as you scroll — no rail, no left border. Obvious-style. */
2779
- :where(.bp-sidebar > ul, .bp-sidebar__panel > ul, .bp-toc > ul) {
2818
+ :where(.bp-sidebar > ul, .bp-toc-switcher > ul, .bp-toc > ul) {
2780
2819
  position: relative;
2781
2820
  counter-reset: bp-toc;
2782
2821
  margin: 0;
@@ -2789,7 +2828,7 @@
2789
2828
  }
2790
2829
  /* The gliding active pill, drawn behind the entries and travelling via
2791
2830
  --bp-toc-y / --bp-toc-h (set by blueprint.js). */
2792
- :where(.bp-sidebar > ul, .bp-sidebar__panel > ul, .bp-toc > ul)::before {
2831
+ :where(.bp-sidebar > ul, .bp-toc-switcher > ul, .bp-toc > ul)::before {
2793
2832
  content: "";
2794
2833
  position: absolute;
2795
2834
  left: 0;
@@ -2807,7 +2846,7 @@
2807
2846
  transition-duration: var(--bp-duration-slow);
2808
2847
  transition-timing-function: var(--bp-ease-out);
2809
2848
  }
2810
- :where(.bp-sidebar > ul > li, .bp-sidebar__panel > ul > li, .bp-toc > ul > li) {
2849
+ :where(.bp-sidebar > ul > li, .bp-toc-switcher > ul > li, .bp-toc > ul > li) {
2811
2850
  counter-increment: bp-toc;
2812
2851
  margin: 0;
2813
2852
  padding: 0;
@@ -2833,7 +2872,7 @@
2833
2872
  transition: none;
2834
2873
  }
2835
2874
  /* Auto-numbered index (01, 02…), tabular so the digits line up. */
2836
- :where(.bp-sidebar > ul > li > a, .bp-sidebar__panel > ul > li > a, .bp-toc > ul > li > a)::before {
2875
+ :where(.bp-sidebar > ul > li > a, .bp-toc-switcher > ul > li > a, .bp-toc > ul > li > a)::before {
2837
2876
  content: counter(bp-toc, decimal-leading-zero);
2838
2877
  flex: 0 0 var(--bp-toc-index-w, 14px);
2839
2878
  min-width: var(--bp-toc-index-w, 14px);
@@ -2880,9 +2919,6 @@
2880
2919
  :where(.bp-sidebar a:hover, .bp-toc a:hover) {
2881
2920
  color: var(--bp-ink);
2882
2921
  background: var(--bp-fill-amb);
2883
- transition:
2884
- color var(--bp-duration-fast) var(--bp-ease),
2885
- background-color var(--bp-duration-fast) var(--bp-ease);
2886
2922
  }
2887
2923
  :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) {
2888
2924
  color: var(--bp-ink);
@@ -2891,7 +2927,6 @@
2891
2927
  }
2892
2928
  :where(.bp-sidebar a:hover::before, .bp-toc a:hover::before) {
2893
2929
  color: var(--bp-ink);
2894
- transition: color var(--bp-duration-fast) var(--bp-ease);
2895
2930
  }
2896
2931
  :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) {
2897
2932
  color: var(--bp-ink);
@@ -4251,10 +4286,16 @@
4251
4286
  margin: 0;
4252
4287
  }
4253
4288
  /* Expanded mock floats fullscreen (position: fixed): collapse the host so
4254
- no empty dotted mat is left behind in the document flow. */
4289
+ no empty dotted mat is left behind in the document flow, and clear any
4290
+ host clipping / board paint so browsers that clip fixed descendants
4291
+ through overflow-hidden ancestors still show the overlay fullscreen. */
4255
4292
  :where(bp-mock:has(.bp-mock.is-expanded)) {
4256
4293
  margin: 0;
4257
4294
  padding: 0;
4295
+ overflow: visible;
4296
+ border-radius: 0;
4297
+ background-color: oklch(1 0 0 / 0);
4298
+ background-image: none;
4258
4299
  }
4259
4300
 
4260
4301
  /* Single-surface specimens read as ONE continuous paper card on the mat;
@@ -4425,7 +4466,7 @@
4425
4466
  }
4426
4467
  /* The gliding pill only makes sense in the fixed column; in the collapsed
4427
4468
  horizontal nav the active entry just shows its own static pill. */
4428
- :where(.bp-sidebar > ul, .bp-sidebar__panel > ul, .bp-toc > ul)::before { content: none; }
4469
+ :where(.bp-sidebar > ul, .bp-toc-switcher > ul, .bp-toc > ul)::before { content: none; }
4429
4470
  :where(.bp-sidebar a[aria-current="location"], .bp-toc a[aria-current="location"]) {
4430
4471
  background: var(--bp-fill-hi);
4431
4472
  }
@@ -4694,9 +4735,10 @@ body > .site-nav .site-nav__theme:hover {
4694
4735
  border: 1px solid var(--bp-edge);
4695
4736
  border-radius: var(--bp-radius-pill);
4696
4737
  background: var(--bp-paper);
4697
- /* Hover-only color motion — idle fields snap on theme flip so the root
4698
- view-transition crossfade isn't fighting per-field token tweens. */
4699
- transition: none;
4738
+ /* Color motion is hover-only so idle fields snap on theme flip and don't
4739
+ fight the root view-transition crossfade; transform is exempt (not a
4740
+ color) so the press nudge can ease in every state. */
4741
+ transition: transform var(--bp-duration-fast) var(--bp-ease-out);
4700
4742
  }
4701
4743
  .sidebar-field:hover,
4702
4744
  .sidebar-field:focus-within {
@@ -4704,7 +4746,13 @@ body > .site-nav .site-nav__theme:hover {
4704
4746
  background: var(--bp-fill-amb);
4705
4747
  transition:
4706
4748
  border-color var(--bp-duration-fast) var(--bp-ease-out),
4707
- background-color var(--bp-duration-fast) var(--bp-ease-out);
4749
+ background-color var(--bp-duration-fast) var(--bp-ease-out),
4750
+ transform var(--bp-duration-fast) var(--bp-ease-out);
4751
+ }
4752
+ /* Press feedback — match the Obvious app: the select triggers (doc switcher,
4753
+ contents dropdown) and the search trigger nudge 1px down on press. */
4754
+ .sidebar-field:active {
4755
+ transform: translateY(1px);
4708
4756
  }
4709
4757
  .doc-switcher__current {
4710
4758
  display: flex;
@@ -4723,7 +4771,7 @@ body > .site-nav .site-nav__theme:hover {
4723
4771
  display: none;
4724
4772
  }
4725
4773
  .doc-switcher__current::before {
4726
- content: none !important;
4774
+ content: none;
4727
4775
  }
4728
4776
  .doc-switcher__label {
4729
4777
  display: flex;
@@ -4760,7 +4808,7 @@ body > .site-nav .site-nav__theme:hover {
4760
4808
  padding: var(--bp-space-1);
4761
4809
  border: 1px solid var(--bp-edge);
4762
4810
  /* Container radius − padding = item radius, so the rows nest perfectly. */
4763
- border-radius: var(--bp-radius-8, 8px);
4811
+ border-radius: var(--bp-shell-panel-radius, var(--bp-radius-12, 12px));
4764
4812
  background: var(--bp-paper);
4765
4813
  box-shadow: var(--bp-shadow-pop);
4766
4814
  transform-origin: top center;
@@ -4787,7 +4835,7 @@ body > .site-nav .site-nav__theme:hover {
4787
4835
  justify-content: space-between;
4788
4836
  gap: var(--bp-space-2);
4789
4837
  padding: var(--bp-space-2) var(--bp-space-3);
4790
- border-radius: var(--bp-radius-4, 4px);
4838
+ border-radius: var(--bp-radius-8, 8px);
4791
4839
  text-decoration: none;
4792
4840
  }
4793
4841
  .doc-switcher__menu a:hover {
@@ -4829,6 +4877,130 @@ body > .site-nav .site-nav__theme:hover {
4829
4877
  opacity: 1;
4830
4878
  }
4831
4879
 
4880
+ /* =====================================================================
4881
+ Contents dropdown (.bp-toc-switcher)
4882
+ ---------------------------------------------------------------------
4883
+ The runtime wraps the contents list in a <details> so the rail can
4884
+ collapse into the same select-style control as the doc switcher once it
4885
+ becomes a horizontal band (≤860px). On the wide column the summary is
4886
+ hidden and blueprint.js keeps the <details> open, so the list renders as
4887
+ the plain vertical rail; only the narrow strip shows the trigger field +
4888
+ popover menu. ===================================================== */
4889
+ /* Reset the base <details> disclosure chrome (border/surface/padding) so the
4890
+ wrapper is invisible on the wide column — the list must read as the plain
4891
+ rail, not a boxed disclosure. */
4892
+ .bp-toc-switcher {
4893
+ position: relative;
4894
+ margin: 0;
4895
+ padding: 0;
4896
+ border: 0;
4897
+ background: none;
4898
+ }
4899
+ /* Trigger field — hidden on the wide column, shown as the dropdown summary on
4900
+ the narrow strip. Mirrors the doc-switcher current row. */
4901
+ .bp-toc-switcher__current {
4902
+ display: none;
4903
+ align-items: center;
4904
+ justify-content: space-between;
4905
+ gap: var(--bp-space-3);
4906
+ height: var(--bp-sidebar-toggle-size);
4907
+ margin: 0;
4908
+ padding: 0 var(--bp-space-3);
4909
+ list-style: none;
4910
+ font-weight: 400;
4911
+ cursor: pointer;
4912
+ }
4913
+ .bp-toc-switcher__current::-webkit-details-marker {
4914
+ display: none;
4915
+ }
4916
+ .bp-toc-switcher__current::before {
4917
+ content: none;
4918
+ }
4919
+ .bp-toc-switcher__label {
4920
+ display: flex;
4921
+ min-width: 0;
4922
+ }
4923
+ .bp-toc-switcher__title {
4924
+ min-width: 0;
4925
+ overflow: hidden;
4926
+ font-size: var(--bp-text-small);
4927
+ line-height: 1.3;
4928
+ font-weight: var(--bp-weight-medium);
4929
+ color: var(--bp-text);
4930
+ white-space: nowrap;
4931
+ text-overflow: ellipsis;
4932
+ }
4933
+ .bp-toc-switcher__chevron {
4934
+ display: inline-flex;
4935
+ flex: 0 0 auto;
4936
+ color: var(--bp-text-secondary);
4937
+ }
4938
+ .bp-toc-switcher[open] .bp-toc-switcher__chevron {
4939
+ transform: rotate(180deg);
4940
+ }
4941
+
4942
+ @media (max-width: 860px) {
4943
+ /* The rail is a horizontal band here — let the dropdown's popover escape it
4944
+ instead of being clipped by the strip's own scroll box. Size to content so
4945
+ the rail's bottom border lands right after the footer (theme + author) — the
4946
+ fixed-rail full-viewport height (.site-nav ~ .bp-sidebar) would otherwise
4947
+ push it to the bottom of the screen with dead space between. */
4948
+ .bp-sidebar:has(.bp-toc-switcher) {
4949
+ overflow: visible;
4950
+ max-height: none;
4951
+ height: auto;
4952
+ }
4953
+ .bp-toc-switcher__current {
4954
+ display: flex;
4955
+ }
4956
+ /* Popover menu: float the contents below the trigger like the doc-switcher
4957
+ menu, replacing the inline horizontal strip. Native <details> hides it
4958
+ while collapsed, so only the open state needs the floating chrome. */
4959
+ .bp-toc-switcher[open] > ul {
4960
+ position: absolute;
4961
+ left: 0;
4962
+ right: 0;
4963
+ top: calc(100% + var(--bp-space-1));
4964
+ z-index: 300;
4965
+ display: block;
4966
+ max-height: min(60vh, 360px);
4967
+ overflow: hidden auto;
4968
+ gap: 0;
4969
+ margin: 0;
4970
+ padding: var(--bp-space-1);
4971
+ border: 1px solid var(--bp-edge);
4972
+ border-radius: var(--bp-radius-8, 8px);
4973
+ background: var(--bp-paper);
4974
+ box-shadow: var(--bp-shadow-pop);
4975
+ animation: bp-doc-switcher-in var(--bp-duration-normal) var(--bp-ease-out);
4976
+ }
4977
+ /* Nested sub-entries stack as rows too rather than the strip's inline run. */
4978
+ .bp-toc-switcher ul ul {
4979
+ display: block;
4980
+ overflow: visible;
4981
+ }
4982
+ /* Group eyebrow heads its run as a quiet stacked label, not an inline chip. */
4983
+ .bp-toc-switcher .bp-nav-group {
4984
+ display: block;
4985
+ margin: var(--bp-space-2) 0 var(--bp-space-1);
4986
+ }
4987
+ .bp-toc-switcher .bp-nav-group:first-child {
4988
+ margin-top: 0;
4989
+ }
4990
+ /* The gliding pill is suppressed on the strip; the active row carries its
4991
+ own fill, matching the doc-switcher's current row. */
4992
+ .bp-toc-switcher a[aria-current="location"] {
4993
+ background: var(--bp-fill-hi);
4994
+ }
4995
+ /* Menu rows snap on hover like the doc-switcher — no row color tween. */
4996
+ .bp-toc-switcher[open] ul a,
4997
+ .bp-toc-switcher[open] ul a:hover,
4998
+ .bp-toc-switcher[open] ul a::before,
4999
+ .bp-toc-switcher[open] ul a:hover::before {
5000
+ transition: none;
5001
+ }
5002
+ }
5003
+
4832
5004
  /* Sidebar search — header row beside the corner toggle, above the header scrim. */
4833
5005
  .sidebar-search {
4834
5006
  position: relative;
@@ -4895,8 +5067,8 @@ body > .site-nav .site-nav__theme:hover {
4895
5067
  Search command menu (docs chrome) — Mintlify-style palette
4896
5068
  ---------------------------------------------------------------------
4897
5069
  Opened from the sidebar trigger or ⌘K / Ctrl+K (see wireSearchPalette).
4898
- Reuses the rounded sidebar control family: paper surface, --bp-radius-8
4899
- panel with --bp-shadow-pop, --bp-radius-4 rows, the --bp-fill-amb /
5070
+ Reuses the rounded sidebar control family: paper surface, --bp-radius-16
5071
+ panel with --bp-shadow-pop, --bp-radius-8 rows, the --bp-fill-amb /
4900
5072
  --bp-fill-hi hover + selected surfaces, and the shared open animation.
4901
5073
  Chrome stays on the neutral ink scale. Query filtering is a plain
4902
5074
  substring match over the page's headings; richer search is deferred.
@@ -4942,17 +5114,17 @@ body > .site-nav .site-nav__theme:hover {
4942
5114
  max-height: min(560px, 80vh);
4943
5115
  overflow: hidden;
4944
5116
  border: 1px solid var(--bp-edge);
4945
- border-radius: var(--bp-radius-8, 8px);
5117
+ border-radius: var(--bp-radius-16, 16px);
4946
5118
  background: var(--bp-paper);
4947
5119
  box-shadow: var(--bp-shadow-pop);
4948
5120
  }
4949
5121
 
4950
- /* Search row */
5122
+ /* Search row — icon hugging the input via a tight gap. */
4951
5123
  .docs-search__field {
4952
5124
  display: flex;
4953
5125
  align-items: center;
4954
- gap: var(--bp-space-3);
4955
- padding: var(--bp-space-3) var(--bp-space-4);
5126
+ gap: var(--bp-space-2);
5127
+ padding: var(--bp-space-3);
4956
5128
  border-bottom: 1px solid var(--bp-edge);
4957
5129
  }
4958
5130
  .docs-search__icon {
@@ -4999,12 +5171,15 @@ body > .site-nav .site-nav__theme:hover {
4999
5171
  overflow-y: auto;
5000
5172
  padding: var(--bp-space-2);
5001
5173
  }
5174
+ .docs-search__results:has(> .docs-search__empty:only-child) {
5175
+ padding: 0;
5176
+ }
5002
5177
  /* Group label — machine voice (mono + uppercase) with a trailing count. */
5003
5178
  .docs-search__group {
5004
5179
  display: flex;
5005
5180
  align-items: center;
5006
5181
  gap: var(--bp-space-2);
5007
- padding: var(--bp-space-2) var(--bp-space-3) var(--bp-space-1);
5182
+ padding: var(--bp-space-1) var(--bp-space-3) var(--bp-space-1);
5008
5183
  font-family: var(--bp-mono);
5009
5184
  font-size: var(--bp-label-md);
5010
5185
  letter-spacing: var(--bp-label-md-ls);
@@ -5020,12 +5195,11 @@ body > .site-nav .site-nav__theme:hover {
5020
5195
  Rows are <a> for navigation; suppress the base prose link underline except
5021
5196
  on the selected/hovered surface (background carries the affordance). */
5022
5197
  .docs-search__row {
5023
- display: grid;
5024
- grid-template-columns: 1fr auto;
5025
- align-items: center;
5026
- gap: var(--bp-space-1) var(--bp-space-3);
5027
- padding: var(--bp-space-2) var(--bp-space-3);
5028
- border-radius: var(--bp-radius-4, 4px);
5198
+ display: flex;
5199
+ align-items: flex-start;
5200
+ gap: var(--bp-space-1);
5201
+ padding: var(--bp-space-1) var(--bp-space-2);
5202
+ border-radius: var(--bp-radius-8, 8px);
5029
5203
  cursor: pointer;
5030
5204
  text-decoration: none;
5031
5205
  color: inherit;
@@ -5039,13 +5213,31 @@ body > .site-nav .site-nav__theme:hover {
5039
5213
  background: var(--bp-fill-hi);
5040
5214
  text-decoration: none;
5041
5215
  }
5216
+ /* Leading icon cell — a fixed square holding the heading-anchor "#" in the
5217
+ machine voice, so every row aligns on one icon column. */
5218
+ .docs-search__icon-box {
5219
+ grid-column: 1;
5220
+ grid-row: 1 / -1;
5221
+ align-self: start;
5222
+ display: inline-flex;
5223
+ align-items: center;
5224
+ justify-content: center;
5225
+ width: 20px;
5226
+ height: 20px;
5227
+ font-family: var(--bp-mono);
5228
+ font-size: var(--bp-text-small);
5229
+ color: var(--bp-text-secondary);
5230
+ }
5042
5231
  .docs-search__row-main {
5043
- display: flex;
5044
- flex-direction: column;
5045
- gap: var(--bp-space-1);
5232
+ display: grid;
5233
+ flex: 1 1 auto;
5234
+ grid-template-columns: 20px 1fr;
5235
+ gap: 0;
5046
5236
  min-width: 0;
5047
5237
  }
5048
5238
  .docs-search__crumb {
5239
+ grid-column: 2;
5240
+ grid-row: 1;
5049
5241
  display: block;
5050
5242
  font-family: var(--bp-mono);
5051
5243
  font-size: var(--bp-label-md);
@@ -5056,22 +5248,22 @@ body > .site-nav .site-nav__theme:hover {
5056
5248
  text-overflow: ellipsis;
5057
5249
  }
5058
5250
  .docs-search__title {
5059
- display: flex;
5060
- align-items: baseline;
5061
- gap: var(--bp-space-1);
5251
+ grid-column: 2;
5252
+ grid-row: 2;
5253
+ display: block;
5062
5254
  min-width: 0;
5063
5255
  font-family: var(--bp-sans);
5064
5256
  font-size: var(--bp-text-small);
5065
- line-height: 1.3;
5066
- font-weight: var(--bp-weight-medium);
5257
+ line-height: var(--bp-lh-small);
5258
+ font-weight: var(--bp-weight-body);
5067
5259
  color: var(--bp-text);
5260
+ white-space: nowrap;
5261
+ overflow: hidden;
5262
+ text-overflow: ellipsis;
5068
5263
  }
5069
- /* The "#" anchor glyph borrows the machine voice so it reads as a heading
5070
- link, not prose. */
5071
- .docs-search__hash {
5072
- flex: 0 0 auto;
5073
- font-family: var(--bp-mono);
5074
- color: var(--bp-text-secondary);
5264
+ /* When a row has no breadcrumb, the title sits on the icon's line. */
5265
+ .docs-search__row-main:not(:has(.docs-search__crumb)) .docs-search__title {
5266
+ grid-row: 1;
5075
5267
  }
5076
5268
  .docs-search__snippet {
5077
5269
  overflow: hidden;
@@ -5091,8 +5283,15 @@ body > .site-nav .site-nav__theme:hover {
5091
5283
  .docs-search__enter {
5092
5284
  flex: 0 0 auto;
5093
5285
  align-self: center;
5094
- padding: 1px 5px;
5095
- font-size: var(--bp-label-md);
5286
+ display: inline-flex;
5287
+ align-items: center;
5288
+ justify-content: center;
5289
+ width: 20px;
5290
+ height: 20px;
5291
+ padding: 0;
5292
+ border: 0;
5293
+ background: none;
5294
+ font-size: var(--bp-text-small);
5096
5295
  color: var(--bp-text-secondary);
5097
5296
  opacity: 0;
5098
5297
  }
@@ -5101,64 +5300,110 @@ body > .site-nav .site-nav__theme:hover {
5101
5300
  opacity: 1;
5102
5301
  }
5103
5302
 
5104
- /* Empty state */
5303
+ /* Empty state — a quiet centered text stack, no icon. */
5105
5304
  .docs-search__empty {
5305
+ border-top: 1px solid var(--bp-edge);
5306
+ padding: var(--bp-space-2);
5307
+ font-size: var(--bp-text-small);
5308
+ line-height: var(--bp-lh-small);
5309
+ }
5310
+ .docs-search__empty-center {
5311
+ display: flex;
5312
+ align-items: center;
5313
+ justify-content: center;
5314
+ padding-block: var(--bp-space-5);
5315
+ }
5316
+ .docs-search__empty-copy {
5106
5317
  display: flex;
5107
5318
  flex-direction: column;
5108
5319
  align-items: center;
5109
- gap: var(--bp-space-2);
5110
- padding: var(--bp-space-6) var(--bp-space-4);
5111
5320
  text-align: center;
5112
5321
  }
5113
- .docs-search__empty-icon {
5114
- display: inline-flex;
5115
- color: var(--bp-text-secondary);
5116
- opacity: 0.6;
5117
- }
5118
5322
  .docs-search__empty-title {
5323
+ margin: 0;
5119
5324
  font-family: var(--bp-sans);
5120
5325
  font-size: var(--bp-text-small);
5121
- font-weight: var(--bp-weight-medium);
5122
- color: var(--bp-text);
5326
+ line-height: var(--bp-lh-small);
5327
+ font-weight: var(--bp-weight-body);
5328
+ color: var(--bp-text-secondary);
5329
+ text-align: center;
5123
5330
  }
5124
5331
  .docs-search__empty-note {
5125
- font-size: var(--bp-text-small);
5126
- color: var(--bp-text-secondary);
5332
+ margin: var(--bp-space-1) 0 0;
5333
+ font-family: var(--bp-sans);
5334
+ font-size: var(--bp-text-code);
5335
+ line-height: var(--bp-lh-code);
5336
+ color: var(--bp-ink-soft);
5337
+ text-align: center;
5127
5338
  }
5128
5339
 
5129
- /* Footer keyboard legend */
5340
+ /* Footer command rows. A quiet stack of secondary actions, each a full-width
5341
+ button whose icon, label, and chip step from secondary to primary ink on
5342
+ hover — no background wash — and trails a mono shortcut
5343
+ chip — the layout the source palette parks its workspace commands in. */
5130
5344
  .docs-search__foot {
5131
- display: flex;
5132
- flex-wrap: wrap;
5133
- align-items: center;
5134
- justify-content: space-between;
5135
- gap: var(--bp-space-1) var(--bp-space-3);
5136
- padding: var(--bp-space-2) var(--bp-space-4);
5137
5345
  border-top: 1px solid var(--bp-edge);
5138
- font-family: var(--bp-mono);
5139
- font-size: var(--bp-label-md);
5140
- letter-spacing: var(--bp-label-md-ls);
5141
- color: var(--bp-text-secondary);
5346
+ padding: var(--bp-space-2);
5142
5347
  }
5143
- .docs-search__keys {
5348
+ .docs-search__actions {
5349
+ display: flex;
5350
+ flex-direction: column;
5351
+ gap: var(--bp-space-1);
5352
+ }
5353
+ .docs-search__action {
5144
5354
  display: flex;
5145
- flex-wrap: wrap;
5146
5355
  align-items: center;
5147
- gap: var(--bp-space-1) var(--bp-space-3);
5356
+ gap: var(--bp-space-2);
5357
+ width: 100%;
5358
+ padding: var(--bp-space-1) var(--bp-space-2);
5359
+ border: 0;
5360
+ border-radius: var(--bp-radius-4, 4px);
5361
+ background: none;
5362
+ font-family: var(--bp-sans);
5363
+ font-size: var(--bp-text-small);
5364
+ line-height: var(--bp-lh-control);
5365
+ color: var(--bp-text-secondary);
5366
+ text-align: left;
5367
+ cursor: pointer;
5148
5368
  }
5149
- .docs-search__key {
5369
+ .docs-search__action:hover {
5370
+ color: var(--bp-text);
5371
+ }
5372
+ .docs-search__action:focus-visible {
5373
+ color: var(--bp-text);
5374
+ outline: none;
5375
+ box-shadow: inset 0 0 0 2px var(--bp-ink-line);
5376
+ }
5377
+ .docs-search__action-icon {
5378
+ flex: 0 0 auto;
5150
5379
  display: inline-flex;
5151
- align-items: center;
5152
- gap: var(--bp-space-1);
5380
+ color: inherit;
5153
5381
  }
5154
- .docs-search__foot kbd {
5155
- padding: 0 4px;
5156
- border-width: 1px;
5382
+ .docs-search__action-label {
5383
+ flex: 1 1 auto;
5384
+ min-width: 0;
5385
+ white-space: nowrap;
5386
+ overflow: hidden;
5387
+ text-overflow: ellipsis;
5388
+ }
5389
+ .docs-search__chip {
5390
+ flex: 0 0 auto;
5391
+ display: inline-flex;
5392
+ align-items: center;
5393
+ gap: 3px;
5394
+ padding: 1px var(--bp-space-1);
5395
+ border: 1px solid var(--bp-edge);
5157
5396
  border-radius: var(--bp-radius-2, 2px);
5397
+ background: var(--bp-fill-hi);
5398
+ font-family: var(--bp-mono);
5158
5399
  font-size: var(--bp-label-md);
5159
5400
  line-height: 1.5;
5160
5401
  color: var(--bp-text-secondary);
5161
5402
  }
5403
+ .docs-search__action:hover .docs-search__chip,
5404
+ .docs-search__action:focus-visible .docs-search__chip {
5405
+ color: var(--bp-text);
5406
+ }
5162
5407
 
5163
5408
  /* Sidebar footer scrim token. The footer (theme switch + author/date) gets a
5164
5409
  soft fade above it so the scrollable TOC dissolves into the rail rather than
@@ -5433,7 +5678,27 @@ body > .site-nav .site-nav__theme:hover {
5433
5678
  padding-top: var(--bp-space-6);
5434
5679
  padding-inline: var(--bp-sidebar-content-inset);
5435
5680
  padding-bottom: var(--bp-sidebar-scroll-padding-bottom);
5681
+ /* Mirror the footer clearance at the top: when the runtime scrolls the
5682
+ active entry into view (blueprint.js revealInRail), keep it below the
5683
+ header dissolve scrim instead of tucking under the corner toggle. */
5684
+ scroll-padding-top: var(--bp-sidebar-header-scrim);
5436
5685
  scroll-padding-bottom: var(--bp-sidebar-scroll-padding-bottom);
5686
+ display: flex;
5687
+ flex-direction: column;
5688
+ }
5689
+ /* Wrapper is mobile-only chrome; on the wide column its children join the
5690
+ panel flex in search → doc switcher → TOC order. */
5691
+ .sidebar-mobile-head {
5692
+ display: contents;
5693
+ }
5694
+ .sidebar-search {
5695
+ order: 1;
5696
+ }
5697
+ .doc-switcher {
5698
+ order: 2;
5699
+ }
5700
+ .bp-toc-switcher {
5701
+ order: 3;
5437
5702
  }
5438
5703
 
5439
5704
  /* Drop the switcher below the header scrim (search stays above it on z 300). */
@@ -5524,12 +5789,154 @@ body > .site-nav .site-nav__theme:hover {
5524
5789
  }
5525
5790
  }
5526
5791
 
5527
- /* Below the rail's fixed breakpoint it becomes a static horizontal nav,
5528
- where collapsing has no meaning hide the control entirely. */
5792
+ /* Below the rail's fixed breakpoint the chrome flattens into a static
5793
+ horizontal nav. The runtime chrome lives in this (unlayered) sheet, so its
5794
+ resting rules beat blueprint.css's layered responsive block — the flattened
5795
+ layout has to be reconciled here or the fixed-rail geometry leaks through. */
5529
5796
  @media (max-width: 860px) {
5797
+ :root {
5798
+ --bp-sidebar-mobile-head-h: calc(
5799
+ var(--bp-sidebar-toggle-size) + var(--bp-space-3) * 2
5800
+ );
5801
+ }
5802
+
5803
+ /* Hash / TOC jumps land below the pinned mobile head instead of tucking the
5804
+ heading under it. Framed layouts (701–860px) reserve the shell top band +
5805
+ head; phones and site-nav offsets are handled in their own blocks below. */
5806
+ html[data-bp-document] {
5807
+ --bp-scroll-margin: calc(
5808
+ var(--bp-sidebar-mobile-head-h) + var(--bp-space-2)
5809
+ );
5810
+ }
5811
+
5812
+ /* Collapsing has no meaning once the rail is a horizontal strip. */
5530
5813
  .sidebar-toggle {
5531
5814
  display: none;
5532
5815
  }
5816
+
5817
+ /* Fixed band: section select + search icon stay pinned while the doc
5818
+ switcher and footer scroll with the page. Sticky failed here because the
5819
+ whole rail scrolls away — fixed keeps the controls always reachable on long
5820
+ documents. No own border: the rail's bottom edge carries the single
5821
+ divider, landing below the footer where the nav content ends. Between phone
5822
+ and rail breakpoints the head sits inside the shell frame, below the top
5823
+ rule and one stroke inset on the sides so the pseudo borders stay visible
5824
+ (≤700px drops the frame and returns to full bleed). */
5825
+ .sidebar-mobile-head {
5826
+ position: fixed;
5827
+ top: 0;
5828
+ left: 0;
5829
+ right: 0;
5830
+ z-index: 320;
5831
+ display: grid;
5832
+ grid-template-columns: 1fr auto;
5833
+ column-gap: var(--bp-space-2);
5834
+ background: var(--bp-bg);
5835
+ padding: var(--bp-space-3) var(--bp-space-4);
5836
+ }
5837
+
5838
+ /* Framed range (701–860px): the desk frame is drawn, so tuck the head inside
5839
+ it and back the top rule so nothing scrolls through. */
5840
+ @media (min-width: 701px) {
5841
+ /* The shell top rule is a height:0 box with an 8%-alpha border-top, so when
5842
+ content scrolls beneath it the near-transparent border lets it bleed
5843
+ through that 1px row (the desk mask only covers the band above it).
5844
+ Extend the opaque desk mask by one stroke so it backs the border row;
5845
+ the frame border (z 210) then draws over solid bg, staying visible. */
5846
+ html[data-bp-document] body::before {
5847
+ height: calc(var(--bp-shell-inset-top) + var(--bp-stroke));
5848
+ }
5849
+ html[data-bp-document] {
5850
+ --bp-scroll-margin: calc(
5851
+ var(--bp-shell-inset-top) + var(--bp-stroke) +
5852
+ var(--bp-sidebar-mobile-head-h) + var(--bp-space-2)
5853
+ );
5854
+ }
5855
+ html[data-bp-document] .sidebar-mobile-head {
5856
+ /* Flush below the top rule (inset-top + its hairline); one stroke inside
5857
+ the vertical rules so they stay visible. */
5858
+ top: calc(var(--bp-shell-inset-top) + var(--bp-stroke));
5859
+ left: calc(var(--bp-shell-inset-left) + var(--bp-stroke));
5860
+ right: calc(var(--bp-shell-inset-right) + var(--bp-stroke));
5861
+ /* Frame inset + this padding matches .bp-sidebar padding-inline. */
5862
+ padding-inline: max(
5863
+ 0px,
5864
+ calc(
5865
+ var(--bp-space-4) - var(--bp-shell-inset-left) - var(--bp-stroke)
5866
+ )
5867
+ );
5868
+ }
5869
+ }
5870
+
5871
+ /* Phone range (≤700px): the desk frame is removed, so the head is full bleed.
5872
+ Only the site nav, when present, pushes it down. */
5873
+ @media (max-width: 700px) {
5874
+ html[data-bp-document]:has(body > .site-nav) {
5875
+ --bp-scroll-margin: calc(
5876
+ var(--bp-site-nav-height) + var(--bp-sidebar-mobile-head-h) +
5877
+ var(--bp-space-2)
5878
+ );
5879
+ }
5880
+ html:has(body > .site-nav) .sidebar-mobile-head {
5881
+ top: var(--bp-site-nav-height);
5882
+ }
5883
+ }
5884
+
5885
+ /* Once the rail's own bottom border has scrolled up under the pinned head,
5886
+ the head carries the divider at that same line (blueprint.js toggles the
5887
+ attribute). At rest the head is borderless — the rail edge is the divider. */
5888
+ .sidebar-mobile-head[data-bp-head-stuck] {
5889
+ border-bottom: 1px solid var(--bp-edge);
5890
+ }
5891
+ .bp-sidebar__panel {
5892
+ display: block;
5893
+ /* Reserve the fixed head height so the doc switcher starts below it. */
5894
+ padding-top: var(--bp-sidebar-mobile-head-h);
5895
+ }
5896
+ .bp-toc-switcher {
5897
+ min-width: 0;
5898
+ }
5899
+ .sidebar-search {
5900
+ margin: 0;
5901
+ align-self: start;
5902
+ }
5903
+ /* Pill icon button — same trigger as the wide search field, opens ⌘K palette. */
5904
+ .sidebar-search__field {
5905
+ width: var(--bp-sidebar-toggle-size);
5906
+ min-width: var(--bp-sidebar-toggle-size);
5907
+ padding: 0;
5908
+ gap: 0;
5909
+ justify-content: center;
5910
+ }
5911
+ .sidebar-search__label,
5912
+ .sidebar-search__hint {
5913
+ display: none;
5914
+ }
5915
+ .doc-switcher {
5916
+ margin: 0;
5917
+ }
5918
+
5919
+ /* The fixed rail reserved a tall bottom band for its pinned footer; in the
5920
+ static strip that band is just dead space below the meta row. Match the
5921
+ top inset so the nav ends as tightly as it begins. Give the search,
5922
+ switchers, and footer row breathing room from the viewport edges. */
5923
+ .bp-sidebar {
5924
+ display: flex;
5925
+ flex-direction: column;
5926
+ gap: var(--bp-space-3);
5927
+ padding-inline: var(--bp-space-4);
5928
+ padding-bottom: var(--bp-space-4);
5929
+ padding-top: 0;
5930
+ }
5931
+
5932
+ /* The footer is a single meta row now, not a full-height column: pull the
5933
+ theme control and author/date into one left-aligned cluster so they read
5934
+ together rather than stranded at opposite edges of a wide strip. */
5935
+ .sidebar-footer {
5936
+ justify-content: flex-start;
5937
+ gap: var(--bp-space-5);
5938
+ margin-top: 0;
5939
+ }
5533
5940
  }
5534
5941
 
5535
5942
  @media print {