@veluai/velu 0.2.42 → 0.2.44

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/mintlify-migration.md +2 -0
  3. package/package.json +1 -1
  4. package/runtime/velu-ui/base.css +4 -0
  5. package/runtime/velu-ui/components/ApiReferencePage.jsx +4 -4
  6. package/runtime/velu-ui/components/ApiSamples.jsx +2 -2
  7. package/runtime/velu-ui/components/Card.jsx +50 -8
  8. package/runtime/velu-ui/components/PageFooter.jsx +1 -1
  9. package/runtime/velu-ui/components/PageHeader.jsx +12 -18
  10. package/runtime/velu-ui/components/PoweredBy.jsx +2 -0
  11. package/runtime/velu-ui/components/VepaFooter.jsx +5 -4
  12. package/runtime/velu-ui/components/api-page.css +6 -6
  13. package/runtime/velu-ui/components/api.css +2 -2
  14. package/runtime/velu-ui/components/ask-bar.css +3 -3
  15. package/runtime/velu-ui/components/card.css +16 -1
  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 +21 -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 +6 -2
  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
@@ -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.42",
3
+ "version": "0.2.44",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -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 && (
@@ -16,13 +16,19 @@ import Switcher from '../primitives/Switcher.jsx';
16
16
  * Variants (combinable):
17
17
  * - basic: icon (accent, --f-h2) + title (--f-h4) + body
18
18
  * - `horizontal`: icon vertically centered on the left, title+body right
19
- * - `img`: image fills the top, clipped to the radius
19
+ * - `img`: image fills the top, clipped to the radius. Covers both themes,
20
+ * unless `imgLight`/`imgDark` override per theme.
21
+ * - `imgLight` / `imgDark`: per-theme image. When the effective light and
22
+ * dark sources differ, both render and CSS shows the right one per
23
+ * `[data-theme]` (same pattern as Logo's light/dark swap). When only one
24
+ * of the three is present, it covers both themes.
20
25
  * - `cta`: { label, href } → the single clickable "Click Here ›" link
21
26
  *
22
27
  * `icon` is a lucide id string (shared resolveIcon).
23
28
  *
24
29
  * @param {{ title?: React.ReactNode, icon?: string, horizontal?: boolean,
25
- * img?: string, imgAlt?: string, href?: string,
30
+ * img?: string, imgAlt?: string, imgLight?: string, imgDark?: string,
31
+ * href?: string,
26
32
  * cta?: {label: React.ReactNode, href: string},
27
33
  * children?: React.ReactNode, className?: string }} props
28
34
  */
@@ -33,6 +39,8 @@ export function Card({
33
39
  horizontal = false,
34
40
  img,
35
41
  imgAlt = '',
42
+ imgLight,
43
+ imgDark,
36
44
  cta,
37
45
  href,
38
46
  children,
@@ -74,11 +82,40 @@ export function Card({
74
82
  return (
75
83
  <card className={cls} {...rest}>
76
84
  {href && !cta && <a className="velu-card__link" href={href} aria-label={typeof title === 'string' ? title : 'Read more'} />}
77
- {img && (
78
- <div className="velu-card__media" data-component-part="card-image">
79
- <img src={img} alt={imgAlt} loading="lazy" />
80
- </div>
81
- )}
85
+ {(() => {
86
+ const lightSrc = imgLight ?? img;
87
+ const darkSrc = imgDark ?? img;
88
+ if (lightSrc && darkSrc && lightSrc !== darkSrc) {
89
+ return (
90
+ <div className="velu-card__media" data-component-part="card-image">
91
+ <img
92
+ className="velu-card__img velu-card__img--light"
93
+ src={lightSrc}
94
+ alt={imgAlt}
95
+ loading="lazy"
96
+ />
97
+ <img
98
+ className="velu-card__img velu-card__img--dark"
99
+ src={darkSrc}
100
+ alt=""
101
+ aria-hidden="true"
102
+ loading="lazy"
103
+ />
104
+ </div>
105
+ );
106
+ }
107
+ const singleSrc = imgLight ?? imgDark ?? img;
108
+ return singleSrc ? (
109
+ <div className="velu-card__media" data-component-part="card-image">
110
+ <img
111
+ className="velu-card__img"
112
+ src={singleSrc}
113
+ alt={imgAlt}
114
+ loading="lazy"
115
+ />
116
+ </div>
117
+ ) : null;
118
+ })()}
82
119
 
83
120
  <div className="velu-card__body" data-component-part="card-content-container">
84
121
  {horizontal ? (
@@ -117,6 +154,11 @@ export function CardGroup({
117
154
  className = '',
118
155
  ...rest
119
156
  }) {
157
+ // Grid column count follows the child count (capped by limit) so a group
158
+ // never renders emptier than it is: 2 cards fill 2 columns instead of
159
+ // leaving a third one blank. Mintlify's CardGroup defaults to 2 columns.
160
+ const count = React.Children.count(children);
161
+ const cols = Math.max(1, Math.min(4, Math.min(Number(limit) || 2, count || 1)));
120
162
  return (
121
163
  <Switcher
122
164
  as="card-group"
@@ -124,7 +166,7 @@ export function CardGroup({
124
166
  space={space}
125
167
  min={min}
126
168
  limit={limit}
127
- style={{ '--vepa-columns': Math.max(1, Math.min(4, Number(limit) || 2)) }}
169
+ style={{ '--vepa-columns': cols }}
128
170
  {...rest}
129
171
  >
130
172
  {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';
@@ -4,6 +4,7 @@ import resolveIcon from '../lib/resolveIcon.jsx';
4
4
  import Stack from '../primitives/Stack.jsx';
5
5
  import Cluster from '../primitives/Cluster.jsx';
6
6
  import Logo from './Logo.jsx';
7
+ import VeluMarkArt from '../lib/velu-mark.jsx';
7
8
 
8
9
  /**
9
10
  * PageHeader — top of the docs site.
@@ -51,22 +52,11 @@ import Logo from './Logo.jsx';
51
52
  /* Velu brand mark. Color: --accent-color via the wrapping wordmark
52
53
  span; the path uses currentColor. Exported so consumers (e.g. the
53
54
  mobile nav drawer) can render the mark alongside its own chrome.
54
- Only the default theme shows it — the vepa / thulir / aalam / nila
55
- presets hide it via CSS (text-only branding). */
56
- export function VeluMark() {
55
+ Only the default theme shows it — the vepa / thulir / aalam / nila /
56
+ kadhir presets hide it via CSS (text-only branding). */
57
+ export function VeluMark({ className = '', ...rest }) {
57
58
  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>
59
+ <VeluMarkArt className={`velu-header__mark ${className}`.trim()} {...rest} />
70
60
  );
71
61
  }
72
62
 
@@ -277,11 +267,15 @@ export default function PageHeader({
277
267
  Boolean(tabsTrailing) ||
278
268
  Boolean(onMenuClick);
279
269
 
280
- // A configured logo replaces the mark + wordmark entirely (Mintlify-style).
270
+ // A configured logo renders alongside the wordmark (Mintlify-style
271
+ // headers show mark + name together).
281
272
  const brandBlock = (
282
273
  <Cluster space="var(--s-3)" align="center" className="velu-header__brand" style={{ flexWrap: 'nowrap' }}>
283
274
  {brand?.logo ? (
284
- <Logo logo={brand.logo} alt={brandLabel} />
275
+ <>
276
+ <Logo logo={brand.logo} alt="" />
277
+ <span className="velu-header__wordmark">{brandLabel}</span>
278
+ </>
285
279
  ) : (
286
280
  <>
287
281
  <VeluMark />
@@ -294,7 +288,7 @@ export default function PageHeader({
294
288
  return (
295
289
  <Stack
296
290
  as="header"
297
- space={preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset) ? '0px' : 'var(--s0)'}
291
+ space={preset === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) ? '0px' : 'var(--s0)'}
298
292
  data-has-tabs={tabs.length > 0 ? 'true' : 'false'}
299
293
  className={`velu-header${scrolled ? ' velu-header--scrolled' : ''}${
300
294
  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
  }
@@ -46,6 +46,7 @@
46
46
  flex-direction: column;
47
47
  gap: var(--s-2);
48
48
  padding: var(--s0) var(--s3);
49
+ flex: 1; /* fill the card so a CTA can pin to the bottom across a row */
49
50
  }
50
51
  .velu-card--horizontal .velu-card__body {
51
52
  flex-direction: row;
@@ -81,7 +82,8 @@
81
82
  display: inline-flex;
82
83
  align-items: center;
83
84
  gap: var(--s-3);
84
- margin-block-start: var(--s-2);
85
+ margin-block-start: auto; /* pin to the card bottom so CTAs line up across a row */
86
+ padding-block-start: var(--s-2); /* keep the text gap when there is no free space */
85
87
  font-weight: 500;
86
88
  color: var(--text-color);
87
89
  text-decoration: none;
@@ -106,6 +108,19 @@
106
108
  block-size: auto;
107
109
  object-fit: cover;
108
110
  }
111
+ /* Per-theme swap (same pattern as Logo): light image by default, `img` under
112
+ [data-theme=dark]. `display` (not opacity) so the inactive one leaves the
113
+ a11y tree too. A lone `img` (no `imgLight`) has neither modifier and always
114
+ shows. */
115
+ .velu-card__media .velu-card__img--dark {
116
+ display: none;
117
+ }
118
+ [data-theme='dark'] .velu-card__media .velu-card__img--light {
119
+ display: none;
120
+ }
121
+ [data-theme='dark'] .velu-card__media .velu-card__img--dark {
122
+ display: block;
123
+ }
109
124
 
110
125
  /* CardGroup layout is the Switcher primitive (composed in Card.jsx). Its
111
126
  threshold is computed count-aware in Card.jsx so the row flips to a full
@@ -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;