@veluai/velu 0.2.41 → 0.2.43

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 (42) hide show
  1. package/dist/cli.js +43 -43
  2. package/docs/kadhir.md +31 -0
  3. package/docs/mintlify-migration.md +2 -0
  4. package/package.json +3 -2
  5. package/runtime/velu-ui/base.css +4 -0
  6. package/runtime/velu-ui/components/ApiReferencePage.jsx +4 -4
  7. package/runtime/velu-ui/components/ApiSamples.jsx +2 -2
  8. package/runtime/velu-ui/components/Card.jsx +6 -1
  9. package/runtime/velu-ui/components/PageFooter.jsx +1 -1
  10. package/runtime/velu-ui/components/PageHeader.jsx +8 -19
  11. package/runtime/velu-ui/components/PoweredBy.jsx +2 -0
  12. package/runtime/velu-ui/components/VepaFooter.jsx +5 -4
  13. package/runtime/velu-ui/components/api-page.css +6 -6
  14. package/runtime/velu-ui/components/api.css +2 -2
  15. package/runtime/velu-ui/components/ask-bar.css +3 -3
  16. package/runtime/velu-ui/components/changelog-filters.css +5 -5
  17. package/runtime/velu-ui/components/chatbot.css +5 -5
  18. package/runtime/velu-ui/components/code-block.css +2 -2
  19. package/runtime/velu-ui/components/context-menu.css +1 -1
  20. package/runtime/velu-ui/components/docs-layout.css +3 -3
  21. package/runtime/velu-ui/components/field.css +1 -1
  22. package/runtime/velu-ui/components/image.css +1 -1
  23. package/runtime/velu-ui/components/not-found.css +1 -1
  24. package/runtime/velu-ui/components/page-feedback.css +5 -5
  25. package/runtime/velu-ui/components/page-footer.css +1 -1
  26. package/runtime/velu-ui/components/page-header.css +11 -3
  27. package/runtime/velu-ui/components/page-nav.css +1 -1
  28. package/runtime/velu-ui/components/powered-by.css +1 -1
  29. package/runtime/velu-ui/components/prompt.css +2 -2
  30. package/runtime/velu-ui/components/search.css +1 -1
  31. package/runtime/velu-ui/components/sidebar.css +2 -2
  32. package/runtime/velu-ui/components/toc-bar.css +2 -2
  33. package/runtime/velu-ui/components/update.css +1 -1
  34. package/runtime/velu-ui/lib/velu-mark.jsx +20 -0
  35. package/runtime/velu-ui/styles.css +13 -1
  36. package/runtime/velu-ui/themes/aalam.css +19 -8
  37. package/runtime/velu-ui/themes/kadhir.css +77 -0
  38. package/runtime/velu-ui/themes/nila.css +6 -1
  39. package/runtime/velu-ui/themes/thulir.css +240 -231
  40. package/runtime/velu-ui/themes/vepa.css +21 -10
  41. package/schema/velu.schema.json +1 -1
  42. package/src/runtime/App.jsx +49 -37
package/docs/kadhir.md ADDED
@@ -0,0 +1,31 @@
1
+ # Kadhir
2
+
3
+ Kadhir is Velu's independently implemented Luma equivalent. Mintlify migration maps `theme: "luma"` to `theme: "kadhir"`; native Velu sites can select Kadhir directly.
4
+
5
+ The implementation uses the existing Velu component and navigation infrastructure, with a new single-row 48px desktop header, pill tabs, compact search, a 248px sidebar, a 648px reading column, and a separate API sample rail. Mobile uses the accessible left drawer and on-page TOC. Pagination has 48px separation from content.
6
+
7
+ Reference: the cached npm renderer `@mintlify/client@0.0.3566`. Luma-specific classes were inspected in `.next/server/chunks/ssr/_6753cfba._.js` and `_450d9e92._.js`. These are reference inputs, not distributed code. Reference API data was generated using `@mintlify/validation@0.1.849` and its `openApiToSchemaGraph` export.
8
+
9
+ ## Local comparison
10
+
11
+ The fresh fixture is `.incoming/kadhir/source`. It contains 220 pages, three products, two versions, API operations, component examples, and a custom stylesheet/script. The migrated output is `.incoming/kadhir/final`.
12
+
13
+ ```powershell
14
+ node packages/velu-cli/scripts/build.mjs
15
+ node packages/velu-cli/dist/cli.js migrate mintlify .incoming/kadhir/source --out .incoming/kadhir/fresh-output
16
+ $env:PORT='8476'
17
+ node packages/velu-cli/dist/cli.js dev .incoming/kadhir/fresh-output
18
+ ```
19
+
20
+ Browser tests expect that fixture on port 8476:
21
+
22
+ ```powershell
23
+ node --test packages/velu-cli/test/kadhir.test.mjs
24
+ node packages/velu-cli/test/kadhir-tabs-browser.mjs
25
+ node packages/velu-cli/test/kadhir-navigation-browser.mjs
26
+ node packages/velu-cli/test/kadhir-mobile-browser.mjs
27
+ ```
28
+
29
+ Tests cover tab clicks, API content, products/versions, reloads, long sidebars, drawer keyboard behavior, TOC clearance, and overflow. Screenshots and reference preparation scripts are under `.incoming/kadhir/`.
30
+
31
+ This is not a pixel-perfect guarantee. Icon artwork, syntax highlighting, generated response examples, component spacing, and arbitrary custom DOM selectors can differ. The fixture migration reports compatibility warnings for endpoint snapshots, component adapters, and CSS selectors; review those before deploying a migrated site.
@@ -163,3 +163,5 @@ Tests cover clean and review-required outputs, source immutability, JSONC/YAML
163
163
  references, ignore negation, collisions/junctions, script preservation, AST
164
164
  rewrites, assets, endpoint snapshots, generated-project validation, and CLI exit
165
165
  codes. No network is needed for the migration tests.
166
+
167
+ Luma sites migrate to the Kadhir theme. See [Kadhir](kadhir.md) for layout details and verification.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veluai/velu",
3
- "version": "0.2.41",
3
+ "version": "0.2.43",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -21,7 +21,8 @@
21
21
  "docs/vepa.md",
22
22
  "docs/thulir.md",
23
23
  "docs/aalam.md",
24
- "docs/nila.md"
24
+ "docs/nila.md",
25
+ "docs/kadhir.md"
25
26
  ],
26
27
  "scripts": {
27
28
  "build": "node scripts/build.mjs",
@@ -107,6 +107,10 @@
107
107
  --font-sans: "Google Sans Flex", sans-serif;
108
108
  --font-mono: "Google Sans Code", ui-monospace, SFMono-Regular, monospace;
109
109
  --font-brand: "Outfit", var(--font-sans);
110
+ /* Brand wordmark (header site name, footer credits) — always
111
+ Google Sans Flex on every theme for a consistent Velu identity,
112
+ even when a project overrides --font-sans. */
113
+ --font-wordmark: "Google Sans Flex", var(--font-sans);
110
114
  font-family: var(--font-sans);
111
115
 
112
116
  /* Approx height of the docs PageHeader (used as the offset for sticky
@@ -78,7 +78,7 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
78
78
  className="velu-api-page__tryit"
79
79
  />
80
80
 
81
- {(preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset)) && <div className="velu-api-page__inline-samples"><ApiSamples idPrefix="inline-" samples={samples} responses={operation.responses} preset={preset} title={operation.title} /></div>}
81
+ {(preset === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset)) && <div className="velu-api-page__inline-samples"><ApiSamples idPrefix="inline-" samples={samples} responses={operation.responses} preset={preset} title={operation.title} /></div>}
82
82
  {children && <div className="velu-api-page__custom-content">{children}</div>}
83
83
 
84
84
  {/* Authorizations */}
@@ -114,7 +114,7 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
114
114
  {operation.body?.fields?.length > 0 && (
115
115
  <api-section class="velu-api-page__section">
116
116
  <h2 data-component="api-section-heading">Body</h2>
117
- {['thulir', 'aalam', 'nila'].includes(preset) && operation.body.contentType && <span className="velu-api-body-type">{operation.body.contentType}</span>}
117
+ {['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) && operation.body.contentType && <span className="velu-api-body-type">{operation.body.contentType}</span>}
118
118
  {operation.body.fields.map((f, i) => (
119
119
  <Field key={i} name={f.name} type={f.type} required={f.required}>
120
120
  {f.description}
@@ -180,10 +180,10 @@ function EnumHint({ values }) {
180
180
  }
181
181
 
182
182
  function ResponseSections({ responses, preset }) {
183
- if (preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset)) return <div className="velu-vepa-responses">{responses.map((r, i) => <section key={i}>
183
+ if (preset === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset)) return <div className="velu-vepa-responses">{responses.map((r, i) => <section key={i}>
184
184
  <div className="velu-vepa-responses__status">{r.status}{r.contentType && ` · ${r.contentType}`}</div>
185
185
  {r.description && <p>{r.description}</p>}
186
- {(['thulir', 'aalam', 'nila'].includes(preset) ? [...(r.fields || [])].sort((a, b) => Number(Boolean(b.required)) - Number(Boolean(a.required))) : r.fields)?.map((f, j) => <Field key={f.name || j} name={f.name} type={f.type} required={f.required}>{f.description}</Field>)}
186
+ {(['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) ? [...(r.fields || [])].sort((a, b) => Number(Boolean(b.required)) - Number(Boolean(a.required))) : r.fields)?.map((f, j) => <Field key={f.name || j} name={f.name} type={f.type} required={f.required}>{f.description}</Field>)}
187
187
  </section>)}</div>;
188
188
  return (
189
189
  <AccordionGroup className="velu-api-resp">
@@ -14,7 +14,7 @@ export default function ApiSamples({ samples = [], responses = [], preset, title
14
14
  const sample = samples.find((s) => s.key === selected) || samples[0];
15
15
  // Mint displays JSON request bodies across lines. Keep custom/non-JSON
16
16
  // snippets intact, and change whitespace only in a valid JSON body.
17
- const sampleCode = ['thulir', 'aalam', 'nila'].includes(preset) && sample?.key === 'curl'
17
+ const sampleCode = ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) && sample?.key === 'curl'
18
18
  ? sample.code.replace(/(--data )'([\s\S]*)'$/, (original, prefix, body) => {
19
19
  try { return `${prefix}'\n${JSON.stringify(JSON.parse(body), null, 2)}\n'`; }
20
20
  catch { return original; }
@@ -22,7 +22,7 @@ export default function ApiSamples({ samples = [], responses = [], preset, title
22
22
  const withExample = responses.filter((r) => r.example != null);
23
23
  return (
24
24
  <div className="velu-api-samples">
25
- {sample && (preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset)) ? <section id={idPrefix + 'request-example'} className="velu-vepa-request">
25
+ {sample && (preset === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset)) ? <section id={idPrefix + 'request-example'} className="velu-vepa-request">
26
26
  <div className="velu-vepa-request__header"><span>{title}</span><select aria-label="Request example language" value={sample.key} onChange={(e) => setSelected(e.target.value)}>{samples.map((s) => <option key={s.key} value={s.key}>{s.label}</option>)}</select></div>
27
27
  <CodeBlock language={sample.language}>{sampleCode}</CodeBlock>
28
28
  </section> : samples.length > 0 && (
@@ -117,6 +117,11 @@ export function CardGroup({
117
117
  className = '',
118
118
  ...rest
119
119
  }) {
120
+ // Grid column count follows the child count (capped by limit) so a group
121
+ // never renders emptier than it is: 2 cards fill 2 columns instead of
122
+ // leaving a third one blank. Mintlify's CardGroup defaults to 2 columns.
123
+ const count = React.Children.count(children);
124
+ const cols = Math.max(1, Math.min(4, Math.min(Number(limit) || 2, count || 1)));
120
125
  return (
121
126
  <Switcher
122
127
  as="card-group"
@@ -124,7 +129,7 @@ export function CardGroup({
124
129
  space={space}
125
130
  min={min}
126
131
  limit={limit}
127
- style={{ '--vepa-columns': Math.max(1, Math.min(4, Number(limit) || 2)) }}
132
+ style={{ '--vepa-columns': cols }}
128
133
  {...rest}
129
134
  >
130
135
  {children}
@@ -57,7 +57,7 @@ export default function PageFooter({
57
57
  ...rest
58
58
  }) {
59
59
  const Link = linkComponent;
60
- if (preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset)) {
60
+ if (preset === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset)) {
61
61
  return <VepaFooter preset={preset} brand={brand} columns={columns} socials={socials} linkComponent={Link} />;
62
62
  }
63
63
  const brandLabel = brand?.label ?? 'Velu';
@@ -53,22 +53,7 @@ import Logo from './Logo.jsx';
53
53
  mobile nav drawer) can render the mark alongside its own chrome.
54
54
  Only the default theme shows it — the vepa / thulir / aalam / nila
55
55
  presets hide it via CSS (text-only branding). */
56
- export function VeluMark() {
57
- return (
58
- <svg
59
- className="velu-header__mark"
60
- viewBox="0 0 32 24"
61
- fill="currentColor"
62
- aria-hidden="true"
63
- >
64
- <path
65
- fillRule="evenodd"
66
- clipRule="evenodd"
67
- d="M29.6564 14.1534C29.5739 14.5616 29.3507 14.9288 29.0248 15.1926C28.699 15.4565 28.2907 15.6007 27.8694 15.6006H21.3702C20.5341 15.6006 19.8051 16.1627 19.6031 16.9638L18.1671 22.6368C18.0685 23.0263 17.8409 23.372 17.5205 23.619C17.2002 23.866 16.8054 24.0001 16.399 24H10.4378C10.1609 24.0001 9.88759 23.9378 9.63868 23.8179C9.38976 23.6979 9.1718 23.5235 9.00135 23.3079C8.8309 23.0923 8.71245 22.8412 8.65499 22.5736C8.59754 22.306 8.6026 22.029 8.66978 21.7636L10.1888 15.7636C10.2874 15.3743 10.5148 15.0287 10.835 14.7817C11.1551 14.5347 11.5496 14.4005 11.9559 14.4004H18.3931C19.2621 14.4004 20.0101 13.7949 20.1801 12.9532L22.3882 2.04774C22.4705 1.63945 22.6936 1.27198 23.0195 1.00793C23.3453 0.74388 23.7538 0.599578 24.1752 0.599605H30.1774C30.4471 0.599529 30.7134 0.658587 30.9572 0.772515C31.201 0.886444 31.4161 1.0524 31.5871 1.25842C31.758 1.46443 31.8806 1.70535 31.9458 1.96383C32.0111 2.2223 32.0175 2.49185 31.9645 2.75305L29.6564 14.1534ZM9.61081 13.5538C9.52825 13.962 9.30507 14.3292 8.97924 14.593C8.65341 14.857 8.24508 15.0011 7.82375 15.001H1.82257C1.55289 15.0011 1.28656 14.942 1.04279 14.8281C0.79902 14.7141 0.583892 14.5482 0.412928 14.3422C0.241965 14.1362 0.11943 13.8952 0.0541639 13.6368C-0.0111017 13.3783 -0.0174699 13.1087 0.0355186 12.8475L2.34359 1.44715C2.42614 1.03904 2.64933 0.671798 2.97516 0.407944C3.30099 0.144089 3.70932 -7.24082e-05 4.13064 7.25899e-08H10.1318C10.4015 -7.55792e-05 10.6678 0.0589818 10.9116 0.172911C11.1554 0.286839 11.3705 0.453799 11.5415 0.659813C11.7124 0.864828 11.835 1.10576 11.9002 1.36423C11.9655 1.62269 11.9719 1.89225 11.9189 2.15344L9.61081 13.5538Z"
68
- />
69
- </svg>
70
- );
71
- }
56
+ export function VeluMark() { return null; }
72
57
 
73
58
  function ActionButton({ action, Link, id }) {
74
59
  const { label, icon, kind = 'outlined', href, onClick, external, component } = action;
@@ -277,11 +262,15 @@ export default function PageHeader({
277
262
  Boolean(tabsTrailing) ||
278
263
  Boolean(onMenuClick);
279
264
 
280
- // A configured logo replaces the mark + wordmark entirely (Mintlify-style).
265
+ // A configured logo renders alongside the wordmark (Mintlify-style
266
+ // headers show mark + name together).
281
267
  const brandBlock = (
282
268
  <Cluster space="var(--s-3)" align="center" className="velu-header__brand" style={{ flexWrap: 'nowrap' }}>
283
269
  {brand?.logo ? (
284
- <Logo logo={brand.logo} alt={brandLabel} />
270
+ <>
271
+ <Logo logo={brand.logo} alt="" />
272
+ <span className="velu-header__wordmark">{brandLabel}</span>
273
+ </>
285
274
  ) : (
286
275
  <>
287
276
  <VeluMark />
@@ -294,7 +283,7 @@ export default function PageHeader({
294
283
  return (
295
284
  <Stack
296
285
  as="header"
297
- space={preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset) ? '0px' : 'var(--s0)'}
286
+ space={preset === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) ? '0px' : 'var(--s0)'}
298
287
  data-has-tabs={tabs.length > 0 ? 'true' : 'false'}
299
288
  className={`velu-header${scrolled ? ' velu-header--scrolled' : ''}${
300
289
  showTabsRow ? '' : ' velu-header--no-tabs'
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import VeluMark from '../lib/velu-mark.jsx';
2
3
 
3
4
  /**
4
5
  * PoweredBy — "Powered by Velu" badge. Right-aligned, sits above the
@@ -23,6 +24,7 @@ export default function PoweredBy({
23
24
  >
24
25
  <span className="velu-powered-by__label">Powered by</span>
25
26
  <span className="velu-powered-by__brand">
27
+ <VeluMark className="velu-powered-by__mark" />
26
28
  <span className="velu-powered-by__wordmark">Velu</span>
27
29
  </span>
28
30
  </a>
@@ -1,20 +1,21 @@
1
1
  import React from 'react';
2
2
  import Logo from './Logo.jsx';
3
+ import VeluMark from '../lib/velu-mark.jsx';
3
4
  import SocialLinks from './SocialLinks.jsx';
4
5
  import ThemePreferenceMenu from './ThemePreferenceMenu.jsx';
5
6
 
6
7
  export default function VepaFooter({ preset = 'vepa', brand, columns = [], socials = [], linkComponent: Link = 'a' }) {
7
8
  const single = columns.length === 1;
8
9
  const completeHeadings = columns.every((c) => c.title);
9
- const root = ['thulir', 'aalam', 'nila'].includes(preset) ? 'thulir-footer' : 'vepa-footer';
10
+ const root = ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) ? 'thulir-footer' : 'vepa-footer';
10
11
  return <footer id="footer" className={root} data-layout={columns.length <= 3 ? 'compact' : 'expanded'} data-single={single} data-headings={completeHeadings}>
11
12
  <div className={`${root}__inner`}>
12
13
  <div className={`${root}__top`}>
13
14
  <div className={`${root}__brand-column`}>
14
15
  <Link href={brand?.href || '/'} className={`${root}__brand`}>
15
- {brand?.logo ? <Logo logo={brand.logo} alt={brand.label} /> : <span>{brand?.label || 'Velu'}</span>}
16
+ {brand?.logo ? <><Logo logo={brand.logo} alt="" /><span>{brand?.label || 'Velu'}</span></> : <span>{brand?.label || 'Velu'}</span>}
16
17
  </Link>
17
- {['thulir', 'aalam', 'nila'].includes(preset) && <SocialLinks socials={socials} className="thulir-footer__brand-mobile-socials" />}
18
+ {['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) && <SocialLinks socials={socials} className="thulir-footer__brand-mobile-socials" />}
18
19
  <SocialLinks socials={socials} className={`${root}__brand-socials`} />
19
20
  </div>
20
21
  <div className={`${root}__columns`} style={{ '--footer-columns': columns.length }}>
@@ -34,7 +35,7 @@ export default function VepaFooter({ preset = 'vepa', brand, columns = [], socia
34
35
  {!completeHeadings && <SocialLinks socials={socials} className={`${root}__mobile-socials`} />}
35
36
  <div className={`${root}__divider`} />
36
37
  <div className={`${root}__bottom`}>
37
- <a href="https://veludocs.com" target="_blank" rel="noreferrer" className={`${root}__powered`}>Powered by <strong>Velu</strong></a>
38
+ <a href="https://veludocs.com" target="_blank" rel="noreferrer" className={`${root}__powered`}>Powered by <VeluMark className={`${root}__powered-mark`} /><strong>Velu</strong></a>
38
39
  <ThemePreferenceMenu inline />
39
40
  </div>
40
41
  </div>
@@ -164,7 +164,7 @@
164
164
  justify-content: center;
165
165
  padding: clamp(var(--s1), 6vh, var(--s5)) var(--s2);
166
166
  overflow-y: auto;
167
- background: color-mix(in srgb, var(--page-bg) 35%, rgba(0, 0, 0, 0.55));
167
+ background: rgb(from var(--page-bg) r g b / 40%), rgba(0, 0, 0, 0.55);
168
168
  backdrop-filter: blur(6px) saturate(1.1);
169
169
  -webkit-backdrop-filter: blur(6px) saturate(1.1);
170
170
  animation: velu-api-modal-in 140ms ease-out;
@@ -204,8 +204,8 @@
204
204
  border-radius: var(--radius-xs, 4px);
205
205
  text-transform: uppercase;
206
206
  }
207
- .velu-sidebar__method.velu-method-badge--get { color: var(--get-pill-color); background: color-mix(in srgb, var(--get-pill-color) 14%, transparent); }
208
- .velu-sidebar__method.velu-method-badge--post { color: var(--post-pill-color); background: color-mix(in srgb, var(--post-pill-color) 14%, transparent); }
209
- .velu-sidebar__method.velu-method-badge--put { color: var(--put-pill-color); background: color-mix(in srgb, var(--put-pill-color) 14%, transparent); }
210
- .velu-sidebar__method.velu-method-badge--patch { color: var(--patch-pill-color); background: color-mix(in srgb, var(--patch-pill-color) 14%, transparent); }
211
- .velu-sidebar__method.velu-method-badge--delete { color: var(--delete-pill-color); background: color-mix(in srgb, var(--delete-pill-color) 14%, transparent); }
207
+ .velu-sidebar__method.velu-method-badge--get { color: var(--get-pill-color); background: rgb(from var(--get-pill-color) r g b / 14%); }
208
+ .velu-sidebar__method.velu-method-badge--post { color: var(--post-pill-color); background: rgb(from var(--post-pill-color) r g b / 14%); }
209
+ .velu-sidebar__method.velu-method-badge--put { color: var(--put-pill-color); background: rgb(from var(--put-pill-color) r g b / 14%); }
210
+ .velu-sidebar__method.velu-method-badge--patch { color: var(--patch-pill-color); background: rgb(from var(--patch-pill-color) r g b / 14%); }
211
+ .velu-sidebar__method.velu-method-badge--delete { color: var(--delete-pill-color); background: rgb(from var(--delete-pill-color) r g b / 14%); }
@@ -194,7 +194,7 @@
194
194
  cursor: pointer;
195
195
  }
196
196
  .velu-try-it__cta:hover {
197
- background: color-mix(in srgb, var(--method-cta, var(--accent-color)) 88%, #000);
197
+ background: rgb(from var(--method-cta, var(--accent-color)) calc(r * 0.88) calc(g * 0.88) calc(b * 0.88));
198
198
  }
199
199
  .velu-try-it__cta:disabled {
200
200
  cursor: default;
@@ -528,7 +528,7 @@
528
528
  align-items: center;
529
529
  padding-block: 0;
530
530
  padding-inline: var(--s-3);
531
- background: color-mix(in srgb, var(--accent-color) 18%, transparent);
531
+ background: rgb(from var(--accent-color) r g b / 18%);
532
532
  border-radius: var(--radius-sm);
533
533
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
534
534
  font-size: var(--f-h6);
@@ -43,7 +43,7 @@
43
43
  border-color: var(--accent-color);
44
44
  box-shadow:
45
45
  0 0 0 var(--border-width) var(--accent-color),
46
- 0 0 var(--s1) color-mix(in srgb, var(--accent-color) 40%, transparent);
46
+ 0 0 var(--s1) rgb(from var(--accent-color) r g b / 40%);
47
47
  }
48
48
 
49
49
  /* Multi-line input. padding-inline-end clears the send button so typed
@@ -87,7 +87,7 @@
87
87
  cursor: pointer;
88
88
  }
89
89
  .velu-ask-bar__send:hover {
90
- background: color-mix(in srgb, var(--accent-color) 88%, #000);
90
+ background: rgb(from var(--accent-color) calc(r * 0.88) calc(g * 0.88) calc(b * 0.88));
91
91
  }
92
92
  .velu-ask-bar__send:focus-visible {
93
93
  outline: var(--border-width) solid var(--text-color);
@@ -97,7 +97,7 @@
97
97
  palette) so the button reads as "not yet ready" rather than a
98
98
  different control entirely. No pointer events / hover affordance. */
99
99
  .velu-ask-bar__send:disabled {
100
- background: color-mix(in srgb, var(--accent-color) 35%, transparent);
100
+ background: rgb(from var(--accent-color) r g b / 35%);
101
101
  cursor: not-allowed;
102
102
  pointer-events: none;
103
103
  }
@@ -26,18 +26,18 @@
26
26
  font-size: var(--f-h6);
27
27
  font-weight: var(--weight-medium);
28
28
  color: var(--text-color);
29
- background: color-mix(in srgb, var(--muted-color) 14%, transparent);
29
+ background: rgb(from var(--muted-color) r g b / 14%);
30
30
  border: var(--border-width) solid transparent;
31
31
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
32
32
  }
33
33
 
34
34
  .velu-changelog-filter:hover {
35
- border-color: color-mix(in srgb, var(--accent-color) 40%, var(--border-color));
36
- background: color-mix(in srgb, var(--accent-color) 8%, transparent);
35
+ border-color: rgb(from var(--accent-color) r g b / 50%);
36
+ background: rgb(from var(--accent-color) r g b / 8%);
37
37
  }
38
38
 
39
39
  .velu-changelog-filter--active {
40
40
  color: var(--accent-color);
41
- border-color: color-mix(in srgb, var(--accent-color) 55%, transparent);
42
- background: color-mix(in srgb, var(--accent-color) 12%, transparent);
41
+ border-color: rgb(from var(--accent-color) r g b / 55%);
42
+ background: rgb(from var(--accent-color) r g b / 12%);
43
43
  }
@@ -215,7 +215,7 @@
215
215
  color: var(--text-color);
216
216
  }
217
217
  .velu-chatbot__iconbtn.is-active {
218
- background: color-mix(in srgb, var(--accent-color) 12%, transparent);
218
+ background: rgb(from var(--accent-color) r g b / 12%);
219
219
  color: var(--accent-color);
220
220
  }
221
221
 
@@ -322,7 +322,7 @@
322
322
  }
323
323
  .velu-chatbot__suggest:hover {
324
324
  border-color: var(--accent-color);
325
- background: color-mix(in srgb, var(--accent-color) 6%, transparent);
325
+ background: rgb(from var(--accent-color) r g b / 6%);
326
326
  }
327
327
  .velu-chatbot__suggest svg {
328
328
  flex: none;
@@ -497,7 +497,7 @@
497
497
  font-size: var(--f-h7);
498
498
  font-weight: var(--weight-medium);
499
499
  color: var(--accent-color);
500
- background: color-mix(in srgb, var(--accent-color) 14%, transparent);
500
+ background: rgb(from var(--accent-color) r g b / 14%);
501
501
  border: var(--border-width) solid transparent;
502
502
  border-radius: var(--radius-sm);
503
503
  text-decoration: none;
@@ -552,7 +552,7 @@
552
552
  font-size: var(--f-h7);
553
553
  font-weight: var(--weight-medium);
554
554
  color: var(--accent-color);
555
- background: color-mix(in srgb, var(--accent-color) 14%, transparent);
555
+ background: rgb(from var(--accent-color) r g b / 14%);
556
556
  border-radius: var(--radius-sm);
557
557
  }
558
558
  .velu-chatbot__source-meta {
@@ -635,7 +635,7 @@
635
635
  }
636
636
  /* Chosen thumb (up/down) — accent tint so the click is clearly registered. */
637
637
  .velu-chatbot__msg-action.is-active {
638
- background: color-mix(in srgb, var(--accent-color) 12%, transparent);
638
+ background: rgb(from var(--accent-color) r g b / 12%);
639
639
  color: var(--accent-color);
640
640
  }
641
641
 
@@ -128,7 +128,7 @@
128
128
  inline-size: 1.75em;
129
129
  block-size: 1.75em;
130
130
  padding: 0;
131
- background: color-mix(in srgb, var(--surface-color) 75%, transparent);
131
+ background: rgb(from var(--surface-color) r g b / 75%);
132
132
  -webkit-backdrop-filter: blur(6px);
133
133
  backdrop-filter: blur(6px);
134
134
  border: var(--border-width) solid var(--border-color);
@@ -160,7 +160,7 @@
160
160
  the code text. The bg is a subtle accent tint — works in both
161
161
  light + dark via [data-theme] color-mix. */
162
162
  .velu-code-block__line--highlight {
163
- background: color-mix(in srgb, var(--accent-color) 12%, transparent);
163
+ background: rgb(from var(--accent-color) r g b / 12%);
164
164
  box-shadow: inset 2px 0 0 0 var(--accent-color);
165
165
  margin-inline: calc(var(--s0) * -1);
166
166
  padding-inline: var(--s0);
@@ -51,7 +51,7 @@
51
51
  .velu-context-menu__rss:hover {
52
52
  color: var(--accent-color);
53
53
  border-color: var(--accent-color);
54
- background: color-mix(in srgb, var(--accent-color) 8%, transparent);
54
+ background: rgb(from var(--accent-color) r g b / 8%);
55
55
  }
56
56
 
57
57
  /* The split button: "Copy Page" + a divider + a chevron toggle, joined as one
@@ -194,12 +194,12 @@
194
194
  scrollbar-width: thin;
195
195
  /* Firefox: thumb then track. */
196
196
  scrollbar-color: var(--muted-color)
197
- color-mix(in srgb, var(--muted-color) 28%, var(--page-bg));
197
+ rgb(from var(--muted-color) r g b / 28%);
198
198
  /* Chromium overlay scrollbars often ignore ::-webkit-scrollbar-track,
199
199
  so paint a permanent rail on the inline-end edge as well. */
200
200
  background-image: linear-gradient(
201
201
  to left,
202
- color-mix(in srgb, var(--muted-color) 28%, var(--page-bg)) 10px,
202
+ rgb(from var(--muted-color) r g b / 28%) 10px,
203
203
  transparent 10px
204
204
  );
205
205
  }
@@ -207,7 +207,7 @@
207
207
  inline-size: 10px;
208
208
  }
209
209
  .velu-docs-nav-scroll::-webkit-scrollbar-track {
210
- background: color-mix(in srgb, var(--muted-color) 28%, var(--page-bg));
210
+ background: rgb(from var(--muted-color) r g b / 28%);
211
211
  border-radius: 0;
212
212
  }
213
213
  .velu-docs-nav-scroll::-webkit-scrollbar-thumb {
@@ -46,7 +46,7 @@
46
46
  align-items: center;
47
47
  padding-block: 0;
48
48
  padding-inline: var(--s-3);
49
- background: color-mix(in srgb, var(--accent-color) 18%, transparent);
49
+ background: rgb(from var(--accent-color) r g b / 18%);
50
50
  border-radius: var(--radius-sm);
51
51
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
52
52
  font-size: var(--f-h6);
@@ -205,7 +205,7 @@
205
205
  inline-size: 15%;
206
206
  block-size: 0.5em;
207
207
  border-radius: 999px;
208
- background: color-mix(in srgb, var(--text-color) 14%, transparent);
208
+ background: rgb(from var(--text-color) r g b / 14%);
209
209
  }
210
210
 
211
211
  /* ── Device-frame variant ───────────────────────────────────────────────── */
@@ -86,7 +86,7 @@
86
86
  font-weight: var(--weight-bold);
87
87
  }
88
88
  .velu-404__btn--primary:hover {
89
- background: color-mix(in srgb, var(--accent-color) 88%, #000);
89
+ background: rgb(from var(--accent-color) calc(r * 0.88) calc(g * 0.88) calc(b * 0.88));
90
90
  }
91
91
  .velu-404__btn-ic {
92
92
  display: inline-flex;
@@ -45,7 +45,7 @@
45
45
  The vote stays changeable: clicking the other button moves it here. */
46
46
  .velu-feedback__btn--chosen {
47
47
  border-color: var(--accent-color);
48
- background: color-mix(in srgb, var(--accent-color) 10%, transparent);
48
+ background: rgb(from var(--accent-color) r g b / 10%);
49
49
  color: var(--accent-color);
50
50
  }
51
51
  .velu-feedback__btn--chosen svg {
@@ -168,7 +168,7 @@
168
168
  color: #fff;
169
169
  }
170
170
  .velu-feedback__action--submit:hover {
171
- background: color-mix(in srgb, var(--accent-color) 88%, #000);
171
+ background: rgb(from var(--accent-color) calc(r * 0.88) calc(g * 0.88) calc(b * 0.88));
172
172
  }
173
173
 
174
174
  /* ── Submitted confirmation — sits below the divider line ───────────── */
@@ -214,13 +214,13 @@
214
214
  color: var(--accent-color);
215
215
  }
216
216
  .velu-feedback__heart--1 {
217
- color: color-mix(in srgb, var(--accent-color) 60%, #fff);
217
+ color: rgb(from var(--accent-color) calc(r * 0.6 + 255 * 0.4) calc(g * 0.6 + 255 * 0.4) calc(b * 0.6 + 255 * 0.4));
218
218
  }
219
219
  .velu-feedback__heart--2 {
220
- color: color-mix(in srgb, var(--accent-color) 55%, var(--text-color));
220
+ color: rgb(from var(--accent-color) calc(r * 0.55) calc(g * 0.55) calc(b * 0.55));
221
221
  }
222
222
  .velu-feedback__heart--3 {
223
- color: color-mix(in srgb, var(--accent-color) 82%, #fff);
223
+ color: rgb(from var(--accent-color) calc(r * 0.82 + 255 * 0.18) calc(g * 0.82 + 255 * 0.18) calc(b * 0.82 + 255 * 0.18));
224
224
  }
225
225
  /* Register the per-heart custom props so they're typed + animatable inside the
226
226
  keyframes. WebKit (newer iOS Safari) won't interpolate UNregistered custom
@@ -58,7 +58,7 @@
58
58
  }
59
59
  /* Outfit Light, per the brand spec — falls back to --font-sans. */
60
60
  .velu-footer__wordmark {
61
- font-family: var(--font-brand);
61
+ font-family: var(--font-wordmark);
62
62
  font-weight: var(--weight-light);
63
63
  font-size: var(--f-h4);
64
64
  line-height: 1;
@@ -54,7 +54,7 @@
54
54
  through faintly underneath. color-mix keeps the recipe token-driven:
55
55
  75% --page-bg + transparent → 75% opaque in either theme. */
56
56
  .velu-header--scrolled {
57
- background: color-mix(in srgb, var(--page-bg) 75%, transparent);
57
+ background: rgb(from var(--page-bg) r g b / 75%);
58
58
  backdrop-filter: saturate(140%) blur(12px);
59
59
  -webkit-backdrop-filter: saturate(140%) blur(12px);
60
60
  /* Lift shadow (theme-aware recipe defined on .velu-header) so the
@@ -108,7 +108,7 @@
108
108
  color: var(--accent-color);
109
109
  }
110
110
  .velu-header__wordmark {
111
- font-family: var(--font-brand);
111
+ font-family: var(--font-wordmark);
112
112
  font-weight: var(--weight-light);
113
113
  /* 32px wordmark. */
114
114
  font-size: 2rem;
@@ -174,7 +174,7 @@
174
174
  color: #fff;
175
175
  }
176
176
  .velu-header__action--primary:hover {
177
- background: color-mix(in srgb, var(--accent-color) 88%, #000);
177
+ background: rgb(from var(--accent-color) calc(r * 0.88) calc(g * 0.88) calc(b * 0.88));
178
178
  }
179
179
  /* Plain text link (navbar.links) — no fill or border, just text that goes
180
180
  accent on hover. Transparent border keeps it the same height as the
@@ -573,3 +573,11 @@
573
573
  font-size: var(--f-h7);
574
574
  opacity: 0.7;
575
575
  }
576
+ /* Ask AI header entry — icon + "Ask AI" must stay on one line (no wrap
577
+ into a stacked pill) in every theme's header, where it shares a narrow
578
+ flex row with search. Icon-circle themes (kadhir) hide the label
579
+ separately; the nowrap + flex:none here keeps the pill intact. */
580
+ #assistant-entry {
581
+ white-space: nowrap;
582
+ flex: none;
583
+ }
@@ -194,7 +194,7 @@
194
194
  flex: none;
195
195
  width: 1px;
196
196
  height: 32px;
197
- background: color-mix(in srgb, var(--muted-color) 14%, transparent);
197
+ background: rgb(from var(--muted-color) r g b / 14%);
198
198
  }
199
199
 
200
200
  [data-theme='dark'] .velu-pagenav__rule {
@@ -81,7 +81,7 @@
81
81
  }
82
82
 
83
83
  .velu-powered-by__wordmark {
84
- font-family: var(--font-brand);
84
+ font-family: var(--font-wordmark);
85
85
  font-weight: var(--weight-light);
86
86
  font-size: var(--f-h4);
87
87
  line-height: 1;
@@ -60,7 +60,7 @@
60
60
  cursor: pointer;
61
61
  }
62
62
  .velu-prompt__copy:hover {
63
- background: color-mix(in srgb, var(--accent-color) 88%, #000);
63
+ background: rgb(from var(--accent-color) calc(r * 0.88) calc(g * 0.88) calc(b * 0.88));
64
64
  }
65
65
  .velu-prompt__copy:focus-visible {
66
66
  outline: var(--border-width) solid var(--text-color);
@@ -91,7 +91,7 @@
91
91
  text-decoration: none;
92
92
  }
93
93
  .velu-prompt__open-cursor:hover {
94
- background: color-mix(in srgb, var(--text-color) 88%, var(--accent-color));
94
+ background: rgb(from var(--text-color) calc(r * 0.88) calc(g * 0.88) calc(b * 0.88));
95
95
  }
96
96
  .velu-prompt__open-cursor:focus-visible {
97
97
  outline: var(--border-width) solid var(--accent-color);
@@ -199,7 +199,7 @@
199
199
  background: var(--surface-color);
200
200
  }
201
201
  .velu-search__row--selected {
202
- background: color-mix(in srgb, var(--accent-color) 12%, transparent);
202
+ background: rgb(from var(--accent-color) r g b / 12%);
203
203
  }
204
204
 
205
205
  .velu-search__row-glyph {