@veluai/velu 0.2.13 → 0.2.15

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.
Files changed (109) hide show
  1. package/README.md +80 -80
  2. package/dist/cli.js +37 -37
  3. package/package.json +64 -64
  4. package/runtime/velu-ui/base.css +320 -320
  5. package/runtime/velu-ui/components/Accordion.jsx +64 -64
  6. package/runtime/velu-ui/components/ApiClient.jsx +207 -207
  7. package/runtime/velu-ui/components/ApiField.jsx +87 -87
  8. package/runtime/velu-ui/components/ApiPath.jsx +63 -63
  9. package/runtime/velu-ui/components/ApiReferencePage.jsx +384 -384
  10. package/runtime/velu-ui/components/ApiSamples.jsx +36 -36
  11. package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
  12. package/runtime/velu-ui/components/AskBar.jsx +71 -71
  13. package/runtime/velu-ui/components/Callout.jsx +114 -114
  14. package/runtime/velu-ui/components/Card.jsx +131 -131
  15. package/runtime/velu-ui/components/Chatbot.jsx +885 -885
  16. package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
  17. package/runtime/velu-ui/components/Columns.jsx +56 -56
  18. package/runtime/velu-ui/components/ContextMenu.jsx +298 -273
  19. package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
  20. package/runtime/velu-ui/components/Field.jsx +81 -81
  21. package/runtime/velu-ui/components/Image.jsx +163 -163
  22. package/runtime/velu-ui/components/Logo.jsx +31 -31
  23. package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
  24. package/runtime/velu-ui/components/NavSelect.jsx +108 -108
  25. package/runtime/velu-ui/components/NotFound.jsx +63 -63
  26. package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
  27. package/runtime/velu-ui/components/PageFooter.jsx +145 -145
  28. package/runtime/velu-ui/components/PageHeader.jsx +422 -422
  29. package/runtime/velu-ui/components/PageNav.jsx +77 -77
  30. package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
  31. package/runtime/velu-ui/components/Prompt.jsx +115 -115
  32. package/runtime/velu-ui/components/Search.jsx +460 -460
  33. package/runtime/velu-ui/components/Sidebar.jsx +254 -254
  34. package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
  35. package/runtime/velu-ui/components/Steps.jsx +65 -65
  36. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
  37. package/runtime/velu-ui/components/Toc.jsx +537 -537
  38. package/runtime/velu-ui/components/TocBar.jsx +195 -195
  39. package/runtime/velu-ui/components/Tree.jsx +87 -87
  40. package/runtime/velu-ui/components/TryItBar.jsx +102 -102
  41. package/runtime/velu-ui/components/accordion.css +92 -92
  42. package/runtime/velu-ui/components/api-page.css +208 -208
  43. package/runtime/velu-ui/components/api.css +635 -635
  44. package/runtime/velu-ui/components/ask-bar.css +94 -94
  45. package/runtime/velu-ui/components/card.css +105 -105
  46. package/runtime/velu-ui/components/chatbot.css +622 -622
  47. package/runtime/velu-ui/components/code-block.css +263 -263
  48. package/runtime/velu-ui/components/context-menu.css +173 -173
  49. package/runtime/velu-ui/components/docs-layout.css +822 -822
  50. package/runtime/velu-ui/components/field.css +82 -82
  51. package/runtime/velu-ui/components/image.css +237 -237
  52. package/runtime/velu-ui/components/nav-select.css +157 -157
  53. package/runtime/velu-ui/components/not-found.css +94 -94
  54. package/runtime/velu-ui/components/page-feedback.css +241 -241
  55. package/runtime/velu-ui/components/page-footer.css +130 -130
  56. package/runtime/velu-ui/components/page-header.css +558 -558
  57. package/runtime/velu-ui/components/page-nav.css +50 -50
  58. package/runtime/velu-ui/components/powered-by.css +92 -92
  59. package/runtime/velu-ui/components/prompt.css +99 -99
  60. package/runtime/velu-ui/components/search.css +307 -307
  61. package/runtime/velu-ui/components/sidebar.css +205 -205
  62. package/runtime/velu-ui/components/steps.css +77 -77
  63. package/runtime/velu-ui/components/theme-toggle.css +102 -102
  64. package/runtime/velu-ui/components/toc-bar.css +234 -234
  65. package/runtime/velu-ui/components/tree.css +49 -49
  66. package/runtime/velu-ui/index.js +54 -54
  67. package/runtime/velu-ui/lib/api-send.js +92 -92
  68. package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
  69. package/runtime/velu-ui/lib/component-schemas.js +100 -100
  70. package/runtime/velu-ui/lib/copyText.js +64 -64
  71. package/runtime/velu-ui/lib/docs-assistant.js +250 -250
  72. package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
  73. package/runtime/velu-ui/lib/pagefind.js +113 -113
  74. package/runtime/velu-ui/lib/prism-langs.js +957 -957
  75. package/runtime/velu-ui/lib/prism-loader.js +74 -74
  76. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
  77. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
  78. package/runtime/velu-ui/mdx-components.jsx +105 -105
  79. package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
  80. package/runtime/velu-ui/primitives/Stack.jsx +63 -63
  81. package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
  82. package/runtime/velu-ui/primitives/stack.css +3 -3
  83. package/runtime/velu-ui/primitives/switcher.css +25 -25
  84. package/runtime/velu-ui/styles.css +46 -46
  85. package/runtime/velu-ui/tokens.css +4 -4
  86. package/schema/velu.schema.json +423 -423
  87. package/src/lib/extract-mdx-error.js +170 -170
  88. package/src/lib/issues.js +159 -159
  89. package/src/lib/known-components.js +34 -34
  90. package/src/navigation.js +443 -443
  91. package/src/runtime/App.jsx +1669 -1668
  92. package/src/runtime/ErrorBoundary.jsx +54 -54
  93. package/src/runtime/client-entry.jsx +22 -22
  94. package/src/runtime/server-entry.jsx +16 -16
  95. package/src/template.html +48 -48
  96. package/templates/starter/ai-tools/claude-code.mdx +26 -26
  97. package/templates/starter/ai-tools/cursor.mdx +17 -17
  98. package/templates/starter/api-reference/introduction.mdx +43 -43
  99. package/templates/starter/development.mdx +19 -19
  100. package/templates/starter/essentials/code.mdx +29 -29
  101. package/templates/starter/essentials/images.mdx +29 -29
  102. package/templates/starter/essentials/markdown.mdx +25 -25
  103. package/templates/starter/essentials/navigation.mdx +39 -39
  104. package/templates/starter/essentials/settings.mdx +30 -30
  105. package/templates/starter/favicon.svg +6 -6
  106. package/templates/starter/index.mdx +31 -31
  107. package/templates/starter/openapi.json +160 -160
  108. package/templates/starter/quickstart.mdx +31 -31
  109. package/templates/starter/velu.json +41 -41
@@ -1,822 +1,822 @@
1
- /* DocsLayout — the chrome of a Velu docs page (header, fixed left
2
- sidebar, center column, fixed right TOC, footer). The interesting
3
- bit lives in the @container rule at the bottom: at narrow widths
4
- the right TOC is hidden and the center column expands, with no
5
- media queries. Container query → reacts to the layout wrapper's
6
- inline-size, not the viewport, so the same component works in any
7
- surrounding shell (modal, split view, etc).
8
-
9
- All values are tokens; light/dark via [data-theme]. */
10
-
11
- .velu-docs-layout {
12
- /* `inline-size` containment lets @container queries below react to
13
- this element's width. `docs` is the named container so component
14
- CSS elsewhere can target it specifically. */
15
- container-type: inline-size;
16
- container-name: docs;
17
-
18
- /* Rail width — exposed as a custom property so the actual rail
19
- element AND the centre-column margin that tracks it can reference
20
- a single source of truth. Set OUTSIDE the @container block so the
21
- value is unconditionally inherited by descendants (defining it
22
- inside the container query was leaving it un-set for descendants
23
- in some cascade edge cases, so the rail collapsed to 0 width). */
24
- --velu-rail-width: 3.5rem; /* 56px */
25
- /* Left sidebar column width — single source of truth so the aside
26
- and the centre-column margin that reserves space for it stay in
27
- sync. Wide enough that nav labels don't wrap given the left inset
28
- gutter on the aside. */
29
- --velu-sidebar-width: 18rem; /* 288px */
30
- /* Right column reserve — the TOC aside width AND the centre's right
31
- margin. Constant whether the TOC or the chatbot is shown, so the
32
- article never shifts when the chatbot opens. The chatbot is wider
33
- than this and simply slides OVER the right edge as a floating
34
- panel (its own --vchat-width), rather than reflowing the article. */
35
- --velu-aside-right-width: 17.5rem; /* 280px */
36
-
37
- display: flex;
38
- flex-direction: column;
39
- min-height: 100vh;
40
- background: var(--page-bg);
41
- }
42
-
43
- /* ── Asides (fixed left sidebar + right TOC) ───────────────────────── */
44
- .velu-docs-layout__aside {
45
- position: fixed;
46
- /* Span full viewport height so the aside extends behind the
47
- translucent header (its top shows through the header's frost on
48
- scroll). Inner padding-top pushes the nav content down below the
49
- header band; scroll-padding-top keeps auto-scroll out of it. */
50
- inset-block-start: 0;
51
- inset-block-end: 0;
52
- overflow-y: auto;
53
- /* Horizontal clip: the left aside animates its `inline-size` down
54
- to a thin rail when collapsed, and its child nav (which keeps
55
- its full intrinsic width) MUST be visually cropped to the new
56
- box. `overflow-y: auto` + `overflow-x: clip` is allowed (clip
57
- is the spec partner that works with a scrollable y-axis) and
58
- gives reliable visual clipping that pure `overflow: hidden`
59
- can interact badly with on a `position: fixed` element. */
60
- overflow-x: clip;
61
- overscroll-behavior: contain;
62
- padding-block-start: calc(var(--velu-header-height) + var(--s3));
63
- scroll-padding-block-start: var(--velu-header-height);
64
- z-index: 10;
65
- /* `inline-size` is animated for the narrow-width collapse
66
- (240px ↔ rail). `transform` is animated for the mobile drawer
67
- (translateX(-100%) ↔ 0). Both timings live on the same property
68
- list so each animation runs only when its property actually
69
- changes at the active breakpoint. */
70
- transition: inline-size 0.2s ease, transform 0.25s ease;
71
- }
72
-
73
- .velu-docs-layout__aside--left {
74
- inset-inline-start: 0;
75
- inline-size: var(--velu-sidebar-width);
76
- /* Left gutter so the nav sits inset from the viewport edge rather
77
- than hugging it. (At mobile the @container block resets this to 0
78
- — the drawer manages its own padding.) */
79
- padding-inline-start: var(--s5);
80
- padding-inline-end: 0;
81
- /* The aside itself does NOT scroll — it's a flex column whose context
82
- zone (anchors/switchers) stays pinned and whose nav region
83
- (.velu-docs-nav-scroll) scrolls on its own. Overrides the base
84
- aside's `overflow-y: auto`. */
85
- display: flex;
86
- flex-direction: column;
87
- overflow: hidden;
88
- /* Bottom boundary: end the column a fixed gap above the viewport
89
- bottom (mirrors the pinned top), so the nav scrolls within a
90
- contained panel rather than running to the screen edge. The value
91
- is driven by --velu-aside-bottom (set inline in App.jsx) so the
92
- edge lifts to stay above the footer as it scrolls in; falls back
93
- to a static gap. The mobile drawer resets this to 0. */
94
- inset-block-end: var(--velu-aside-bottom, var(--s4));
95
- }
96
- /* Stack wrapper fills the aside so the inner nav region can flex-grow
97
- and own the scroll. */
98
- .velu-docs-aside-stack {
99
- flex: 1 1 auto;
100
- min-block-size: 0;
101
- }
102
- /* Region wrapping the scroll area + the overlay arrows. Fills the
103
- remaining aside height; relative so the arrows position to its
104
- top/bottom edges. */
105
- /* Hairline between the top anchor links and the nav sections (the sidebar
106
- design's anchor↔sidebar separator). Tokenized; sits flush in the aside flow. */
107
- .velu-docs-context-divider {
108
- flex: none;
109
- block-size: var(--border-width);
110
- background: var(--border-color);
111
- margin-block: var(--s-2);
112
- margin-inline: var(--s-2);
113
- }
114
-
115
- .velu-docs-nav-region {
116
- position: relative;
117
- flex: 1 1 auto;
118
- min-block-size: 0;
119
- display: flex;
120
- flex-direction: column;
121
- }
122
- .velu-docs-nav-scroll {
123
- flex: 1 1 auto;
124
- min-block-size: 0;
125
- overflow-y: auto;
126
- }
127
-
128
- /* Up / down scroll arrows — circular buttons centered on the top and
129
- bottom edges of the scroll region. Hidden by default; revealed (via
130
- the sibling combinator off the scroll div's data-fade-* attrs) only
131
- when there's content beyond that edge. Clicking smooth-scrolls the
132
- nav fully to that end (handlers in App.jsx). */
133
- .velu-docs-nav-arrow {
134
- position: absolute;
135
- inset-inline-start: 50%;
136
- display: inline-flex;
137
- align-items: center;
138
- justify-content: center;
139
- inline-size: 2rem;
140
- block-size: 2rem;
141
- padding: 0;
142
- border: var(--border-width) solid var(--border-color);
143
- border-radius: 999px;
144
- /* Frosted glass — same recipe as the scrolled header / toc-bar: 75%
145
- --page-bg + a saturate/blur backdrop, so the nav content shows through
146
- faintly behind the button. */
147
- background: color-mix(in srgb, var(--page-bg) 75%, transparent);
148
- backdrop-filter: saturate(140%) blur(12px);
149
- -webkit-backdrop-filter: saturate(140%) blur(12px);
150
- color: var(--muted-color);
151
- cursor: pointer;
152
- z-index: 3;
153
- opacity: 0;
154
- pointer-events: none;
155
- transform: translateX(-50%) scale(0.9);
156
- transition: opacity 0.15s ease, transform 0.15s ease, color 0.12s ease,
157
- border-color 0.12s ease;
158
- box-shadow: 0 var(--s-4) var(--s-1) color-mix(in srgb, #000 10%, transparent);
159
- }
160
- .velu-docs-nav-arrow:hover {
161
- color: var(--accent-color);
162
- border-color: var(--accent-color);
163
- }
164
- .velu-docs-nav-arrow svg {
165
- inline-size: 1.1em;
166
- block-size: 1.1em;
167
- }
168
- .velu-docs-nav-arrow--up {
169
- /* Float ABOVE the scroll region — up in the context-divider gap — instead
170
- of over the first section heading. The lift is the arrow's own height
171
- (2rem, matching block-size) plus a small gap, so its bottom clears the
172
- scroll content and it never overlaps nav text. */
173
- inset-block-start: calc(-2rem - var(--s-4));
174
- }
175
- .velu-docs-nav-arrow--down {
176
- inset-block-end: var(--s-3);
177
- }
178
- /* Reveal whenever there's content beyond that edge — always on (not just on
179
- hover), so the scroll affordance is visible as long as there's more to
180
- scroll. The data-fade-* attrs (set by the runtime from scroll position)
181
- already gate each arrow to the direction that actually has overflow. */
182
- .velu-docs-nav-scroll[data-fade-top='true'] ~ .velu-docs-nav-arrow--up,
183
- .velu-docs-nav-scroll[data-fade-bottom='true'] ~ .velu-docs-nav-arrow--down {
184
- opacity: 1;
185
- pointer-events: auto;
186
- transform: translateX(-50%) scale(1);
187
- }
188
-
189
- /* Edge fade: the scroll regions (left nav + right TOC) fade to
190
- transparent at whichever edge has more content beyond it. The
191
- --vf-top / --vf-bottom lengths default to 0 (no fade) and are
192
- switched on by the data-fade-* attrs that App.jsx sets from scroll
193
- position — so the top only fades once you've scrolled down, and the
194
- bottom fade vanishes when you reach the end. */
195
- .velu-docs-nav-scroll,
196
- .velu-docs-layout__aside--right {
197
- --vf-top: 0px;
198
- --vf-bottom: 0px;
199
- -webkit-mask-image: linear-gradient(
200
- to bottom,
201
- transparent,
202
- #000 var(--vf-top),
203
- #000 calc(100% - var(--vf-bottom)),
204
- transparent
205
- );
206
- mask-image: linear-gradient(
207
- to bottom,
208
- transparent,
209
- #000 var(--vf-top),
210
- #000 calc(100% - var(--vf-bottom)),
211
- transparent
212
- );
213
- }
214
- /* Top fade only on the right TOC — the left nav uses sticky section
215
- headers as its top treatment, so a top fade would just blur the
216
- pinned heading. */
217
- .velu-docs-layout__aside--right[data-fade-top='true'] {
218
- --vf-top: var(--s2);
219
- }
220
- /* The left nav's under-heading fade (the ::after below a sticky section
221
- heading) is shown only while the nav is scrolled up AND only under the
222
- currently-PINNED section ([data-stuck], set by the runtime). Other sections'
223
- headings scroll normally, so their first item must not be dimmed — only the
224
- item sliding under the top-pinned heading fades. */
225
- .velu-docs-nav-scroll[data-fade-top='true']
226
- .velu-sidebar__section[data-stuck='true']::after {
227
- opacity: 1;
228
- }
229
- .velu-docs-nav-scroll[data-fade-bottom='true'],
230
- .velu-docs-layout__aside--right[data-fade-bottom='true'] {
231
- --vf-bottom: var(--s2);
232
- }
233
-
234
- .velu-docs-layout__aside--right {
235
- inset-inline-end: 0;
236
- inline-size: var(--velu-aside-right-width);
237
- padding-inline: var(--s1);
238
- }
239
-
240
- /* Scrollbars hide at rest and reveal only while the cursor is over the
241
- column. Applies to the right TOC aside (scrolls directly) and the
242
- left nav's inner scroll region (.velu-docs-nav-scroll). The gutter
243
- stays reserved (scrollbar-width: thin from .velu-hide-scrollbar) —
244
- only the thumb's color toggles — so revealing it never shifts
245
- content. Overrides .velu-hide-scrollbar's always-on thumb. */
246
- .velu-docs-layout__aside--right,
247
- .velu-docs-nav-scroll {
248
- scrollbar-color: transparent transparent;
249
- }
250
- .velu-docs-layout__aside--right:hover,
251
- .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll {
252
- scrollbar-color: var(--border-color) transparent;
253
- }
254
- .velu-docs-layout__aside--right::-webkit-scrollbar-thumb,
255
- .velu-docs-nav-scroll::-webkit-scrollbar-thumb {
256
- background: transparent;
257
- }
258
- .velu-docs-layout__aside--right:hover::-webkit-scrollbar-thumb,
259
- .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll::-webkit-scrollbar-thumb {
260
- background: var(--border-color);
261
- }
262
- .velu-docs-layout__aside--right:hover::-webkit-scrollbar-thumb:hover,
263
- .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll::-webkit-scrollbar-thumb:hover {
264
- background: var(--muted-color);
265
- }
266
-
267
- /* ── Center column ─────────────────────────────────────────────────── */
268
- /* Reserves the aside widths via margin so the article never slides
269
- under them. The end margin collapses to 0 while the chatbot is
270
- open (the right TOC is hidden then, the chatbot uses that slot).
271
- Both can be overridden by the container query below. */
272
- .velu-docs-layout__center {
273
- position: relative;
274
- min-inline-size: 0;
275
- margin-inline-start: var(--velu-sidebar-width);
276
- /* Right reserve stays constant whether the right TOC or the chatbot
277
- occupies it — so opening the chatbot does NOT shift the article.
278
- The chatbot's desktop width matches this reserve (chatbot.css) so
279
- it never overlaps the content. */
280
- margin-inline-end: var(--velu-aside-right-width);
281
- /* Tracks the sidebar's `inline-size` animation at narrow widths so
282
- the article slides in sync with the collapse, no snap. Same 0.2s
283
- timing as the aside's own `inline-size` transition. */
284
- transition: margin-inline-start 0.2s ease;
285
- }
286
-
287
- /* <main> inside the centre column owns the article-area padding —
288
- defined here (not inline) so the narrow + sidebar-closed @container
289
- rule below can override just the inline-start side. */
290
- .velu-docs-layout__main {
291
- padding-block-start: var(--s3);
292
- padding-inline: var(--s4);
293
- }
294
-
295
- /* Article wrapper inside <main> — caps the prose at 46rem and centres
296
- it by default. Container query below switches it to left-aligned
297
- when the sidebar is closed at narrow widths, so the article hugs
298
- the freed space rather than centring inside it (which would leave
299
- the perceived "ghost sidebar" on the left). */
300
- .velu-docs-layout__article {
301
- max-inline-size: 46rem;
302
- margin-inline: auto;
303
- }
304
-
305
- /* Inner sidebar content (the <Sidebar> nav root, direct child of the
306
- left aside) cross-fades during the collapse animation. Hidden when
307
- the layout's `data-sidebar-open='false'` flag is set — see the
308
- @container block below for the actual opacity:0 toggle, which is
309
- scoped to narrow widths only (sidebar can't collapse at wide). */
310
- .velu-docs-layout__aside--left > * {
311
- transition: opacity 0.15s ease;
312
- }
313
-
314
- /* ── Scrim (mobile-only) ───────────────────────────────────────────── */
315
- /* Backdrop layer behind the mobile drawer — dims + blurs the article
316
- underneath and absorbs pointer events so nothing beneath is
317
- clickable while the drawer is open. Always in the DOM so the
318
- opacity + visibility transition runs in both directions; toggled
319
- visible via [data-sidebar-open='true'] in the @container block
320
- at the bottom. */
321
- .velu-docs-layout__scrim {
322
- /* Hidden until the mobile @container block flips display on. */
323
- display: none;
324
- position: fixed;
325
- inset: 0;
326
- z-index: 34;
327
- background: color-mix(in srgb, #000 28%, transparent);
328
- backdrop-filter: blur(4px);
329
- -webkit-backdrop-filter: blur(4px);
330
- /* Closed state — invisible + non-interactive. The
331
- [data-sidebar-open='true'] rule below flips both back on. */
332
- opacity: 0;
333
- pointer-events: none;
334
- visibility: hidden;
335
- transition:
336
- opacity 0.25s ease,
337
- visibility 0s linear 0.25s;
338
- cursor: pointer;
339
- }
340
-
341
- /* ── Drawer chrome (mobile-only) ───────────────────────────────────── */
342
- /* Drawer-only elements (brand bar + docs-set selector) — hidden at
343
- wide / narrow widths; the @container block at the bottom flips them
344
- on at mobile. Defined here (above the @container blocks) so source
345
- order works in the cascade's favor. */
346
- .velu-docs-layout__drawer-head,
347
- .velu-docs-layout__drawer-docselect {
348
- display: none;
349
- }
350
- .velu-docs-layout__drawer-brand {
351
- display: inline-flex;
352
- align-items: center;
353
- gap: var(--s-3);
354
- color: inherit;
355
- text-decoration: none;
356
- }
357
- .velu-docs-layout__drawer-brand .velu-header__mark {
358
- inline-size: 2em;
359
- block-size: 1.5em;
360
- color: var(--accent-color);
361
- }
362
- .velu-docs-layout__drawer-close {
363
- display: inline-flex;
364
- align-items: center;
365
- justify-content: center;
366
- inline-size: 1.5rem;
367
- block-size: 1.5rem;
368
- padding: 0;
369
- background: transparent;
370
- border: 0;
371
- color: var(--text-color);
372
- cursor: pointer;
373
- }
374
- .velu-docs-layout__drawer-close svg {
375
- inline-size: 1.5rem;
376
- block-size: 1.5rem;
377
- stroke-width: 1;
378
- }
379
- .velu-docs-layout__drawer-docselect {
380
- position: relative;
381
- }
382
- .velu-docs-layout__drawer-docselect-btn {
383
- display: flex;
384
- align-items: center;
385
- justify-content: space-between;
386
- inline-size: 100%;
387
- /* 8px top/bottom — matches the modular-scale step --s-3 (~0.5rem). */
388
- padding-block: var(--s-3);
389
- padding-inline: var(--s0);
390
- background: var(--page-bg);
391
- border: var(--border-width) solid var(--border-color);
392
- border-radius: var(--radius-sm);
393
- font: inherit;
394
- font-size: var(--f-h5);
395
- color: var(--text-color);
396
- text-align: start;
397
- cursor: pointer;
398
- transition: border-color 0.12s ease;
399
- }
400
- .velu-docs-layout__drawer-docselect-btn:hover {
401
- border-color: var(--accent-color);
402
- }
403
- .velu-docs-layout__drawer-docselect-label {
404
- flex: 1;
405
- min-inline-size: 0;
406
- overflow: hidden;
407
- text-overflow: ellipsis;
408
- white-space: nowrap;
409
- }
410
- .velu-docs-layout__drawer-docselect-chev {
411
- inline-size: 1em;
412
- block-size: 1em;
413
- stroke-width: 1;
414
- /* Match the label color (not the muted variant the kebab/search
415
- icons use) — the chevron reads as part of the trigger text, not
416
- a secondary indicator. */
417
- color: var(--text-color);
418
- stroke: currentColor;
419
- flex: none;
420
- transition: transform 0.18s ease;
421
- }
422
- .velu-docs-layout__drawer-docselect[data-open='true']
423
- .velu-docs-layout__drawer-docselect-chev {
424
- transform: rotate(180deg);
425
- }
426
-
427
- /* Menu — anchored below the trigger, full width of the wrapper.
428
- Always in the DOM so the open/close transition can run in both
429
- directions; visibility delayed so the closed menu isn't focusable
430
- or click-able. */
431
- .velu-docs-layout__drawer-docselect-menu {
432
- position: absolute;
433
- inset-block-start: calc(100% + var(--s-3));
434
- inset-inline-start: 0;
435
- inset-inline-end: 0;
436
- margin: 0;
437
- padding-block: var(--s-2);
438
- padding-inline: 0;
439
- list-style: none;
440
- background: var(--page-bg);
441
- border: var(--border-width) solid var(--border-color);
442
- border-radius: var(--radius-sm);
443
- box-shadow: 0 var(--s-2) var(--s1)
444
- color-mix(in srgb, #000 12%, transparent);
445
- opacity: 0;
446
- visibility: hidden;
447
- transform: translateY(-0.25rem);
448
- pointer-events: none;
449
- transition:
450
- opacity 0.15s ease,
451
- transform 0.15s ease,
452
- visibility 0s linear 0.15s;
453
- z-index: 1;
454
- }
455
- .velu-docs-layout__drawer-docselect[data-open='true']
456
- .velu-docs-layout__drawer-docselect-menu {
457
- opacity: 1;
458
- visibility: visible;
459
- transform: translateY(0);
460
- pointer-events: auto;
461
- transition:
462
- opacity 0.15s ease,
463
- transform 0.15s ease,
464
- visibility 0s;
465
- }
466
- .velu-docs-layout__drawer-docselect-item {
467
- display: block;
468
- padding-block: var(--s-2);
469
- padding-inline: var(--s0);
470
- font-size: var(--f-h5);
471
- color: var(--text-color);
472
- text-decoration: none;
473
- cursor: pointer;
474
- transition: background 0.12s ease, color 0.12s ease;
475
- }
476
- .velu-docs-layout__drawer-docselect-item:hover {
477
- background: var(--surface-color);
478
- color: var(--accent-color);
479
- }
480
-
481
- /* ── Sidebar toggle (narrow-width only) ────────────────────────────── */
482
- /* Small chevron button straddling the sidebar/article boundary.
483
- `position: fixed` (not absolute) so it stays pinned to the
484
- viewport while the article scrolls — the chevron is always
485
- reachable, never buried below the fold. It sits one button-radius
486
- below the header and is centred on the boundary between the
487
- sidebar/rail and the article column. That boundary is exposed
488
- via the `--velu-sidebar-boundary` custom property (defaults to
489
- 240px when the sidebar is open; collapses to the rail width
490
- when closed — see @container rules below).
491
-
492
- Hidden by default; revealed only at narrow widths. */
493
- .velu-docs-layout__sidebar-toggle {
494
- display: none;
495
- position: fixed;
496
- /* Toggle sits well below the TocBar so it doesn't crowd the
497
- bar's bottom border. --s5 (~49px) clears the TocBar height
498
- itself; adding --s2 (~25px) gives a clear breathing gap. */
499
- inset-block-start: calc(var(--velu-header-height) + var(--s5) + var(--s2));
500
- /* Sidebar's right edge minus half the toggle's own width — centres
501
- the 2rem button on the divider. Tracks the actual sidebar width. */
502
- inset-inline-start: calc(var(--velu-sidebar-boundary, var(--velu-sidebar-width)) - 1rem);
503
- inline-size: 2rem;
504
- block-size: 2rem;
505
- align-items: center;
506
- justify-content: center;
507
- padding: 0;
508
- border: var(--border-width) solid var(--border-color);
509
- border-radius: 999px;
510
- background: var(--page-bg);
511
- color: var(--muted-color);
512
- cursor: pointer;
513
- transition: border-color 0.12s ease, color 0.12s ease,
514
- inset-inline-start 0.2s ease;
515
- z-index: 11;
516
- }
517
- .velu-docs-layout__sidebar-toggle:hover {
518
- border-color: var(--accent-color);
519
- color: var(--accent-color);
520
- }
521
-
522
- /* ── Narrow-width layout ───────────────────────────────────────────── */
523
- /* Container query (not media query) so the layout reacts to the
524
- wrapper's inline-size — works the same inside a modal, split pane,
525
- or shrunken window. At < 1024px the right TOC drops out and the
526
- centre column expands to fill its slot; the left sidebar becomes
527
- toggleable via the sidebar-toggle button (default open, per
528
- design). Closing pushes the centre column over rather than overlaying. */
529
- @container docs (max-width: 1024px) {
530
- .velu-docs-layout__aside--right {
531
- display: none;
532
- }
533
- .velu-docs-layout__center {
534
- margin-inline-end: 0;
535
- }
536
- .velu-docs-layout__sidebar-toggle {
537
- display: inline-flex;
538
- }
539
- /* Hairline divider on the left sidebar — only at narrow widths,
540
- where the sidebar visually separates from the article column.
541
- Same --surface-color recipe as the header/footer rails. The big
542
- desktop left-inset is dialed back here (tablet); mobile resets it
543
- to 0 via the drawer block below. */
544
- .velu-docs-layout__aside--left {
545
- border-inline-end: var(--border-width) solid var(--surface-color);
546
- padding-inline-start: var(--s2);
547
- /* The divider IS this aside's right edge, so it can only reach as far
548
- down as the box does. The base rule lifts the box above the footer
549
- (inset-block-end) to keep nav content clear of it — but that also
550
- cuts the divider short. Here (where the divider shows) span the box
551
- to the bottom instead and push the *content* up with matching
552
- padding, so the line runs to the footer while the nav still clears
553
- it. The footer's own z-index covers the line where they overlap. */
554
- inset-block-end: 0;
555
- padding-block-end: var(--velu-aside-bottom, var(--s4));
556
- }
557
- /* Sidebar closed → shrink the aside's `inline-size` from 240px to
558
- the rail width. The aside stays in the DOM and keeps its own
559
- border-inline-end — that border IS the rail's right edge when
560
- collapsed, no separate rail element needed. `overflow-x: clip`
561
- on the aside (set above) crops the still-rendered Sidebar nav
562
- to the narrowed box; the opacity rule below fades the nav out
563
- so the collapsed state reads as a clean stub.
564
- The aside's `transition: inline-size 0.2s ease` (set above) is
565
- what makes the collapse animate; the centre column tracks via
566
- its own `margin-inline-start` transition. */
567
- .velu-docs-layout[data-sidebar-open='false']
568
- .velu-docs-layout__aside--left {
569
- inline-size: var(--velu-rail-width);
570
- /* Collapsed state is a thin stub — the (faded) nav is irrelevant,
571
- so suppress the y-scrollbar that would otherwise appear over
572
- the rail. `overflow-y: auto` is restored implicitly at wide
573
- widths / open state by the base `.velu-docs-layout__aside`
574
- rule above. */
575
- overflow-y: hidden;
576
- }
577
- /* Fade out the Sidebar nav while the aside shrinks — opacity
578
- transitions in parallel with `inline-size`, so the user sees a
579
- single coherent collapse (shrink + fade) rather than a snap. */
580
- .velu-docs-layout[data-sidebar-open='false']
581
- .velu-docs-layout__aside--left > * {
582
- opacity: 0;
583
- pointer-events: none;
584
- }
585
- .velu-docs-layout[data-sidebar-open='false']
586
- .velu-docs-layout__center {
587
- margin-inline-start: var(--velu-rail-width);
588
- }
589
- /* Toggle position tracks the active boundary directly — when the
590
- sidebar is closed the boundary moves from 240px (sidebar's right
591
- edge) to var(--velu-rail-width) (rail's right edge), so the
592
- toggle slides with it. Overriding the property here (instead of
593
- going through a custom-prop indirection) keeps the transition
594
- reliable across engines. */
595
- .velu-docs-layout[data-sidebar-open='false']
596
- .velu-docs-layout__sidebar-toggle {
597
- inset-inline-start: calc(var(--velu-rail-width) - 1rem);
598
- }
599
- /* At narrow widths the article drops its 46rem cap and fills the
600
- available main width. Cap survives at wide widths so the 46rem
601
- prose column lives between the two asides. */
602
- .velu-docs-layout__article {
603
- max-inline-size: none;
604
- margin-inline: 0;
605
- }
606
- }
607
-
608
- /* ── Mobile-width layout (< 640px) ─────────────────────────────────── */
609
- /* At mobile widths the centre column reclaims full width regardless
610
- of sidebar state. The sidebar becomes a RIGHT-edge drawer (anchored
611
- to the trailing edge, slides in from the right) — opened by the
612
- header's burger OR by tapping the breadcrumb strip, both of which
613
- call `onMenuClick` → toggles the layout's `data-drawer-open` flag.
614
- Note: mobile uses its OWN `data-drawer-open` attribute, separate
615
- from `data-sidebar-open` (which drives the narrow-width rail
616
- collapse). Two independent states avoid the SSR/hydration default
617
- collision that would force the drawer to render open on refresh.
618
- The floating chevron toggle is removed; it was a wide-layout
619
- affordance and is redundant at mobile. */
620
- @container docs (max-width: 640px) {
621
- .velu-docs-layout__sidebar-toggle {
622
- display: none;
623
- }
624
- .velu-docs-layout__center,
625
- .velu-docs-layout[data-sidebar-open='false'] .velu-docs-layout__center {
626
- margin-inline-start: 0;
627
- }
628
- .velu-docs-layout__aside--left {
629
- /* Anchor to the right edge instead of the left — override the
630
- base `inset-inline-start: 0` from .velu-docs-layout__aside--left
631
- so the drawer slides in from the trailing edge. Full viewport
632
- height comes from the base `inset-block: 0 0` on the aside.
633
- Drop the header-clearance top padding — the drawer has its own
634
- brand bar. Inline padding stays at 0 — the Sidebar items have
635
- their own `padding-inline: var(--s0)` (16px), and the drawer
636
- chrome below gets matching `padding-inline` directly, so the
637
- inner content lines up 16px from the drawer edge without
638
- double-padding. */
639
- inset-inline-start: auto;
640
- inset-inline-end: 0;
641
- /* Full-height drawer — no bottom boundary gap (overrides the
642
- desktop panel inset). */
643
- inset-block-end: 0;
644
- inline-size: min(20rem, 85vw);
645
- padding-block-start: var(--s0);
646
- padding-inline: 0;
647
- /* Closed default — fully off the trailing edge. The
648
- data-sidebar-open='true' rule below brings it back in. */
649
- transform: translateX(100%);
650
- /* Above the sticky header (z-index 30 in page-header.css) so the
651
- drawer covers the header bar too, but below the search palette
652
- (z-index 60) and chatbot (z-index 50). The aside already spans
653
- the full viewport via the base `inset-block: 0 0`. */
654
- z-index: 35;
655
- /* Solid body background — the aside has no background by default
656
- (it inherits the layout's page-bg by virtue of being a child),
657
- but at mobile it's a `position: fixed` drawer that overlays the
658
- article + header, so transparency would let everything beneath
659
- bleed through. Explicit --page-bg makes the drawer opaque. */
660
- background: var(--page-bg);
661
- }
662
- /* Shadow only when the drawer is OPEN — when closed the drawer
663
- sits off-screen right via `translateX(100%)`, and a left-edge
664
- shadow would bleed leftward into the visible viewport (a ghost
665
- band along the right edge). Gating to the open state keeps the
666
- depth cue while the drawer is visible and hides it the rest of
667
- the time. */
668
- .velu-docs-layout[data-drawer-open='true']
669
- .velu-docs-layout__aside--left {
670
- transform: translateX(0);
671
- box-shadow:
672
- calc(-1 * var(--s0)) 0 var(--s2)
673
- color-mix(in srgb, #000 18%, transparent);
674
- }
675
- /* Drop the narrow-width opacity fade / overflow-hidden on the
676
- drawer's inner content — at mobile the narrow-width rules above
677
- (data-sidebar-open='false') still apply because sidebarOpen
678
- defaults to true and may flip via the chevron at narrow widths,
679
- but at mobile we want the inner nav fully visible and the
680
- drawer scrollable regardless of those narrow rules. */
681
- .velu-docs-layout__aside--left > * {
682
- opacity: 1;
683
- pointer-events: auto;
684
- }
685
- /* The aside stays a non-scrolling flex column on mobile too; the
686
- nav region (.velu-docs-nav-scroll) owns the scroll, with the
687
- context zone pinned above it. */
688
- .velu-docs-layout__aside--left {
689
- overflow: hidden;
690
- }
691
- /* Drawer chrome — brand bar + docs-set selector — visible only at
692
- mobile (default-hidden above). Both elements get `padding-inline:
693
- var(--s0)` so their content lines up at the same 16px-from-edge
694
- position the Sidebar items below them use (which inherit
695
- `padding-inline: var(--s0)` from sidebar.css). */
696
- .velu-docs-layout__drawer-head {
697
- display: flex;
698
- align-items: center;
699
- gap: var(--s-1);
700
- padding-inline: var(--s0);
701
- }
702
- .velu-docs-layout__drawer-head .velu-theme-toggle {
703
- margin-inline-start: auto;
704
- }
705
- .velu-docs-layout__drawer-docselect {
706
- display: block;
707
- margin-inline: var(--s0);
708
- /* Inset 16px from each drawer edge; Stack (column flex with
709
- align-items: stretch) stretches it across the remaining width.
710
- `align-self: stretch` explicit for engines that don't honor
711
- the parent's default stretch. */
712
- align-self: stretch;
713
- }
714
- /* Scrim — present at mobile; toggled on by the mobile nav drawer
715
- here. The chatbot's bottom-sheet variant exists at the wider
716
- `< 1024px` threshold, so the chatbot-driven scrim is enabled in
717
- a SECOND @container block below (at the same wider threshold) —
718
- keeping the drawer-only scrim scoped to mobile and the
719
- chatbot-driven scrim available to both narrow and mobile. */
720
- .velu-docs-layout__scrim {
721
- display: block;
722
- }
723
- .velu-docs-layout[data-drawer-open='true'] .velu-docs-layout__scrim {
724
- opacity: 1;
725
- pointer-events: auto;
726
- visibility: visible;
727
- transition:
728
- opacity 0.25s ease,
729
- visibility 0s;
730
- }
731
- }
732
-
733
- /* ── Chatbot scrim (narrow + mobile, < 1024px) ─────────────────────── */
734
- /* The chatbot becomes a bottom sheet at < 1024px (see chatbot.css);
735
- the scrim follows the same threshold so the dim+blur layer appears
736
- over the article whenever the sheet is up. The mobile-only
737
- `data-drawer-open` trigger lives in the < 640px block above —
738
- separate because the drawer itself doesn't exist at narrow widths. */
739
- @container docs (max-width: 1024px) {
740
- .velu-docs-layout__scrim {
741
- display: block;
742
- }
743
- .velu-docs-layout[data-chat-open='true'] .velu-docs-layout__scrim {
744
- opacity: 1;
745
- pointer-events: auto;
746
- visibility: visible;
747
- transition:
748
- opacity 0.25s ease,
749
- visibility 0s;
750
- }
751
- }
752
-
753
- /* ── Sidebar context zone + responsive switcher helpers ────────────── */
754
- /* Product / version / language switchers + anchors above the group
755
- nav. Inset matches the Sidebar items (which carry padding-inline:
756
- var(--s0) from sidebar.css). */
757
- .velu-docs-context {
758
- flex: none;
759
- padding-inline: var(--s0);
760
- }
761
- .velu-docs-anchors {
762
- list-style: none;
763
- margin: 0;
764
- padding: 0;
765
- display: flex;
766
- flex-direction: column;
767
- gap: var(--s-3);
768
- }
769
- .velu-docs-anchors__link {
770
- display: flex;
771
- align-items: center;
772
- gap: var(--s-3);
773
- color: var(--text-color);
774
- text-decoration: none;
775
- font-size: var(--f-h6);
776
- transition: color 0.12s ease;
777
- }
778
- .velu-docs-anchors__link:hover {
779
- color: var(--accent-color);
780
- }
781
- .velu-docs-anchors__icon {
782
- display: inline-flex;
783
- flex: none;
784
- color: var(--muted-color);
785
- }
786
- .velu-docs-anchors__link:hover .velu-docs-anchors__icon {
787
- color: var(--accent-color);
788
- }
789
-
790
- /* Show/hide helpers, scoped to the docs container's mobile breakpoint.
791
- Version + language switchers live in the header at desktop/tablet
792
- (hide-on-mobile) and re-appear in the drawer context zone on mobile
793
- (show-on-mobile). */
794
- .velu-show-on-mobile {
795
- display: none;
796
- }
797
- @container docs (max-width: 640px) {
798
- .velu-hide-on-mobile {
799
- display: none !important;
800
- }
801
- .velu-show-on-mobile {
802
- display: block;
803
- }
804
- }
805
-
806
- /* ── 404 takeover ─────────────────────────────────────────────────────── */
807
- /* On a not-found route the docs chrome (both fixed asides) is hidden and the
808
- centre column spans full width to center the <NotFound> content. The header
809
- and (configured) footer stay. */
810
- .velu-docs-layout[data-not-found='true'] .velu-docs-layout__aside {
811
- display: none;
812
- }
813
- .velu-docs-layout[data-not-found='true'] .velu-docs-layout__center {
814
- margin-inline: 0;
815
- }
816
- .velu-404-main {
817
- display: flex;
818
- align-items: center;
819
- justify-content: center;
820
- min-block-size: 70vh;
821
- padding: var(--s4) var(--s2);
822
- }
1
+ /* DocsLayout — the chrome of a Velu docs page (header, fixed left
2
+ sidebar, center column, fixed right TOC, footer). The interesting
3
+ bit lives in the @container rule at the bottom: at narrow widths
4
+ the right TOC is hidden and the center column expands, with no
5
+ media queries. Container query → reacts to the layout wrapper's
6
+ inline-size, not the viewport, so the same component works in any
7
+ surrounding shell (modal, split view, etc).
8
+
9
+ All values are tokens; light/dark via [data-theme]. */
10
+
11
+ .velu-docs-layout {
12
+ /* `inline-size` containment lets @container queries below react to
13
+ this element's width. `docs` is the named container so component
14
+ CSS elsewhere can target it specifically. */
15
+ container-type: inline-size;
16
+ container-name: docs;
17
+
18
+ /* Rail width — exposed as a custom property so the actual rail
19
+ element AND the centre-column margin that tracks it can reference
20
+ a single source of truth. Set OUTSIDE the @container block so the
21
+ value is unconditionally inherited by descendants (defining it
22
+ inside the container query was leaving it un-set for descendants
23
+ in some cascade edge cases, so the rail collapsed to 0 width). */
24
+ --velu-rail-width: 3.5rem; /* 56px */
25
+ /* Left sidebar column width — single source of truth so the aside
26
+ and the centre-column margin that reserves space for it stay in
27
+ sync. Wide enough that nav labels don't wrap given the left inset
28
+ gutter on the aside. */
29
+ --velu-sidebar-width: 18rem; /* 288px */
30
+ /* Right column reserve — the TOC aside width AND the centre's right
31
+ margin. Constant whether the TOC or the chatbot is shown, so the
32
+ article never shifts when the chatbot opens. The chatbot is wider
33
+ than this and simply slides OVER the right edge as a floating
34
+ panel (its own --vchat-width), rather than reflowing the article. */
35
+ --velu-aside-right-width: 17.5rem; /* 280px */
36
+
37
+ display: flex;
38
+ flex-direction: column;
39
+ min-height: 100vh;
40
+ background: var(--page-bg);
41
+ }
42
+
43
+ /* ── Asides (fixed left sidebar + right TOC) ───────────────────────── */
44
+ .velu-docs-layout__aside {
45
+ position: fixed;
46
+ /* Span full viewport height so the aside extends behind the
47
+ translucent header (its top shows through the header's frost on
48
+ scroll). Inner padding-top pushes the nav content down below the
49
+ header band; scroll-padding-top keeps auto-scroll out of it. */
50
+ inset-block-start: 0;
51
+ inset-block-end: 0;
52
+ overflow-y: auto;
53
+ /* Horizontal clip: the left aside animates its `inline-size` down
54
+ to a thin rail when collapsed, and its child nav (which keeps
55
+ its full intrinsic width) MUST be visually cropped to the new
56
+ box. `overflow-y: auto` + `overflow-x: clip` is allowed (clip
57
+ is the spec partner that works with a scrollable y-axis) and
58
+ gives reliable visual clipping that pure `overflow: hidden`
59
+ can interact badly with on a `position: fixed` element. */
60
+ overflow-x: clip;
61
+ overscroll-behavior: contain;
62
+ padding-block-start: calc(var(--velu-header-height) + var(--s3));
63
+ scroll-padding-block-start: var(--velu-header-height);
64
+ z-index: 10;
65
+ /* `inline-size` is animated for the narrow-width collapse
66
+ (240px ↔ rail). `transform` is animated for the mobile drawer
67
+ (translateX(-100%) ↔ 0). Both timings live on the same property
68
+ list so each animation runs only when its property actually
69
+ changes at the active breakpoint. */
70
+ transition: inline-size 0.2s ease, transform 0.25s ease;
71
+ }
72
+
73
+ .velu-docs-layout__aside--left {
74
+ inset-inline-start: 0;
75
+ inline-size: var(--velu-sidebar-width);
76
+ /* Left gutter so the nav sits inset from the viewport edge rather
77
+ than hugging it. (At mobile the @container block resets this to 0
78
+ — the drawer manages its own padding.) */
79
+ padding-inline-start: var(--s5);
80
+ padding-inline-end: 0;
81
+ /* The aside itself does NOT scroll — it's a flex column whose context
82
+ zone (anchors/switchers) stays pinned and whose nav region
83
+ (.velu-docs-nav-scroll) scrolls on its own. Overrides the base
84
+ aside's `overflow-y: auto`. */
85
+ display: flex;
86
+ flex-direction: column;
87
+ overflow: hidden;
88
+ /* Bottom boundary: end the column a fixed gap above the viewport
89
+ bottom (mirrors the pinned top), so the nav scrolls within a
90
+ contained panel rather than running to the screen edge. The value
91
+ is driven by --velu-aside-bottom (set inline in App.jsx) so the
92
+ edge lifts to stay above the footer as it scrolls in; falls back
93
+ to a static gap. The mobile drawer resets this to 0. */
94
+ inset-block-end: var(--velu-aside-bottom, var(--s4));
95
+ }
96
+ /* Stack wrapper fills the aside so the inner nav region can flex-grow
97
+ and own the scroll. */
98
+ .velu-docs-aside-stack {
99
+ flex: 1 1 auto;
100
+ min-block-size: 0;
101
+ }
102
+ /* Region wrapping the scroll area + the overlay arrows. Fills the
103
+ remaining aside height; relative so the arrows position to its
104
+ top/bottom edges. */
105
+ /* Hairline between the top anchor links and the nav sections (the sidebar
106
+ design's anchor↔sidebar separator). Tokenized; sits flush in the aside flow. */
107
+ .velu-docs-context-divider {
108
+ flex: none;
109
+ block-size: var(--border-width);
110
+ background: var(--border-color);
111
+ margin-block: var(--s-2);
112
+ margin-inline: var(--s-2);
113
+ }
114
+
115
+ .velu-docs-nav-region {
116
+ position: relative;
117
+ flex: 1 1 auto;
118
+ min-block-size: 0;
119
+ display: flex;
120
+ flex-direction: column;
121
+ }
122
+ .velu-docs-nav-scroll {
123
+ flex: 1 1 auto;
124
+ min-block-size: 0;
125
+ overflow-y: auto;
126
+ }
127
+
128
+ /* Up / down scroll arrows — circular buttons centered on the top and
129
+ bottom edges of the scroll region. Hidden by default; revealed (via
130
+ the sibling combinator off the scroll div's data-fade-* attrs) only
131
+ when there's content beyond that edge. Clicking smooth-scrolls the
132
+ nav fully to that end (handlers in App.jsx). */
133
+ .velu-docs-nav-arrow {
134
+ position: absolute;
135
+ inset-inline-start: 50%;
136
+ display: inline-flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ inline-size: 2rem;
140
+ block-size: 2rem;
141
+ padding: 0;
142
+ border: var(--border-width) solid var(--border-color);
143
+ border-radius: 999px;
144
+ /* Frosted glass — same recipe as the scrolled header / toc-bar: 75%
145
+ --page-bg + a saturate/blur backdrop, so the nav content shows through
146
+ faintly behind the button. */
147
+ background: color-mix(in srgb, var(--page-bg) 75%, transparent);
148
+ backdrop-filter: saturate(140%) blur(12px);
149
+ -webkit-backdrop-filter: saturate(140%) blur(12px);
150
+ color: var(--muted-color);
151
+ cursor: pointer;
152
+ z-index: 3;
153
+ opacity: 0;
154
+ pointer-events: none;
155
+ transform: translateX(-50%) scale(0.9);
156
+ transition: opacity 0.15s ease, transform 0.15s ease, color 0.12s ease,
157
+ border-color 0.12s ease;
158
+ box-shadow: 0 var(--s-4) var(--s-1) color-mix(in srgb, #000 10%, transparent);
159
+ }
160
+ .velu-docs-nav-arrow:hover {
161
+ color: var(--accent-color);
162
+ border-color: var(--accent-color);
163
+ }
164
+ .velu-docs-nav-arrow svg {
165
+ inline-size: 1.1em;
166
+ block-size: 1.1em;
167
+ }
168
+ .velu-docs-nav-arrow--up {
169
+ /* Float ABOVE the scroll region — up in the context-divider gap — instead
170
+ of over the first section heading. The lift is the arrow's own height
171
+ (2rem, matching block-size) plus a small gap, so its bottom clears the
172
+ scroll content and it never overlaps nav text. */
173
+ inset-block-start: calc(-2rem - var(--s-4));
174
+ }
175
+ .velu-docs-nav-arrow--down {
176
+ inset-block-end: var(--s-3);
177
+ }
178
+ /* Reveal whenever there's content beyond that edge — always on (not just on
179
+ hover), so the scroll affordance is visible as long as there's more to
180
+ scroll. The data-fade-* attrs (set by the runtime from scroll position)
181
+ already gate each arrow to the direction that actually has overflow. */
182
+ .velu-docs-nav-scroll[data-fade-top='true'] ~ .velu-docs-nav-arrow--up,
183
+ .velu-docs-nav-scroll[data-fade-bottom='true'] ~ .velu-docs-nav-arrow--down {
184
+ opacity: 1;
185
+ pointer-events: auto;
186
+ transform: translateX(-50%) scale(1);
187
+ }
188
+
189
+ /* Edge fade: the scroll regions (left nav + right TOC) fade to
190
+ transparent at whichever edge has more content beyond it. The
191
+ --vf-top / --vf-bottom lengths default to 0 (no fade) and are
192
+ switched on by the data-fade-* attrs that App.jsx sets from scroll
193
+ position — so the top only fades once you've scrolled down, and the
194
+ bottom fade vanishes when you reach the end. */
195
+ .velu-docs-nav-scroll,
196
+ .velu-docs-layout__aside--right {
197
+ --vf-top: 0px;
198
+ --vf-bottom: 0px;
199
+ -webkit-mask-image: linear-gradient(
200
+ to bottom,
201
+ transparent,
202
+ #000 var(--vf-top),
203
+ #000 calc(100% - var(--vf-bottom)),
204
+ transparent
205
+ );
206
+ mask-image: linear-gradient(
207
+ to bottom,
208
+ transparent,
209
+ #000 var(--vf-top),
210
+ #000 calc(100% - var(--vf-bottom)),
211
+ transparent
212
+ );
213
+ }
214
+ /* Top fade only on the right TOC — the left nav uses sticky section
215
+ headers as its top treatment, so a top fade would just blur the
216
+ pinned heading. */
217
+ .velu-docs-layout__aside--right[data-fade-top='true'] {
218
+ --vf-top: var(--s2);
219
+ }
220
+ /* The left nav's under-heading fade (the ::after below a sticky section
221
+ heading) is shown only while the nav is scrolled up AND only under the
222
+ currently-PINNED section ([data-stuck], set by the runtime). Other sections'
223
+ headings scroll normally, so their first item must not be dimmed — only the
224
+ item sliding under the top-pinned heading fades. */
225
+ .velu-docs-nav-scroll[data-fade-top='true']
226
+ .velu-sidebar__section[data-stuck='true']::after {
227
+ opacity: 1;
228
+ }
229
+ .velu-docs-nav-scroll[data-fade-bottom='true'],
230
+ .velu-docs-layout__aside--right[data-fade-bottom='true'] {
231
+ --vf-bottom: var(--s2);
232
+ }
233
+
234
+ .velu-docs-layout__aside--right {
235
+ inset-inline-end: 0;
236
+ inline-size: var(--velu-aside-right-width);
237
+ padding-inline: var(--s1);
238
+ }
239
+
240
+ /* Scrollbars hide at rest and reveal only while the cursor is over the
241
+ column. Applies to the right TOC aside (scrolls directly) and the
242
+ left nav's inner scroll region (.velu-docs-nav-scroll). The gutter
243
+ stays reserved (scrollbar-width: thin from .velu-hide-scrollbar) —
244
+ only the thumb's color toggles — so revealing it never shifts
245
+ content. Overrides .velu-hide-scrollbar's always-on thumb. */
246
+ .velu-docs-layout__aside--right,
247
+ .velu-docs-nav-scroll {
248
+ scrollbar-color: transparent transparent;
249
+ }
250
+ .velu-docs-layout__aside--right:hover,
251
+ .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll {
252
+ scrollbar-color: var(--border-color) transparent;
253
+ }
254
+ .velu-docs-layout__aside--right::-webkit-scrollbar-thumb,
255
+ .velu-docs-nav-scroll::-webkit-scrollbar-thumb {
256
+ background: transparent;
257
+ }
258
+ .velu-docs-layout__aside--right:hover::-webkit-scrollbar-thumb,
259
+ .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll::-webkit-scrollbar-thumb {
260
+ background: var(--border-color);
261
+ }
262
+ .velu-docs-layout__aside--right:hover::-webkit-scrollbar-thumb:hover,
263
+ .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll::-webkit-scrollbar-thumb:hover {
264
+ background: var(--muted-color);
265
+ }
266
+
267
+ /* ── Center column ─────────────────────────────────────────────────── */
268
+ /* Reserves the aside widths via margin so the article never slides
269
+ under them. The end margin collapses to 0 while the chatbot is
270
+ open (the right TOC is hidden then, the chatbot uses that slot).
271
+ Both can be overridden by the container query below. */
272
+ .velu-docs-layout__center {
273
+ position: relative;
274
+ min-inline-size: 0;
275
+ margin-inline-start: var(--velu-sidebar-width);
276
+ /* Right reserve stays constant whether the right TOC or the chatbot
277
+ occupies it — so opening the chatbot does NOT shift the article.
278
+ The chatbot's desktop width matches this reserve (chatbot.css) so
279
+ it never overlaps the content. */
280
+ margin-inline-end: var(--velu-aside-right-width);
281
+ /* Tracks the sidebar's `inline-size` animation at narrow widths so
282
+ the article slides in sync with the collapse, no snap. Same 0.2s
283
+ timing as the aside's own `inline-size` transition. */
284
+ transition: margin-inline-start 0.2s ease;
285
+ }
286
+
287
+ /* <main> inside the centre column owns the article-area padding —
288
+ defined here (not inline) so the narrow + sidebar-closed @container
289
+ rule below can override just the inline-start side. */
290
+ .velu-docs-layout__main {
291
+ padding-block-start: var(--s3);
292
+ padding-inline: var(--s4);
293
+ }
294
+
295
+ /* Article wrapper inside <main> — caps the prose at 46rem and centres
296
+ it by default. Container query below switches it to left-aligned
297
+ when the sidebar is closed at narrow widths, so the article hugs
298
+ the freed space rather than centring inside it (which would leave
299
+ the perceived "ghost sidebar" on the left). */
300
+ .velu-docs-layout__article {
301
+ max-inline-size: 46rem;
302
+ margin-inline: auto;
303
+ }
304
+
305
+ /* Inner sidebar content (the <Sidebar> nav root, direct child of the
306
+ left aside) cross-fades during the collapse animation. Hidden when
307
+ the layout's `data-sidebar-open='false'` flag is set — see the
308
+ @container block below for the actual opacity:0 toggle, which is
309
+ scoped to narrow widths only (sidebar can't collapse at wide). */
310
+ .velu-docs-layout__aside--left > * {
311
+ transition: opacity 0.15s ease;
312
+ }
313
+
314
+ /* ── Scrim (mobile-only) ───────────────────────────────────────────── */
315
+ /* Backdrop layer behind the mobile drawer — dims + blurs the article
316
+ underneath and absorbs pointer events so nothing beneath is
317
+ clickable while the drawer is open. Always in the DOM so the
318
+ opacity + visibility transition runs in both directions; toggled
319
+ visible via [data-sidebar-open='true'] in the @container block
320
+ at the bottom. */
321
+ .velu-docs-layout__scrim {
322
+ /* Hidden until the mobile @container block flips display on. */
323
+ display: none;
324
+ position: fixed;
325
+ inset: 0;
326
+ z-index: 34;
327
+ background: color-mix(in srgb, #000 28%, transparent);
328
+ backdrop-filter: blur(4px);
329
+ -webkit-backdrop-filter: blur(4px);
330
+ /* Closed state — invisible + non-interactive. The
331
+ [data-sidebar-open='true'] rule below flips both back on. */
332
+ opacity: 0;
333
+ pointer-events: none;
334
+ visibility: hidden;
335
+ transition:
336
+ opacity 0.25s ease,
337
+ visibility 0s linear 0.25s;
338
+ cursor: pointer;
339
+ }
340
+
341
+ /* ── Drawer chrome (mobile-only) ───────────────────────────────────── */
342
+ /* Drawer-only elements (brand bar + docs-set selector) — hidden at
343
+ wide / narrow widths; the @container block at the bottom flips them
344
+ on at mobile. Defined here (above the @container blocks) so source
345
+ order works in the cascade's favor. */
346
+ .velu-docs-layout__drawer-head,
347
+ .velu-docs-layout__drawer-docselect {
348
+ display: none;
349
+ }
350
+ .velu-docs-layout__drawer-brand {
351
+ display: inline-flex;
352
+ align-items: center;
353
+ gap: var(--s-3);
354
+ color: inherit;
355
+ text-decoration: none;
356
+ }
357
+ .velu-docs-layout__drawer-brand .velu-header__mark {
358
+ inline-size: 2em;
359
+ block-size: 1.5em;
360
+ color: var(--accent-color);
361
+ }
362
+ .velu-docs-layout__drawer-close {
363
+ display: inline-flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ inline-size: 1.5rem;
367
+ block-size: 1.5rem;
368
+ padding: 0;
369
+ background: transparent;
370
+ border: 0;
371
+ color: var(--text-color);
372
+ cursor: pointer;
373
+ }
374
+ .velu-docs-layout__drawer-close svg {
375
+ inline-size: 1.5rem;
376
+ block-size: 1.5rem;
377
+ stroke-width: 1;
378
+ }
379
+ .velu-docs-layout__drawer-docselect {
380
+ position: relative;
381
+ }
382
+ .velu-docs-layout__drawer-docselect-btn {
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: space-between;
386
+ inline-size: 100%;
387
+ /* 8px top/bottom — matches the modular-scale step --s-3 (~0.5rem). */
388
+ padding-block: var(--s-3);
389
+ padding-inline: var(--s0);
390
+ background: var(--page-bg);
391
+ border: var(--border-width) solid var(--border-color);
392
+ border-radius: var(--radius-sm);
393
+ font: inherit;
394
+ font-size: var(--f-h5);
395
+ color: var(--text-color);
396
+ text-align: start;
397
+ cursor: pointer;
398
+ transition: border-color 0.12s ease;
399
+ }
400
+ .velu-docs-layout__drawer-docselect-btn:hover {
401
+ border-color: var(--accent-color);
402
+ }
403
+ .velu-docs-layout__drawer-docselect-label {
404
+ flex: 1;
405
+ min-inline-size: 0;
406
+ overflow: hidden;
407
+ text-overflow: ellipsis;
408
+ white-space: nowrap;
409
+ }
410
+ .velu-docs-layout__drawer-docselect-chev {
411
+ inline-size: 1em;
412
+ block-size: 1em;
413
+ stroke-width: 1;
414
+ /* Match the label color (not the muted variant the kebab/search
415
+ icons use) — the chevron reads as part of the trigger text, not
416
+ a secondary indicator. */
417
+ color: var(--text-color);
418
+ stroke: currentColor;
419
+ flex: none;
420
+ transition: transform 0.18s ease;
421
+ }
422
+ .velu-docs-layout__drawer-docselect[data-open='true']
423
+ .velu-docs-layout__drawer-docselect-chev {
424
+ transform: rotate(180deg);
425
+ }
426
+
427
+ /* Menu — anchored below the trigger, full width of the wrapper.
428
+ Always in the DOM so the open/close transition can run in both
429
+ directions; visibility delayed so the closed menu isn't focusable
430
+ or click-able. */
431
+ .velu-docs-layout__drawer-docselect-menu {
432
+ position: absolute;
433
+ inset-block-start: calc(100% + var(--s-3));
434
+ inset-inline-start: 0;
435
+ inset-inline-end: 0;
436
+ margin: 0;
437
+ padding-block: var(--s-2);
438
+ padding-inline: 0;
439
+ list-style: none;
440
+ background: var(--page-bg);
441
+ border: var(--border-width) solid var(--border-color);
442
+ border-radius: var(--radius-sm);
443
+ box-shadow: 0 var(--s-2) var(--s1)
444
+ color-mix(in srgb, #000 12%, transparent);
445
+ opacity: 0;
446
+ visibility: hidden;
447
+ transform: translateY(-0.25rem);
448
+ pointer-events: none;
449
+ transition:
450
+ opacity 0.15s ease,
451
+ transform 0.15s ease,
452
+ visibility 0s linear 0.15s;
453
+ z-index: 1;
454
+ }
455
+ .velu-docs-layout__drawer-docselect[data-open='true']
456
+ .velu-docs-layout__drawer-docselect-menu {
457
+ opacity: 1;
458
+ visibility: visible;
459
+ transform: translateY(0);
460
+ pointer-events: auto;
461
+ transition:
462
+ opacity 0.15s ease,
463
+ transform 0.15s ease,
464
+ visibility 0s;
465
+ }
466
+ .velu-docs-layout__drawer-docselect-item {
467
+ display: block;
468
+ padding-block: var(--s-2);
469
+ padding-inline: var(--s0);
470
+ font-size: var(--f-h5);
471
+ color: var(--text-color);
472
+ text-decoration: none;
473
+ cursor: pointer;
474
+ transition: background 0.12s ease, color 0.12s ease;
475
+ }
476
+ .velu-docs-layout__drawer-docselect-item:hover {
477
+ background: var(--surface-color);
478
+ color: var(--accent-color);
479
+ }
480
+
481
+ /* ── Sidebar toggle (narrow-width only) ────────────────────────────── */
482
+ /* Small chevron button straddling the sidebar/article boundary.
483
+ `position: fixed` (not absolute) so it stays pinned to the
484
+ viewport while the article scrolls — the chevron is always
485
+ reachable, never buried below the fold. It sits one button-radius
486
+ below the header and is centred on the boundary between the
487
+ sidebar/rail and the article column. That boundary is exposed
488
+ via the `--velu-sidebar-boundary` custom property (defaults to
489
+ 240px when the sidebar is open; collapses to the rail width
490
+ when closed — see @container rules below).
491
+
492
+ Hidden by default; revealed only at narrow widths. */
493
+ .velu-docs-layout__sidebar-toggle {
494
+ display: none;
495
+ position: fixed;
496
+ /* Toggle sits well below the TocBar so it doesn't crowd the
497
+ bar's bottom border. --s5 (~49px) clears the TocBar height
498
+ itself; adding --s2 (~25px) gives a clear breathing gap. */
499
+ inset-block-start: calc(var(--velu-header-height) + var(--s5) + var(--s2));
500
+ /* Sidebar's right edge minus half the toggle's own width — centres
501
+ the 2rem button on the divider. Tracks the actual sidebar width. */
502
+ inset-inline-start: calc(var(--velu-sidebar-boundary, var(--velu-sidebar-width)) - 1rem);
503
+ inline-size: 2rem;
504
+ block-size: 2rem;
505
+ align-items: center;
506
+ justify-content: center;
507
+ padding: 0;
508
+ border: var(--border-width) solid var(--border-color);
509
+ border-radius: 999px;
510
+ background: var(--page-bg);
511
+ color: var(--muted-color);
512
+ cursor: pointer;
513
+ transition: border-color 0.12s ease, color 0.12s ease,
514
+ inset-inline-start 0.2s ease;
515
+ z-index: 11;
516
+ }
517
+ .velu-docs-layout__sidebar-toggle:hover {
518
+ border-color: var(--accent-color);
519
+ color: var(--accent-color);
520
+ }
521
+
522
+ /* ── Narrow-width layout ───────────────────────────────────────────── */
523
+ /* Container query (not media query) so the layout reacts to the
524
+ wrapper's inline-size — works the same inside a modal, split pane,
525
+ or shrunken window. At < 1024px the right TOC drops out and the
526
+ centre column expands to fill its slot; the left sidebar becomes
527
+ toggleable via the sidebar-toggle button (default open, per
528
+ design). Closing pushes the centre column over rather than overlaying. */
529
+ @container docs (max-width: 1024px) {
530
+ .velu-docs-layout__aside--right {
531
+ display: none;
532
+ }
533
+ .velu-docs-layout__center {
534
+ margin-inline-end: 0;
535
+ }
536
+ .velu-docs-layout__sidebar-toggle {
537
+ display: inline-flex;
538
+ }
539
+ /* Hairline divider on the left sidebar — only at narrow widths,
540
+ where the sidebar visually separates from the article column.
541
+ Same --surface-color recipe as the header/footer rails. The big
542
+ desktop left-inset is dialed back here (tablet); mobile resets it
543
+ to 0 via the drawer block below. */
544
+ .velu-docs-layout__aside--left {
545
+ border-inline-end: var(--border-width) solid var(--surface-color);
546
+ padding-inline-start: var(--s2);
547
+ /* The divider IS this aside's right edge, so it can only reach as far
548
+ down as the box does. The base rule lifts the box above the footer
549
+ (inset-block-end) to keep nav content clear of it — but that also
550
+ cuts the divider short. Here (where the divider shows) span the box
551
+ to the bottom instead and push the *content* up with matching
552
+ padding, so the line runs to the footer while the nav still clears
553
+ it. The footer's own z-index covers the line where they overlap. */
554
+ inset-block-end: 0;
555
+ padding-block-end: var(--velu-aside-bottom, var(--s4));
556
+ }
557
+ /* Sidebar closed → shrink the aside's `inline-size` from 240px to
558
+ the rail width. The aside stays in the DOM and keeps its own
559
+ border-inline-end — that border IS the rail's right edge when
560
+ collapsed, no separate rail element needed. `overflow-x: clip`
561
+ on the aside (set above) crops the still-rendered Sidebar nav
562
+ to the narrowed box; the opacity rule below fades the nav out
563
+ so the collapsed state reads as a clean stub.
564
+ The aside's `transition: inline-size 0.2s ease` (set above) is
565
+ what makes the collapse animate; the centre column tracks via
566
+ its own `margin-inline-start` transition. */
567
+ .velu-docs-layout[data-sidebar-open='false']
568
+ .velu-docs-layout__aside--left {
569
+ inline-size: var(--velu-rail-width);
570
+ /* Collapsed state is a thin stub — the (faded) nav is irrelevant,
571
+ so suppress the y-scrollbar that would otherwise appear over
572
+ the rail. `overflow-y: auto` is restored implicitly at wide
573
+ widths / open state by the base `.velu-docs-layout__aside`
574
+ rule above. */
575
+ overflow-y: hidden;
576
+ }
577
+ /* Fade out the Sidebar nav while the aside shrinks — opacity
578
+ transitions in parallel with `inline-size`, so the user sees a
579
+ single coherent collapse (shrink + fade) rather than a snap. */
580
+ .velu-docs-layout[data-sidebar-open='false']
581
+ .velu-docs-layout__aside--left > * {
582
+ opacity: 0;
583
+ pointer-events: none;
584
+ }
585
+ .velu-docs-layout[data-sidebar-open='false']
586
+ .velu-docs-layout__center {
587
+ margin-inline-start: var(--velu-rail-width);
588
+ }
589
+ /* Toggle position tracks the active boundary directly — when the
590
+ sidebar is closed the boundary moves from 240px (sidebar's right
591
+ edge) to var(--velu-rail-width) (rail's right edge), so the
592
+ toggle slides with it. Overriding the property here (instead of
593
+ going through a custom-prop indirection) keeps the transition
594
+ reliable across engines. */
595
+ .velu-docs-layout[data-sidebar-open='false']
596
+ .velu-docs-layout__sidebar-toggle {
597
+ inset-inline-start: calc(var(--velu-rail-width) - 1rem);
598
+ }
599
+ /* At narrow widths the article drops its 46rem cap and fills the
600
+ available main width. Cap survives at wide widths so the 46rem
601
+ prose column lives between the two asides. */
602
+ .velu-docs-layout__article {
603
+ max-inline-size: none;
604
+ margin-inline: 0;
605
+ }
606
+ }
607
+
608
+ /* ── Mobile-width layout (< 640px) ─────────────────────────────────── */
609
+ /* At mobile widths the centre column reclaims full width regardless
610
+ of sidebar state. The sidebar becomes a RIGHT-edge drawer (anchored
611
+ to the trailing edge, slides in from the right) — opened by the
612
+ header's burger OR by tapping the breadcrumb strip, both of which
613
+ call `onMenuClick` → toggles the layout's `data-drawer-open` flag.
614
+ Note: mobile uses its OWN `data-drawer-open` attribute, separate
615
+ from `data-sidebar-open` (which drives the narrow-width rail
616
+ collapse). Two independent states avoid the SSR/hydration default
617
+ collision that would force the drawer to render open on refresh.
618
+ The floating chevron toggle is removed; it was a wide-layout
619
+ affordance and is redundant at mobile. */
620
+ @container docs (max-width: 640px) {
621
+ .velu-docs-layout__sidebar-toggle {
622
+ display: none;
623
+ }
624
+ .velu-docs-layout__center,
625
+ .velu-docs-layout[data-sidebar-open='false'] .velu-docs-layout__center {
626
+ margin-inline-start: 0;
627
+ }
628
+ .velu-docs-layout__aside--left {
629
+ /* Anchor to the right edge instead of the left — override the
630
+ base `inset-inline-start: 0` from .velu-docs-layout__aside--left
631
+ so the drawer slides in from the trailing edge. Full viewport
632
+ height comes from the base `inset-block: 0 0` on the aside.
633
+ Drop the header-clearance top padding — the drawer has its own
634
+ brand bar. Inline padding stays at 0 — the Sidebar items have
635
+ their own `padding-inline: var(--s0)` (16px), and the drawer
636
+ chrome below gets matching `padding-inline` directly, so the
637
+ inner content lines up 16px from the drawer edge without
638
+ double-padding. */
639
+ inset-inline-start: auto;
640
+ inset-inline-end: 0;
641
+ /* Full-height drawer — no bottom boundary gap (overrides the
642
+ desktop panel inset). */
643
+ inset-block-end: 0;
644
+ inline-size: min(20rem, 85vw);
645
+ padding-block-start: var(--s0);
646
+ padding-inline: 0;
647
+ /* Closed default — fully off the trailing edge. The
648
+ data-sidebar-open='true' rule below brings it back in. */
649
+ transform: translateX(100%);
650
+ /* Above the sticky header (z-index 30 in page-header.css) so the
651
+ drawer covers the header bar too, but below the search palette
652
+ (z-index 60) and chatbot (z-index 50). The aside already spans
653
+ the full viewport via the base `inset-block: 0 0`. */
654
+ z-index: 35;
655
+ /* Solid body background — the aside has no background by default
656
+ (it inherits the layout's page-bg by virtue of being a child),
657
+ but at mobile it's a `position: fixed` drawer that overlays the
658
+ article + header, so transparency would let everything beneath
659
+ bleed through. Explicit --page-bg makes the drawer opaque. */
660
+ background: var(--page-bg);
661
+ }
662
+ /* Shadow only when the drawer is OPEN — when closed the drawer
663
+ sits off-screen right via `translateX(100%)`, and a left-edge
664
+ shadow would bleed leftward into the visible viewport (a ghost
665
+ band along the right edge). Gating to the open state keeps the
666
+ depth cue while the drawer is visible and hides it the rest of
667
+ the time. */
668
+ .velu-docs-layout[data-drawer-open='true']
669
+ .velu-docs-layout__aside--left {
670
+ transform: translateX(0);
671
+ box-shadow:
672
+ calc(-1 * var(--s0)) 0 var(--s2)
673
+ color-mix(in srgb, #000 18%, transparent);
674
+ }
675
+ /* Drop the narrow-width opacity fade / overflow-hidden on the
676
+ drawer's inner content — at mobile the narrow-width rules above
677
+ (data-sidebar-open='false') still apply because sidebarOpen
678
+ defaults to true and may flip via the chevron at narrow widths,
679
+ but at mobile we want the inner nav fully visible and the
680
+ drawer scrollable regardless of those narrow rules. */
681
+ .velu-docs-layout__aside--left > * {
682
+ opacity: 1;
683
+ pointer-events: auto;
684
+ }
685
+ /* The aside stays a non-scrolling flex column on mobile too; the
686
+ nav region (.velu-docs-nav-scroll) owns the scroll, with the
687
+ context zone pinned above it. */
688
+ .velu-docs-layout__aside--left {
689
+ overflow: hidden;
690
+ }
691
+ /* Drawer chrome — brand bar + docs-set selector — visible only at
692
+ mobile (default-hidden above). Both elements get `padding-inline:
693
+ var(--s0)` so their content lines up at the same 16px-from-edge
694
+ position the Sidebar items below them use (which inherit
695
+ `padding-inline: var(--s0)` from sidebar.css). */
696
+ .velu-docs-layout__drawer-head {
697
+ display: flex;
698
+ align-items: center;
699
+ gap: var(--s-1);
700
+ padding-inline: var(--s0);
701
+ }
702
+ .velu-docs-layout__drawer-head .velu-theme-toggle {
703
+ margin-inline-start: auto;
704
+ }
705
+ .velu-docs-layout__drawer-docselect {
706
+ display: block;
707
+ margin-inline: var(--s0);
708
+ /* Inset 16px from each drawer edge; Stack (column flex with
709
+ align-items: stretch) stretches it across the remaining width.
710
+ `align-self: stretch` explicit for engines that don't honor
711
+ the parent's default stretch. */
712
+ align-self: stretch;
713
+ }
714
+ /* Scrim — present at mobile; toggled on by the mobile nav drawer
715
+ here. The chatbot's bottom-sheet variant exists at the wider
716
+ `< 1024px` threshold, so the chatbot-driven scrim is enabled in
717
+ a SECOND @container block below (at the same wider threshold) —
718
+ keeping the drawer-only scrim scoped to mobile and the
719
+ chatbot-driven scrim available to both narrow and mobile. */
720
+ .velu-docs-layout__scrim {
721
+ display: block;
722
+ }
723
+ .velu-docs-layout[data-drawer-open='true'] .velu-docs-layout__scrim {
724
+ opacity: 1;
725
+ pointer-events: auto;
726
+ visibility: visible;
727
+ transition:
728
+ opacity 0.25s ease,
729
+ visibility 0s;
730
+ }
731
+ }
732
+
733
+ /* ── Chatbot scrim (narrow + mobile, < 1024px) ─────────────────────── */
734
+ /* The chatbot becomes a bottom sheet at < 1024px (see chatbot.css);
735
+ the scrim follows the same threshold so the dim+blur layer appears
736
+ over the article whenever the sheet is up. The mobile-only
737
+ `data-drawer-open` trigger lives in the < 640px block above —
738
+ separate because the drawer itself doesn't exist at narrow widths. */
739
+ @container docs (max-width: 1024px) {
740
+ .velu-docs-layout__scrim {
741
+ display: block;
742
+ }
743
+ .velu-docs-layout[data-chat-open='true'] .velu-docs-layout__scrim {
744
+ opacity: 1;
745
+ pointer-events: auto;
746
+ visibility: visible;
747
+ transition:
748
+ opacity 0.25s ease,
749
+ visibility 0s;
750
+ }
751
+ }
752
+
753
+ /* ── Sidebar context zone + responsive switcher helpers ────────────── */
754
+ /* Product / version / language switchers + anchors above the group
755
+ nav. Inset matches the Sidebar items (which carry padding-inline:
756
+ var(--s0) from sidebar.css). */
757
+ .velu-docs-context {
758
+ flex: none;
759
+ padding-inline: var(--s0);
760
+ }
761
+ .velu-docs-anchors {
762
+ list-style: none;
763
+ margin: 0;
764
+ padding: 0;
765
+ display: flex;
766
+ flex-direction: column;
767
+ gap: var(--s-3);
768
+ }
769
+ .velu-docs-anchors__link {
770
+ display: flex;
771
+ align-items: center;
772
+ gap: var(--s-3);
773
+ color: var(--text-color);
774
+ text-decoration: none;
775
+ font-size: var(--f-h6);
776
+ transition: color 0.12s ease;
777
+ }
778
+ .velu-docs-anchors__link:hover {
779
+ color: var(--accent-color);
780
+ }
781
+ .velu-docs-anchors__icon {
782
+ display: inline-flex;
783
+ flex: none;
784
+ color: var(--muted-color);
785
+ }
786
+ .velu-docs-anchors__link:hover .velu-docs-anchors__icon {
787
+ color: var(--accent-color);
788
+ }
789
+
790
+ /* Show/hide helpers, scoped to the docs container's mobile breakpoint.
791
+ Version + language switchers live in the header at desktop/tablet
792
+ (hide-on-mobile) and re-appear in the drawer context zone on mobile
793
+ (show-on-mobile). */
794
+ .velu-show-on-mobile {
795
+ display: none;
796
+ }
797
+ @container docs (max-width: 640px) {
798
+ .velu-hide-on-mobile {
799
+ display: none !important;
800
+ }
801
+ .velu-show-on-mobile {
802
+ display: block;
803
+ }
804
+ }
805
+
806
+ /* ── 404 takeover ─────────────────────────────────────────────────────── */
807
+ /* On a not-found route the docs chrome (both fixed asides) is hidden and the
808
+ centre column spans full width to center the <NotFound> content. The header
809
+ and (configured) footer stay. */
810
+ .velu-docs-layout[data-not-found='true'] .velu-docs-layout__aside {
811
+ display: none;
812
+ }
813
+ .velu-docs-layout[data-not-found='true'] .velu-docs-layout__center {
814
+ margin-inline: 0;
815
+ }
816
+ .velu-404-main {
817
+ display: flex;
818
+ align-items: center;
819
+ justify-content: center;
820
+ min-block-size: 70vh;
821
+ padding: var(--s4) var(--s2);
822
+ }