@aranova/tracking-next 0.20.0 → 0.20.1

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.
@@ -0,0 +1,2219 @@
1
+ // src/blog-server.ts
2
+ import "server-only";
3
+
4
+ // ../tracking-core/src/resources/blog/schema.ts
5
+ import { z as z2 } from "zod";
6
+
7
+ // ../tracking-core/src/resources/blog/theme.ts
8
+ import { z } from "zod";
9
+ var BLOG_BREAKPOINTS = ["sm", "md", "lg", "xl", "2xl", "3xl"];
10
+ var BLOG_BREAKPOINT_MIN_WIDTHS_PX = {
11
+ sm: 640,
12
+ md: 768,
13
+ lg: 1024,
14
+ xl: 1280,
15
+ "2xl": 1536,
16
+ "3xl": 1920
17
+ };
18
+ function toBlogBreakpointMap(value) {
19
+ return typeof value === "object" ? value : { base: value };
20
+ }
21
+ var HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
22
+ var FUNCTIONAL_COLOR = /^(rgb|rgba|hsl|hsla|oklch)\([0-9a-zA-Z ,.%/-]*\)$/;
23
+ var LENGTH = /^-?\d+(\.\d+)?(px|rem|em|%|ch|vw)$/;
24
+ var LINE_HEIGHT = /^-?\d+(\.\d+)?(px|rem|em|%|ch|vw)?$/;
25
+ var FONT_FAMILY_TOKEN = /^['"]?[a-zA-Z0-9 -]+['"]?$/;
26
+ var FONT_FAMILY_VAR_TOKEN = /^var\(--[a-zA-Z0-9-]+\)$/;
27
+ var colorValueSchema = z.string().refine((v) => HEX_COLOR.test(v) || FUNCTIONAL_COLOR.test(v), {
28
+ message: "must be a #hex or rgb()/rgba()/hsl()/hsla()/oklch() color"
29
+ });
30
+ var lengthValueSchema = z.string().regex(LENGTH, {
31
+ message: "must be a CSS length (px|rem|em|%|ch|vw)"
32
+ });
33
+ var lineHeightValueSchema = z.string().regex(LINE_HEIGHT, {
34
+ message: "must be a CSS length or unitless line-height"
35
+ });
36
+ var fontWeightSchema = z.number().int().min(100).max(1e3);
37
+ var alignValueSchema = z.enum(["left", "center", "right"]);
38
+ var fontFamilySchema = z.string().refine(
39
+ (v) => {
40
+ const tokens = v.split(",").map((t) => t.trim());
41
+ return tokens.length > 0 && tokens.every((t) => FONT_FAMILY_TOKEN.test(t) || FONT_FAMILY_VAR_TOKEN.test(t));
42
+ },
43
+ { message: "must be a comma-separated list of font-family or var(--custom-property) tokens" }
44
+ );
45
+ function nullishField(schema) {
46
+ return schema.nullish().transform((v) => v ?? void 0);
47
+ }
48
+ function breakpointMapSchema(valueSchema) {
49
+ return z.object({
50
+ base: valueSchema,
51
+ sm: nullishField(valueSchema).catch(void 0),
52
+ md: nullishField(valueSchema).catch(void 0),
53
+ lg: nullishField(valueSchema).catch(void 0),
54
+ xl: nullishField(valueSchema).catch(void 0),
55
+ "2xl": nullishField(valueSchema).catch(void 0),
56
+ "3xl": nullishField(valueSchema).catch(void 0)
57
+ });
58
+ }
59
+ function responsive(valueSchema) {
60
+ return z.union([valueSchema, breakpointMapSchema(valueSchema)]);
61
+ }
62
+ var blogThemeColorsShape = {
63
+ background: colorValueSchema,
64
+ surface: colorValueSchema,
65
+ text: colorValueSchema,
66
+ text_muted: colorValueSchema,
67
+ primary: colorValueSchema,
68
+ primary_foreground: colorValueSchema,
69
+ link: colorValueSchema,
70
+ border: colorValueSchema,
71
+ code_background: colorValueSchema,
72
+ heading: colorValueSchema
73
+ };
74
+ var blogThemeColorsSchema = z.object({
75
+ ...blogThemeColorsShape,
76
+ // The ONE optional color in a resolved theme: when unset, headings fall back
77
+ // to `text` in the stylesheet (`--_heading-color`), so `blogThemeToCssVars`
78
+ // only emits `--ara-blog-color-heading` when a theme actually sets it.
79
+ heading: nullishField(colorValueSchema)
80
+ });
81
+ var blogThemeTypographySchema = z.object({
82
+ heading_family: fontFamilySchema,
83
+ body_family: fontFamilySchema,
84
+ heading_weight: fontWeightSchema,
85
+ body_weight: fontWeightSchema,
86
+ h1_size: responsive(lengthValueSchema),
87
+ h2_size: responsive(lengthValueSchema),
88
+ h3_size: responsive(lengthValueSchema),
89
+ h4_size: responsive(lengthValueSchema),
90
+ body_size: responsive(lengthValueSchema),
91
+ body_line_height: responsive(lineHeightValueSchema),
92
+ small_size: responsive(lengthValueSchema),
93
+ // Scalar (non-responsive) length; the grammar allows negative em values.
94
+ // h1/h2 consume it directly, h3/h4 at half strength (calc in the base CSS).
95
+ heading_letter_spacing: lengthValueSchema,
96
+ // Figure caption text alignment (left/center/right). Site-owner override;
97
+ // default center in the base CSS if unset.
98
+ caption_align: alignValueSchema
99
+ });
100
+ var blogThemeSpacingSchema = z.object({
101
+ content_max_width: responsive(lengthValueSchema),
102
+ block_gap: responsive(lengthValueSchema),
103
+ section_gap: responsive(lengthValueSchema)
104
+ });
105
+ var blogThemeColorsPartialSchema = z.object(
106
+ Object.fromEntries(
107
+ Object.keys(blogThemeColorsShape).map((key) => [key, nullishField(colorValueSchema)])
108
+ )
109
+ );
110
+ var blogThemeSchema = z.object({
111
+ colors: blogThemeColorsSchema,
112
+ colors_dark: nullishField(blogThemeColorsPartialSchema),
113
+ typography: blogThemeTypographySchema,
114
+ spacing: blogThemeSpacingSchema,
115
+ radius: responsive(lengthValueSchema)
116
+ });
117
+ function dropInvalidFields(shape) {
118
+ const out = {};
119
+ for (const [key, schema] of Object.entries(shape)) {
120
+ out[key] = nullTolerant(schema).catch(void 0);
121
+ }
122
+ return out;
123
+ }
124
+ function nullTolerant(schema) {
125
+ if (schema instanceof z.ZodUnion) {
126
+ const options = schema.options;
127
+ return z.union([...options, z.null()]).optional().transform((v) => v ?? void 0);
128
+ }
129
+ return nullishField(schema);
130
+ }
131
+ var blogThemeColorsOverrideSchema = z.object(dropInvalidFields(blogThemeColorsShape));
132
+ var blogThemeTypographyOverrideSchema = z.object(
133
+ dropInvalidFields(blogThemeTypographySchema.shape)
134
+ );
135
+ var blogThemeSpacingOverrideSchema = z.object(dropInvalidFields(blogThemeSpacingSchema.shape));
136
+ var blogThemeOverrideSchema = z.object(
137
+ dropInvalidFields({
138
+ colors: blogThemeColorsOverrideSchema,
139
+ colors_dark: blogThemeColorsOverrideSchema,
140
+ typography: blogThemeTypographyOverrideSchema,
141
+ spacing: blogThemeSpacingOverrideSchema,
142
+ radius: responsive(lengthValueSchema)
143
+ })
144
+ );
145
+ var BASE_BLOG_THEME = {
146
+ colors: {
147
+ background: "#ffffff",
148
+ surface: "#f5f5f5",
149
+ text: "#171717",
150
+ text_muted: "#6b6b6b",
151
+ primary: "#171717",
152
+ primary_foreground: "#ffffff",
153
+ link: "#1d4ed8",
154
+ border: "#e5e5e5",
155
+ code_background: "#f5f5f5"
156
+ },
157
+ typography: {
158
+ heading_family: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
159
+ body_family: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
160
+ heading_weight: 700,
161
+ body_weight: 400,
162
+ // Responsive defaults: comfortable mobile sizes, larger from `md` up.
163
+ // This replaces the hardcoded mobile media query the base CSS used to need.
164
+ h1_size: { base: "1.9rem", md: "2.25rem" },
165
+ h2_size: { base: "1.5rem", md: "1.75rem" },
166
+ h3_size: "1.25rem",
167
+ h4_size: "1.125rem",
168
+ body_size: "1rem",
169
+ body_line_height: "1.7",
170
+ small_size: "0.875rem",
171
+ heading_letter_spacing: "-0.02em",
172
+ caption_align: "center"
173
+ },
174
+ spacing: {
175
+ content_max_width: "42rem",
176
+ block_gap: "1.25rem",
177
+ section_gap: "3rem"
178
+ },
179
+ radius: "8px"
180
+ };
181
+ function sanitizeBlogTheme(raw) {
182
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
183
+ const parsed = blogThemeOverrideSchema.safeParse(raw);
184
+ return parsed.success ? parsed.data : {};
185
+ }
186
+ function mergeGroup(base, over) {
187
+ if (!over) return base;
188
+ const out = { ...base };
189
+ for (const [key, value] of Object.entries(over)) {
190
+ if (value !== void 0) out[key] = value;
191
+ }
192
+ return out;
193
+ }
194
+ function resolveBlogTheme(...layers) {
195
+ let resolved = {
196
+ colors: { ...BASE_BLOG_THEME.colors },
197
+ colors_dark: BASE_BLOG_THEME.colors_dark ? { ...BASE_BLOG_THEME.colors_dark } : void 0,
198
+ typography: { ...BASE_BLOG_THEME.typography },
199
+ spacing: { ...BASE_BLOG_THEME.spacing },
200
+ radius: BASE_BLOG_THEME.radius
201
+ };
202
+ for (const layer of layers) {
203
+ if (!layer) continue;
204
+ resolved = {
205
+ colors: mergeGroup(resolved.colors, layer.colors),
206
+ colors_dark: layer.colors_dark || resolved.colors_dark ? mergeGroup(resolved.colors_dark ?? {}, layer.colors_dark) : void 0,
207
+ typography: mergeGroup(resolved.typography, layer.typography),
208
+ spacing: mergeGroup(resolved.spacing, layer.spacing),
209
+ radius: layer.radius ?? resolved.radius
210
+ };
211
+ }
212
+ return resolved;
213
+ }
214
+ function kebab(key) {
215
+ return key.split("_").join("-");
216
+ }
217
+ function emitResponsiveVar(vars, name, value) {
218
+ const map = toBlogBreakpointMap(value);
219
+ vars[name] = String(map.base);
220
+ for (const bp of BLOG_BREAKPOINTS) {
221
+ const bpValue = map[bp];
222
+ if (bpValue !== void 0) vars[`${name}-${bp}`] = String(bpValue);
223
+ }
224
+ }
225
+ function blogThemeToCssVars(theme) {
226
+ const vars = {};
227
+ const dark = theme.colors_dark;
228
+ const hasDark = !!dark && Object.values(dark).some((v) => v !== void 0);
229
+ const colorKeys = /* @__PURE__ */ new Set([...Object.keys(theme.colors), ...dark ? Object.keys(dark) : []]);
230
+ for (const key of colorKeys) {
231
+ const light = theme.colors[key];
232
+ const darkValue = hasDark ? dark?.[key] : void 0;
233
+ const lightValue = light ?? (darkValue !== void 0 ? theme.colors.text : void 0);
234
+ if (lightValue === void 0) continue;
235
+ vars[`--ara-blog-color-${kebab(key)}`] = darkValue ? `light-dark(${lightValue}, ${darkValue})` : lightValue;
236
+ }
237
+ if (hasDark) vars.colorScheme = "light dark";
238
+ for (const [key, value] of Object.entries(theme.typography)) {
239
+ emitResponsiveVar(vars, `--ara-blog-type-${kebab(key)}`, value);
240
+ }
241
+ for (const [key, value] of Object.entries(theme.spacing)) {
242
+ emitResponsiveVar(vars, `--ara-blog-space-${kebab(key)}`, value);
243
+ }
244
+ emitResponsiveVar(vars, "--ara-blog-radius", theme.radius);
245
+ return vars;
246
+ }
247
+
248
+ // ../tracking-core/src/resources/blog/schema.ts
249
+ var schemaVersionSchema = z2.literal(1);
250
+ var blogHeroImageSchema = z2.object({
251
+ url: z2.string(),
252
+ alt: z2.string().catch(""),
253
+ width: z2.number().int(),
254
+ height: z2.number().int()
255
+ });
256
+ var blogManifestEntrySchema = z2.object({
257
+ slug: z2.string(),
258
+ title: z2.string(),
259
+ description: z2.string().catch(""),
260
+ content_hash: z2.string(),
261
+ content_key: z2.string(),
262
+ published_at: z2.string(),
263
+ updated_at: z2.string(),
264
+ // Estimated read time in whole minutes (computed at publish from the post
265
+ // body). 0 when unknown; index cards render "N min read".
266
+ reading_minutes: z2.number().int().catch(0),
267
+ // A malformed tags array / hero_image degrades gracefully instead of
268
+ // dropping the whole entry.
269
+ tags: z2.array(z2.string()).catch([]),
270
+ hero_image: blogHeroImageSchema.nullable().catch(null)
271
+ });
272
+ var blogManifestWireSchema = z2.object({
273
+ schema_version: schemaVersionSchema,
274
+ namespace: z2.string(),
275
+ generated_at: z2.string(),
276
+ posts: z2.array(blogManifestEntrySchema).catch([])
277
+ });
278
+ var blogManifestSchema = blogManifestWireSchema.extend({
279
+ // A single malformed entry is dropped; the rest of the manifest survives.
280
+ posts: z2.array(blogManifestEntrySchema.nullable().catch(null)).transform((entries) => entries.filter((e) => e !== null)).catch([])
281
+ });
282
+ var blogImageSchema = z2.object({
283
+ id: z2.string(),
284
+ url: z2.string(),
285
+ alt: z2.string().catch(""),
286
+ caption: z2.string().nullable().catch(null),
287
+ role: z2.enum(["hero", "inline"]).catch("inline"),
288
+ width: z2.number().int(),
289
+ height: z2.number().int(),
290
+ mime: z2.string().catch("")
291
+ });
292
+ var blogSeoSchema = z2.object({
293
+ meta_title: z2.string().catch(""),
294
+ meta_description: z2.string().catch(""),
295
+ canonical_path: z2.string().catch(""),
296
+ og_image: z2.string().nullable().catch(null)
297
+ });
298
+ var blogPostContentWireSchema = z2.object({
299
+ schema_version: schemaVersionSchema,
300
+ slug: z2.string(),
301
+ title: z2.string(),
302
+ description: z2.string().catch(""),
303
+ mdx: z2.string(),
304
+ images: z2.array(blogImageSchema).catch([]),
305
+ // Already schema-validated at publish time, but re-sanitized on read (and
306
+ // again before rendering) — the override schema itself drops invalid values.
307
+ // Nullish: the backend serializes an unset override as an explicit `null`
308
+ // (Pydantic `model_dump()`); it folds into `undefined` for TS consumers.
309
+ theme_overrides: nullishField(blogThemeOverrideSchema).catch(void 0),
310
+ seo: blogSeoSchema.catch({
311
+ meta_title: "",
312
+ meta_description: "",
313
+ canonical_path: "",
314
+ og_image: null
315
+ }),
316
+ jsonld: z2.record(z2.unknown()).catch({}),
317
+ published_at: z2.string(),
318
+ // The applied_at of the version this content was published from; `null`
319
+ // when the backend has no separate modified date. Nullish on the wire
320
+ // (Pydantic `model_dump()` emits explicit `null`s), folded to `null` for a
321
+ // single TS sentinel; a malformed value degrades to `null` instead of
322
+ // dropping the post.
323
+ updated_at: z2.string().nullish().transform((v) => v ?? null).catch(null),
324
+ version_number: z2.number().int().catch(0),
325
+ content_hash: z2.string()
326
+ });
327
+ var blogPostContentSchema = blogPostContentWireSchema.extend({
328
+ // A single malformed image record is dropped; the post survives.
329
+ images: z2.array(blogImageSchema.nullable().catch(null)).transform((images) => images.filter((i) => i !== null)).catch([])
330
+ });
331
+ function parseBlogManifest(raw) {
332
+ const parsed = blogManifestSchema.safeParse(raw);
333
+ return parsed.success ? parsed.data : null;
334
+ }
335
+ function parseBlogPostContent(raw) {
336
+ const parsed = blogPostContentSchema.safeParse(raw);
337
+ return parsed.success ? parsed.data : null;
338
+ }
339
+
340
+ // ../tracking-core/src/resources/blog/client.ts
341
+ function createBlogClient(config) {
342
+ const base = config.cdnBaseUrl.replace(/\/+$/, "");
343
+ const doFetch = (...args) => (config.fetchImpl ?? fetch)(...args);
344
+ async function fetchJson(url, init) {
345
+ try {
346
+ const response = await doFetch(url, init);
347
+ if (!response.ok) return null;
348
+ return await response.json();
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+ async function getManifest() {
354
+ const init = {
355
+ next: { revalidate: config.manifestRevalidateSeconds ?? 300 }
356
+ };
357
+ const raw = await fetchJson(`${base}/blogs/v1/${config.namespace}/manifest.json`, init);
358
+ return raw === null ? null : parseBlogManifest(raw);
359
+ }
360
+ async function listPosts() {
361
+ const manifest = await getManifest();
362
+ if (!manifest) return [];
363
+ return [...manifest.posts].sort((a, b) => b.published_at.localeCompare(a.published_at));
364
+ }
365
+ async function getPost(slug) {
366
+ const manifest = await getManifest();
367
+ const entry = manifest?.posts.find((post) => post.slug === slug);
368
+ if (!entry) return null;
369
+ const init = { cache: "force-cache" };
370
+ const raw = await fetchJson(`${base}/${entry.content_key.replace(/^\/+/, "")}`, init);
371
+ return raw === null ? null : parseBlogPostContent(raw);
372
+ }
373
+ async function getStaticParams() {
374
+ const posts = await listPosts();
375
+ return posts.map((post) => ({ slug: post.slug }));
376
+ }
377
+ return { getManifest, listPosts, getPost, getStaticParams };
378
+ }
379
+
380
+ // ../tracking-core/src/resources/blog/presets.ts
381
+ var BLOG_THEME_PRESETS = {
382
+ /** Serif display headings over a humanist sans body — long-form magazine feel. */
383
+ editorial: {
384
+ typography: {
385
+ heading_family: "'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif",
386
+ body_family: "'Source Sans 3', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
387
+ heading_weight: 600,
388
+ body_weight: 400,
389
+ h1_size: { base: "2rem", md: "2.5rem" },
390
+ h2_size: { base: "1.5rem", md: "1.875rem" },
391
+ h3_size: "1.3rem",
392
+ h4_size: "1.125rem",
393
+ body_size: "1.0625rem",
394
+ body_line_height: "1.75",
395
+ heading_letter_spacing: "-0.015em"
396
+ },
397
+ spacing: {
398
+ block_gap: "1.35rem",
399
+ section_gap: "3.25rem"
400
+ },
401
+ radius: "6px"
402
+ },
403
+ /** Geometric sans pairing (Inter-first) — tight, contemporary product voice. */
404
+ modern: {
405
+ typography: {
406
+ heading_family: "'Inter', 'SF Pro Display', 'Segoe UI', Roboto, sans-serif",
407
+ body_family: "'Inter', 'SF Pro Text', 'Segoe UI', Roboto, sans-serif",
408
+ heading_weight: 700,
409
+ body_weight: 400,
410
+ h1_size: { base: "2.1rem", md: "2.75rem" },
411
+ h2_size: { base: "1.5rem", md: "1.75rem" },
412
+ h3_size: "1.25rem",
413
+ h4_size: "1.125rem",
414
+ body_size: "1rem",
415
+ body_line_height: "1.7",
416
+ heading_letter_spacing: "-0.03em"
417
+ },
418
+ spacing: {
419
+ block_gap: "1.25rem",
420
+ section_gap: "3rem"
421
+ },
422
+ radius: "12px"
423
+ },
424
+ /** Rounded-friendly pairing with a softer scale — approachable, human tone. */
425
+ warm: {
426
+ typography: {
427
+ heading_family: "'Nunito', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif",
428
+ body_family: "'Nunito Sans', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif",
429
+ heading_weight: 700,
430
+ body_weight: 400,
431
+ h1_size: { base: "1.9rem", md: "2.35rem" },
432
+ h2_size: { base: "1.45rem", md: "1.7rem" },
433
+ h3_size: "1.25rem",
434
+ h4_size: "1.125rem",
435
+ body_size: "1.0625rem",
436
+ body_line_height: "1.75",
437
+ heading_letter_spacing: "-0.01em"
438
+ },
439
+ spacing: {
440
+ block_gap: "1.35rem",
441
+ section_gap: "3.25rem"
442
+ },
443
+ radius: "16px"
444
+ },
445
+ /** Workmanlike sans with a prominent code surface — docs/engineering feel. */
446
+ technical: {
447
+ colors: {
448
+ // The one preset that ships a color: a cool code surface is part of the
449
+ // "technical" identity. Everything else stays on the site palette.
450
+ code_background: "#eef1f5"
451
+ },
452
+ colors_dark: {
453
+ code_background: "#1b1f27"
454
+ },
455
+ typography: {
456
+ heading_family: "'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
457
+ body_family: "'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
458
+ heading_weight: 600,
459
+ body_weight: 400,
460
+ h1_size: { base: "1.85rem", md: "2.25rem" },
461
+ h2_size: { base: "1.45rem", md: "1.65rem" },
462
+ h3_size: "1.2rem",
463
+ h4_size: "1.0625rem",
464
+ body_size: "1rem",
465
+ body_line_height: "1.65",
466
+ heading_letter_spacing: "-0.01em"
467
+ },
468
+ spacing: {
469
+ block_gap: "1.15rem",
470
+ section_gap: "2.75rem"
471
+ },
472
+ radius: "6px"
473
+ }
474
+ };
475
+ function prependFamily(front, stack) {
476
+ if (!front) return stack;
477
+ return stack ? `${front}, ${stack}` : front;
478
+ }
479
+ function createBlogFontTheme(preset, fonts) {
480
+ const heading_family = prependFamily(fonts.heading, preset.typography?.heading_family);
481
+ const body_family = prependFamily(fonts.body, preset.typography?.body_family);
482
+ return {
483
+ ...preset,
484
+ typography: {
485
+ ...preset.typography,
486
+ ...heading_family !== void 0 ? { heading_family } : {},
487
+ ...body_family !== void 0 ? { body_family } : {}
488
+ }
489
+ };
490
+ }
491
+
492
+ // ../tracking-core/src/resources/blog/view/class-names.ts
493
+ function joinClassNames(base, extra) {
494
+ return extra ? `${base} ${extra}` : base;
495
+ }
496
+
497
+ // ../tracking-core/src/resources/blog/seo.ts
498
+ function joinUrl(base, ...paths) {
499
+ let out = base.replace(/\/+$/, "");
500
+ for (const path of paths) {
501
+ const segment = path?.replace(/^\/+/, "").replace(/\/+$/, "");
502
+ if (segment) out += `/${segment}`;
503
+ }
504
+ return out;
505
+ }
506
+ function canonicalUrl(post, opts) {
507
+ return joinUrl(opts.siteUrl, opts.basePath ?? "/blog", post.slug);
508
+ }
509
+ function ogImages(post) {
510
+ if (post.seo.og_image) return [{ url: post.seo.og_image }];
511
+ const hero = post.images.find((image) => image.role === "hero");
512
+ if (!hero) return void 0;
513
+ return [{ url: hero.url, width: hero.width, height: hero.height, alt: hero.alt }];
514
+ }
515
+ function buildBlogPostMetadata(post, opts) {
516
+ const url = canonicalUrl(post, opts);
517
+ const title = post.seo.meta_title || post.title;
518
+ const description = post.seo.meta_description || post.description;
519
+ const images = ogImages(post);
520
+ return {
521
+ title,
522
+ description,
523
+ alternates: { canonical: url },
524
+ openGraph: {
525
+ title,
526
+ description,
527
+ type: "article",
528
+ url,
529
+ ...images ? { images } : {},
530
+ ...post.published_at ? { publishedTime: post.published_at } : {}
531
+ }
532
+ };
533
+ }
534
+ function buildBlogPostingJsonLd(post, opts) {
535
+ const url = canonicalUrl(post, opts);
536
+ const images = post.images.map((image) => image.url);
537
+ const doc = {
538
+ headline: post.title,
539
+ description: post.seo.meta_description || post.description,
540
+ datePublished: post.published_at,
541
+ dateModified: post.updated_at ?? post.published_at,
542
+ ...images.length > 0 ? { image: images } : {},
543
+ ...opts.publisherName ? { publisher: { "@type": "Organization", name: opts.publisherName } } : {}
544
+ };
545
+ const backend = post.jsonld;
546
+ const merged = backend && typeof backend === "object" && Object.keys(backend).length > 0 ? { ...doc, ...backend } : doc;
547
+ return {
548
+ ...merged,
549
+ "@context": "https://schema.org",
550
+ "@type": "BlogPosting",
551
+ url,
552
+ mainEntityOfPage: { "@type": "WebPage", "@id": url }
553
+ };
554
+ }
555
+ var FAQ_ITEM_RE = /<FaqItem\s+question=(?:"([^"]*)"|'([^']*)')\s*>([\s\S]*?)<\/FaqItem>/g;
556
+ var CODE_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^ {0,3}\1`* *$|(?![\s\S]))/gm;
557
+ var ANSWER_TAG_RE = /<[^>]*>/g;
558
+ function extractFaqPairs(mdx) {
559
+ const withoutCode = mdx.replaceAll(CODE_FENCE_RE, "");
560
+ const pairs = [];
561
+ for (const match of withoutCode.matchAll(FAQ_ITEM_RE)) {
562
+ const question = (match[1] ?? match[2] ?? "").trim();
563
+ const answer = match[3].replaceAll(ANSWER_TAG_RE, " ").replaceAll(/\s+/g, " ").trim();
564
+ if (question && answer) pairs.push({ question, answer });
565
+ }
566
+ return pairs;
567
+ }
568
+ function buildFaqJsonLd(pairs) {
569
+ if (pairs.length === 0) return null;
570
+ return {
571
+ "@context": "https://schema.org",
572
+ "@type": "FAQPage",
573
+ mainEntity: pairs.map((pair) => ({
574
+ "@type": "Question",
575
+ name: pair.question,
576
+ acceptedAnswer: { "@type": "Answer", text: pair.answer }
577
+ }))
578
+ };
579
+ }
580
+ var JSONLD_ESCAPES = {
581
+ "<": "\\u003c",
582
+ ">": "\\u003e",
583
+ "&": "\\u0026",
584
+ "\u2028": "\\u2028",
585
+ "\u2029": "\\u2029"
586
+ };
587
+ function serializeJsonLd(data) {
588
+ return JSON.stringify(data).replaceAll(/[<>&\u2028\u2029]/g, (ch) => JSONLD_ESCAPES[ch] ?? ch);
589
+ }
590
+ function buildBlogSitemapEntries(posts, opts) {
591
+ return posts.map((post) => ({
592
+ url: joinUrl(opts.siteUrl, opts.basePath ?? "/blog", post.slug),
593
+ lastModified: post.updated_at
594
+ }));
595
+ }
596
+
597
+ // ../tracking-core/src/resources/blog/toc.ts
598
+ import GithubSlugger from "github-slugger";
599
+ import remarkMdx from "remark-mdx";
600
+ import remarkParse from "remark-parse";
601
+ import { unified } from "unified";
602
+ var TEXT_LEAF_TYPES = /* @__PURE__ */ new Set(["text", "inlineCode"]);
603
+ function headingText(node) {
604
+ let text = "";
605
+ const stack = [];
606
+ const roots = node.children ?? [];
607
+ for (let i = roots.length - 1; i >= 0; i--) {
608
+ stack.push(roots[i]);
609
+ }
610
+ while (stack.length > 0) {
611
+ const current = stack.pop();
612
+ if (!current) break;
613
+ if (TEXT_LEAF_TYPES.has(current.type) && typeof current.value === "string") {
614
+ text += current.value;
615
+ }
616
+ if (Array.isArray(current.children)) {
617
+ for (let i = current.children.length - 1; i >= 0; i--) {
618
+ stack.push(current.children[i]);
619
+ }
620
+ }
621
+ }
622
+ return text;
623
+ }
624
+ function extractBlogHeadings(mdx) {
625
+ let tree;
626
+ try {
627
+ tree = unified().use(remarkParse).use(remarkMdx).parse(mdx);
628
+ } catch {
629
+ return [];
630
+ }
631
+ const slugger = new GithubSlugger();
632
+ const headings = [];
633
+ const stack = [tree];
634
+ while (stack.length > 0) {
635
+ const node = stack.pop();
636
+ if (!node) break;
637
+ if (node.type === "heading" && typeof node.depth === "number") {
638
+ const text = headingText(node);
639
+ const id = slugger.slug(text);
640
+ if (node.depth === 2 || node.depth === 3) {
641
+ headings.push({ depth: node.depth, text, id });
642
+ }
643
+ }
644
+ if (Array.isArray(node.children)) {
645
+ for (let i = node.children.length - 1; i >= 0; i--) {
646
+ stack.push(node.children[i]);
647
+ }
648
+ }
649
+ }
650
+ return headings;
651
+ }
652
+ var WORDS_PER_MINUTE = 220;
653
+ var MDX_SYNTAX_RE = /<[^>]*>|```[^`]*```|`|^[#>\-*+\s]+|\]\([^)]*\)|[[\]!]/gm;
654
+ function estimateReadingTime(mdx) {
655
+ const words = mdx.replaceAll(MDX_SYNTAX_RE, " ").split(/\s+/).filter((w) => w.length > 0).length;
656
+ return Math.max(1, Math.round(words / WORDS_PER_MINUTE));
657
+ }
658
+
659
+ // ../tracking-core/src/resources/blog/validate-mdx.ts
660
+ import remarkMdx2 from "remark-mdx";
661
+ import remarkParse2 from "remark-parse";
662
+ import { unified as unified2 } from "unified";
663
+ var BLOG_MDX_MAX_SIZE = 5e5;
664
+ var BLOG_MDX_MAX_DEPTH = 64;
665
+ var BLOG_MDX_MAX_NODES = 1e5;
666
+ var MAX_VIOLATIONS = 50;
667
+ var BLOG_MDX_COMPONENT_GRAMMAR = [
668
+ {
669
+ name: "Callout",
670
+ attrs: [],
671
+ urlAttrs: [],
672
+ selfClosing: false,
673
+ cssClass: "aranova-blog-callout",
674
+ promptUsage: "<Callout>...</Callout>",
675
+ promptDescription: "a highlighted tip/note block; no attributes."
676
+ },
677
+ {
678
+ name: "Cta",
679
+ attrs: ["href", "label"],
680
+ urlAttrs: ["href"],
681
+ selfClosing: true,
682
+ cssClass: "aranova-blog-cta",
683
+ promptUsage: '<Cta href="..." label="..." />',
684
+ promptDescription: "a call-to-action button (self-closing)."
685
+ },
686
+ {
687
+ name: "BlogImage",
688
+ attrs: ["id"],
689
+ urlAttrs: [],
690
+ selfClosing: true,
691
+ promptUsage: '<BlogImage id="..." />',
692
+ promptDescription: "a generated image placeholder (self-closing). The `id` references an image brief id from the plan; the renderer resolves it."
693
+ },
694
+ {
695
+ name: "FaqItem",
696
+ attrs: ["question"],
697
+ urlAttrs: [],
698
+ selfClosing: false,
699
+ cssClass: "aranova-blog-faq-item",
700
+ promptUsage: '<FaqItem question="...">...</FaqItem>',
701
+ promptDescription: "one FAQ entry: the required `question` attribute carries the question, the children are the prose answer. Group consecutive `<FaqItem>`s under an FAQ heading near the end of the post when the topic suits a Q&A section."
702
+ },
703
+ {
704
+ name: "KeyTakeaways",
705
+ attrs: [],
706
+ urlAttrs: [],
707
+ selfClosing: false,
708
+ cssClass: "aranova-blog-key-takeaways",
709
+ promptUsage: "<KeyTakeaways>...</KeyTakeaways>",
710
+ promptDescription: "a TL;DR box containing a standard markdown bullet list of 3-5 takeaways; place it right after the H1 (and hero image)."
711
+ }
712
+ ];
713
+ var BLOG_MDX_ALLOWED_COMPONENTS = BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => entry.name);
714
+ var ALLOWED_NODE_TYPES = /* @__PURE__ */ new Set([
715
+ "root",
716
+ "heading",
717
+ "paragraph",
718
+ "text",
719
+ "emphasis",
720
+ "strong",
721
+ "delete",
722
+ "link",
723
+ "image",
724
+ "linkReference",
725
+ "imageReference",
726
+ "definition",
727
+ "list",
728
+ "listItem",
729
+ "blockquote",
730
+ "code",
731
+ "inlineCode",
732
+ "table",
733
+ "tableRow",
734
+ "tableCell",
735
+ "thematicBreak",
736
+ "break"
737
+ ]);
738
+ var FORBIDDEN_NODE_TYPES = {
739
+ html: { rule: "raw-html", message: "Raw HTML is not allowed" },
740
+ mdxjsEsm: { rule: "esm", message: "import/export statements are not allowed" },
741
+ mdxFlowExpression: { rule: "expression", message: "JSX expressions ({\u2026}) are not allowed" },
742
+ mdxTextExpression: { rule: "expression", message: "JSX expressions ({\u2026}) are not allowed" }
743
+ };
744
+ var COMPONENT_SPECS = Object.fromEntries(
745
+ BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => [
746
+ entry.name,
747
+ { attrs: entry.attrs, urlAttrs: entry.urlAttrs, allowChildren: !entry.selfClosing }
748
+ ])
749
+ );
750
+ var URL_SCHEME_RE = /^([A-Za-z][A-Za-z0-9+.-]*):/;
751
+ var ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto"]);
752
+ function forbiddenUrlScheme(rawUrl) {
753
+ const cleaned = rawUrl.replaceAll(/[\u0000-\u0020\u007F]/g, "");
754
+ const match = URL_SCHEME_RE.exec(cleaned);
755
+ if (!match) return null;
756
+ const scheme = match[1].toLowerCase();
757
+ return ALLOWED_URL_SCHEMES.has(scheme) ? null : scheme;
758
+ }
759
+ function nodeLine(node) {
760
+ return node.position?.start?.line;
761
+ }
762
+ function checkComponent(node, violations) {
763
+ const line = nodeLine(node);
764
+ const name = node.name;
765
+ if (!name) {
766
+ violations.push({
767
+ rule: "unknown-component",
768
+ message: "JSX fragments (<>\u2026</>) are not allowed",
769
+ line
770
+ });
771
+ return;
772
+ }
773
+ const spec = COMPONENT_SPECS[name];
774
+ if (!spec) {
775
+ violations.push({
776
+ rule: "unknown-component",
777
+ message: `<${name}> is not an allowed element (allowed: ${BLOG_MDX_ALLOWED_COMPONENTS.join(", ")})`,
778
+ line
779
+ });
780
+ return;
781
+ }
782
+ const seen = /* @__PURE__ */ new Set();
783
+ for (const attr of node.attributes ?? []) {
784
+ if (attr.type !== "mdxJsxAttribute" || typeof attr.name !== "string") {
785
+ violations.push({
786
+ rule: "component-attribute",
787
+ message: `<${name}> has a non-literal attribute`,
788
+ line
789
+ });
790
+ continue;
791
+ }
792
+ if (!spec.attrs.includes(attr.name)) {
793
+ violations.push({
794
+ rule: "component-attribute",
795
+ message: `<${name}> does not accept attribute "${attr.name}"`,
796
+ line
797
+ });
798
+ continue;
799
+ }
800
+ if (typeof attr.value !== "string") {
801
+ violations.push({
802
+ rule: "component-attribute",
803
+ message: `<${name} ${attr.name}> must be a quoted string literal`,
804
+ line
805
+ });
806
+ continue;
807
+ }
808
+ seen.add(attr.name);
809
+ if (spec.urlAttrs.includes(attr.name)) {
810
+ const scheme = forbiddenUrlScheme(attr.value);
811
+ if (scheme !== null) {
812
+ violations.push({
813
+ rule: "url-scheme",
814
+ message: `<${name} ${attr.name}> uses forbidden URL scheme "${scheme}:"`,
815
+ line
816
+ });
817
+ }
818
+ }
819
+ }
820
+ for (const required of spec.attrs) {
821
+ if (!seen.has(required)) {
822
+ violations.push({
823
+ rule: "component-attribute",
824
+ message: `<${name}> requires attribute "${required}"`,
825
+ line
826
+ });
827
+ }
828
+ }
829
+ if (!spec.allowChildren && (node.children?.length ?? 0) > 0) {
830
+ violations.push({
831
+ rule: "component-attribute",
832
+ message: `<${name}> must be self-closing (no children)`,
833
+ line
834
+ });
835
+ }
836
+ }
837
+ function validateBlogMdx(mdx, options) {
838
+ const violations = [];
839
+ if (typeof mdx !== "string" || mdx.length > BLOG_MDX_MAX_SIZE) {
840
+ return {
841
+ ok: false,
842
+ violations: [
843
+ {
844
+ rule: "size-limit",
845
+ message: `Document exceeds the ${BLOG_MDX_MAX_SIZE}-character limit`
846
+ }
847
+ ]
848
+ };
849
+ }
850
+ let tree;
851
+ try {
852
+ tree = unified2().use(remarkParse2).use(remarkMdx2).parse(mdx);
853
+ } catch (error) {
854
+ const line = error.line;
855
+ return {
856
+ ok: false,
857
+ violations: [
858
+ {
859
+ rule: "parse-error",
860
+ message: "Document is not valid MDX",
861
+ ...typeof line === "number" ? { line } : {}
862
+ }
863
+ ]
864
+ };
865
+ }
866
+ if (options?.requireLeadingH1) {
867
+ const firstChild = tree.children?.[0];
868
+ if (!firstChild || firstChild.type !== "heading" || firstChild.depth !== 1) {
869
+ violations.push({
870
+ rule: "leading-h1",
871
+ message: "Document must start with the H1 title (`# ...`) \u2014 no frontmatter or metadata block before it",
872
+ line: nodeLine(firstChild ?? tree)
873
+ });
874
+ }
875
+ }
876
+ let nodeCount = 0;
877
+ const stack = [{ node: tree, depth: 0 }];
878
+ while (stack.length > 0 && violations.length < MAX_VIOLATIONS) {
879
+ const item = stack.pop();
880
+ if (!item) break;
881
+ const { node, depth } = item;
882
+ nodeCount += 1;
883
+ if (nodeCount > BLOG_MDX_MAX_NODES) {
884
+ violations.push({ rule: "size-limit", message: "Document has too many nodes" });
885
+ break;
886
+ }
887
+ if (depth > BLOG_MDX_MAX_DEPTH) {
888
+ violations.push({ rule: "depth-limit", message: "Document nests too deeply" });
889
+ break;
890
+ }
891
+ const forbidden = FORBIDDEN_NODE_TYPES[node.type];
892
+ if (forbidden) {
893
+ violations.push({ ...forbidden, line: nodeLine(node) });
894
+ continue;
895
+ }
896
+ if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
897
+ checkComponent(node, violations);
898
+ } else if (!ALLOWED_NODE_TYPES.has(node.type)) {
899
+ violations.push({
900
+ rule: "disallowed-node",
901
+ message: `Markdown construct "${node.type}" is not allowed`,
902
+ line: nodeLine(node)
903
+ });
904
+ continue;
905
+ }
906
+ if ((node.type === "link" || node.type === "image" || node.type === "definition") && typeof node.url === "string") {
907
+ const scheme = forbiddenUrlScheme(node.url);
908
+ if (scheme !== null) {
909
+ violations.push({
910
+ rule: "url-scheme",
911
+ message: `${node.type} uses forbidden URL scheme "${scheme}:"`,
912
+ line: nodeLine(node)
913
+ });
914
+ }
915
+ }
916
+ if (Array.isArray(node.children)) {
917
+ for (const child of node.children) {
918
+ stack.push({ node: child, depth: depth + 1 });
919
+ }
920
+ }
921
+ }
922
+ return violations.length === 0 ? { ok: true } : { ok: false, violations };
923
+ }
924
+
925
+ // ../tracking-core/src/resources/blog/view/blog-post-view.tsx
926
+ import { compileMDX } from "next-mdx-remote/rsc";
927
+ import rehypeSlug from "rehype-slug";
928
+
929
+ // ../tracking-core/src/resources/blog/view/components.tsx
930
+ import { jsx, jsxs } from "react/jsx-runtime";
931
+ function createDefaultComponents(images, classNames) {
932
+ const imagesById = new Map(images.map((image) => [image.id, image]));
933
+ return {
934
+ // `children` is destructured explicitly so static a11y analysis
935
+ // (heading-has-content, anchor-has-content) can see the content flow.
936
+ h1: ({ children, ...props }) => /* @__PURE__ */ jsx("h1", { ...props, children }),
937
+ h2: ({ children, ...props }) => /* @__PURE__ */ jsx("h2", { ...props, children }),
938
+ h3: ({ children, ...props }) => /* @__PURE__ */ jsx("h3", { ...props, children }),
939
+ h4: ({ children, ...props }) => /* @__PURE__ */ jsx("h4", { ...props, children }),
940
+ h5: ({ children, ...props }) => /* @__PURE__ */ jsx("h5", { ...props, children }),
941
+ h6: ({ children, ...props }) => /* @__PURE__ */ jsx("h6", { ...props, children }),
942
+ p: ({ children, ...props }) => /* @__PURE__ */ jsx("p", { ...props, children }),
943
+ a: ({ children, ...props }) => /* @__PURE__ */ jsx("a", { ...props, children }),
944
+ img: ({ alt = "", ...props }) => /* @__PURE__ */ jsx("img", { loading: "lazy", alt, ...props }),
945
+ pre: ({ children, ...props }) => /* @__PURE__ */ jsx("pre", { ...props, children }),
946
+ code: ({ children, ...props }) => /* @__PURE__ */ jsx("code", { ...props, children }),
947
+ blockquote: ({ children, ...props }) => /* @__PURE__ */ jsx("blockquote", { ...props, children }),
948
+ ul: ({ children, ...props }) => /* @__PURE__ */ jsx("ul", { ...props, children }),
949
+ ol: ({ children, ...props }) => /* @__PURE__ */ jsx("ol", { ...props, children }),
950
+ li: ({ children, ...props }) => /* @__PURE__ */ jsx("li", { ...props, children }),
951
+ // Wrapped so wide tables scroll horizontally inside the article instead of
952
+ // overflowing the host page (the wrapper also carries border + radius).
953
+ table: ({ children, ...props }) => /* @__PURE__ */ jsx("div", { className: joinClassNames("aranova-blog-table-wrap", classNames?.tableWrap), children: /* @__PURE__ */ jsx("table", { ...props, children }) }),
954
+ hr: (props) => /* @__PURE__ */ jsx("hr", { ...props }),
955
+ Callout: ({ children }) => /* @__PURE__ */ jsx("aside", { className: joinClassNames("aranova-blog-callout", classNames?.callout), children }),
956
+ Cta: ({ href, label }) => (
957
+ // `data-aranova-cta` opts this into the tracking SDK's cta_click
958
+ // auto-capture (default selector `[data-aranova-cta]`) with the label as
959
+ // the cta_name — zero site config. Inert if auto-capture is off.
960
+ /* @__PURE__ */ jsx(
961
+ "a",
962
+ {
963
+ className: joinClassNames("aranova-blog-cta", classNames?.cta),
964
+ href,
965
+ "data-aranova-cta": label,
966
+ "data-aranova-section": "blog",
967
+ children: label
968
+ }
969
+ )
970
+ ),
971
+ BlogImage: ({ id }) => {
972
+ const image = imagesById.get(id);
973
+ if (!image) return null;
974
+ return /* @__PURE__ */ jsxs("figure", { className: classNames?.figure, children: [
975
+ /* @__PURE__ */ jsx(
976
+ "img",
977
+ {
978
+ src: image.url,
979
+ alt: image.alt,
980
+ width: image.width,
981
+ height: image.height,
982
+ loading: "lazy"
983
+ }
984
+ ),
985
+ image.caption ? /* @__PURE__ */ jsx("figcaption", { children: image.caption }) : null
986
+ ] });
987
+ },
988
+ // Native disclosure accordion — no JS, server-renderable. The h3 inside
989
+ // <summary> is valid HTML and keeps the document outline / a11y heading.
990
+ FaqItem: ({ question, children }) => /* @__PURE__ */ jsxs("details", { className: joinClassNames("aranova-blog-faq-item", classNames?.faqItem), children: [
991
+ /* @__PURE__ */ jsx("summary", { className: joinClassNames("aranova-blog-faq-question", classNames?.faqQuestion), children: /* @__PURE__ */ jsx("h3", { children: question }) }),
992
+ /* @__PURE__ */ jsx("div", { className: joinClassNames("aranova-blog-faq-answer", classNames?.faqAnswer), children })
993
+ ] }),
994
+ KeyTakeaways: ({ children }) => /* @__PURE__ */ jsx("aside", { className: joinClassNames("aranova-blog-key-takeaways", classNames?.keyTakeaways), children })
995
+ };
996
+ }
997
+
998
+ // ../tracking-core/src/resources/blog/view/styles.ts
999
+ var BLOG_BASE_CSS = `/* Aranova blog post base stylesheet \u2014 authored source of truth.
1000
+ *
1001
+ * Scoped under \`.aranova-blog-post\` and driven entirely by the sanitized
1002
+ * \`--ara-blog-*\` CSS custom properties emitted by \`blogThemeToCssVars\` on the
1003
+ * article element. Host sites usually run Tailwind preflight, so:
1004
+ *
1005
+ * - element rules use \`.aranova-blog-post :where(el)\` \u2014 one class of
1006
+ * specificity (0,1,0), enough to beat preflight's bare element resets
1007
+ * while every rule in this sheet stays flat and order-resolvable;
1008
+ * - component classes (\`.aranova-blog-callout\`, \u2026) keep their natural
1009
+ * class-on-class specificity.
1010
+ *
1011
+ * Responsive-capable tokens (h1/h2/h3/h4/body/small sizes, body line-height,
1012
+ * content max-width, block gap, section gap, radius) are NEVER consumed via their raw
1013
+ * \`--ara-blog-*\` vars here \u2014 the generated block below re-points internal
1014
+ * \`--_*\` vars per breakpoint, and all rules consume those.
1015
+ *
1016
+ * Rhythm rules near the end target BOTH \`.aranova-blog-post\` (content is a
1017
+ * direct child of the article today) and \`.aranova-blog-content\` (a later
1018
+ * phase wraps content in that div) so that phase needs no CSS changes.
1019
+ *
1020
+ * No raw colors anywhere: every color flows through the theme vars, which
1021
+ * already carry \`light-dark(...)\` values when a dark palette is present.
1022
+ */
1023
+
1024
+ /* Responsive token plumbing \u2014 GENERATED by build-styles.ts from theme.ts
1025
+ (BLOG_BREAKPOINT_MIN_WIDTHS_PX + the responsive() token set). Mobile-first:
1026
+ each breakpoint falls back through every narrower key down to the base var.
1027
+ Authored rules consume ONLY the --_* internal vars for these tokens. */
1028
+ .aranova-blog-post {
1029
+ --_h1-size: var(--ara-blog-type-h1-size);
1030
+ --_h2-size: var(--ara-blog-type-h2-size);
1031
+ --_h3-size: var(--ara-blog-type-h3-size);
1032
+ --_h4-size: var(--ara-blog-type-h4-size);
1033
+ --_body-size: var(--ara-blog-type-body-size);
1034
+ --_body-line-height: var(--ara-blog-type-body-line-height);
1035
+ --_small-size: var(--ara-blog-type-small-size);
1036
+ --_content-max-width: var(--ara-blog-space-content-max-width);
1037
+ --_block-gap: var(--ara-blog-space-block-gap);
1038
+ --_section-gap: var(--ara-blog-space-section-gap);
1039
+ --_radius: var(--ara-blog-radius);
1040
+ }
1041
+ @media (min-width: 640px) {
1042
+ .aranova-blog-post {
1043
+ --_h1-size: var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size));
1044
+ --_h2-size: var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size));
1045
+ --_h3-size: var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size));
1046
+ --_h4-size: var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size));
1047
+ --_body-size: var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size));
1048
+ --_body-line-height: var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height));
1049
+ --_small-size: var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size));
1050
+ --_content-max-width: var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width));
1051
+ --_block-gap: var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap));
1052
+ --_section-gap: var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap));
1053
+ --_radius: var(--ara-blog-radius-sm, var(--ara-blog-radius));
1054
+ }
1055
+ }
1056
+ @media (min-width: 768px) {
1057
+ .aranova-blog-post {
1058
+ --_h1-size: var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size)));
1059
+ --_h2-size: var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size)));
1060
+ --_h3-size: var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size)));
1061
+ --_h4-size: var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size)));
1062
+ --_body-size: var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size)));
1063
+ --_body-line-height: var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height)));
1064
+ --_small-size: var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size)));
1065
+ --_content-max-width: var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width)));
1066
+ --_block-gap: var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap)));
1067
+ --_section-gap: var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap)));
1068
+ --_radius: var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius)));
1069
+ }
1070
+ }
1071
+ @media (min-width: 1024px) {
1072
+ .aranova-blog-post {
1073
+ --_h1-size: var(--ara-blog-type-h1-size-lg, var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size))));
1074
+ --_h2-size: var(--ara-blog-type-h2-size-lg, var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size))));
1075
+ --_h3-size: var(--ara-blog-type-h3-size-lg, var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size))));
1076
+ --_h4-size: var(--ara-blog-type-h4-size-lg, var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size))));
1077
+ --_body-size: var(--ara-blog-type-body-size-lg, var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size))));
1078
+ --_body-line-height: var(--ara-blog-type-body-line-height-lg, var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height))));
1079
+ --_small-size: var(--ara-blog-type-small-size-lg, var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size))));
1080
+ --_content-max-width: var(--ara-blog-space-content-max-width-lg, var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width))));
1081
+ --_block-gap: var(--ara-blog-space-block-gap-lg, var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap))));
1082
+ --_section-gap: var(--ara-blog-space-section-gap-lg, var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap))));
1083
+ --_radius: var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius))));
1084
+ }
1085
+ }
1086
+ @media (min-width: 1280px) {
1087
+ .aranova-blog-post {
1088
+ --_h1-size: var(--ara-blog-type-h1-size-xl, var(--ara-blog-type-h1-size-lg, var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size)))));
1089
+ --_h2-size: var(--ara-blog-type-h2-size-xl, var(--ara-blog-type-h2-size-lg, var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size)))));
1090
+ --_h3-size: var(--ara-blog-type-h3-size-xl, var(--ara-blog-type-h3-size-lg, var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size)))));
1091
+ --_h4-size: var(--ara-blog-type-h4-size-xl, var(--ara-blog-type-h4-size-lg, var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size)))));
1092
+ --_body-size: var(--ara-blog-type-body-size-xl, var(--ara-blog-type-body-size-lg, var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size)))));
1093
+ --_body-line-height: var(--ara-blog-type-body-line-height-xl, var(--ara-blog-type-body-line-height-lg, var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height)))));
1094
+ --_small-size: var(--ara-blog-type-small-size-xl, var(--ara-blog-type-small-size-lg, var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size)))));
1095
+ --_content-max-width: var(--ara-blog-space-content-max-width-xl, var(--ara-blog-space-content-max-width-lg, var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width)))));
1096
+ --_block-gap: var(--ara-blog-space-block-gap-xl, var(--ara-blog-space-block-gap-lg, var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap)))));
1097
+ --_section-gap: var(--ara-blog-space-section-gap-xl, var(--ara-blog-space-section-gap-lg, var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap)))));
1098
+ --_radius: var(--ara-blog-radius-xl, var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius)))));
1099
+ }
1100
+ }
1101
+ @media (min-width: 1536px) {
1102
+ .aranova-blog-post {
1103
+ --_h1-size: var(--ara-blog-type-h1-size-2xl, var(--ara-blog-type-h1-size-xl, var(--ara-blog-type-h1-size-lg, var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size))))));
1104
+ --_h2-size: var(--ara-blog-type-h2-size-2xl, var(--ara-blog-type-h2-size-xl, var(--ara-blog-type-h2-size-lg, var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size))))));
1105
+ --_h3-size: var(--ara-blog-type-h3-size-2xl, var(--ara-blog-type-h3-size-xl, var(--ara-blog-type-h3-size-lg, var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size))))));
1106
+ --_h4-size: var(--ara-blog-type-h4-size-2xl, var(--ara-blog-type-h4-size-xl, var(--ara-blog-type-h4-size-lg, var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size))))));
1107
+ --_body-size: var(--ara-blog-type-body-size-2xl, var(--ara-blog-type-body-size-xl, var(--ara-blog-type-body-size-lg, var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size))))));
1108
+ --_body-line-height: var(--ara-blog-type-body-line-height-2xl, var(--ara-blog-type-body-line-height-xl, var(--ara-blog-type-body-line-height-lg, var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height))))));
1109
+ --_small-size: var(--ara-blog-type-small-size-2xl, var(--ara-blog-type-small-size-xl, var(--ara-blog-type-small-size-lg, var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size))))));
1110
+ --_content-max-width: var(--ara-blog-space-content-max-width-2xl, var(--ara-blog-space-content-max-width-xl, var(--ara-blog-space-content-max-width-lg, var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width))))));
1111
+ --_block-gap: var(--ara-blog-space-block-gap-2xl, var(--ara-blog-space-block-gap-xl, var(--ara-blog-space-block-gap-lg, var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap))))));
1112
+ --_section-gap: var(--ara-blog-space-section-gap-2xl, var(--ara-blog-space-section-gap-xl, var(--ara-blog-space-section-gap-lg, var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap))))));
1113
+ --_radius: var(--ara-blog-radius-2xl, var(--ara-blog-radius-xl, var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius))))));
1114
+ }
1115
+ }
1116
+ @media (min-width: 1920px) {
1117
+ .aranova-blog-post {
1118
+ --_h1-size: var(--ara-blog-type-h1-size-3xl, var(--ara-blog-type-h1-size-2xl, var(--ara-blog-type-h1-size-xl, var(--ara-blog-type-h1-size-lg, var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size)))))));
1119
+ --_h2-size: var(--ara-blog-type-h2-size-3xl, var(--ara-blog-type-h2-size-2xl, var(--ara-blog-type-h2-size-xl, var(--ara-blog-type-h2-size-lg, var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size)))))));
1120
+ --_h3-size: var(--ara-blog-type-h3-size-3xl, var(--ara-blog-type-h3-size-2xl, var(--ara-blog-type-h3-size-xl, var(--ara-blog-type-h3-size-lg, var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size)))))));
1121
+ --_h4-size: var(--ara-blog-type-h4-size-3xl, var(--ara-blog-type-h4-size-2xl, var(--ara-blog-type-h4-size-xl, var(--ara-blog-type-h4-size-lg, var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size)))))));
1122
+ --_body-size: var(--ara-blog-type-body-size-3xl, var(--ara-blog-type-body-size-2xl, var(--ara-blog-type-body-size-xl, var(--ara-blog-type-body-size-lg, var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size)))))));
1123
+ --_body-line-height: var(--ara-blog-type-body-line-height-3xl, var(--ara-blog-type-body-line-height-2xl, var(--ara-blog-type-body-line-height-xl, var(--ara-blog-type-body-line-height-lg, var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height)))))));
1124
+ --_small-size: var(--ara-blog-type-small-size-3xl, var(--ara-blog-type-small-size-2xl, var(--ara-blog-type-small-size-xl, var(--ara-blog-type-small-size-lg, var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size)))))));
1125
+ --_content-max-width: var(--ara-blog-space-content-max-width-3xl, var(--ara-blog-space-content-max-width-2xl, var(--ara-blog-space-content-max-width-xl, var(--ara-blog-space-content-max-width-lg, var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width)))))));
1126
+ --_block-gap: var(--ara-blog-space-block-gap-3xl, var(--ara-blog-space-block-gap-2xl, var(--ara-blog-space-block-gap-xl, var(--ara-blog-space-block-gap-lg, var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap)))))));
1127
+ --_section-gap: var(--ara-blog-space-section-gap-3xl, var(--ara-blog-space-section-gap-2xl, var(--ara-blog-space-section-gap-xl, var(--ara-blog-space-section-gap-lg, var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap)))))));
1128
+ --_radius: var(--ara-blog-radius-3xl, var(--ara-blog-radius-2xl, var(--ara-blog-radius-xl, var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius)))))));
1129
+ }
1130
+ }
1131
+
1132
+ .aranova-blog-post {
1133
+ /* Internal spacing scale, derived from the responsive block gap. */
1134
+ --_flow: var(--_block-gap);
1135
+ --_flow-sm: calc(var(--_block-gap) * 0.5);
1136
+ --_flow-lg: calc(var(--_block-gap) * 1.75);
1137
+ --_flow-section: var(--_section-gap);
1138
+ /* Optional heading color token \u2014 themes that don't set it fall back to text. */
1139
+ --_heading-color: var(--ara-blog-color-heading, var(--ara-blog-color-text));
1140
+
1141
+ max-width: var(--_content-max-width);
1142
+ margin-inline: auto;
1143
+ background: var(--ara-blog-color-background);
1144
+ color: var(--ara-blog-color-text);
1145
+ font-family: var(--ara-blog-type-body-family);
1146
+ font-weight: var(--ara-blog-type-body-weight);
1147
+ font-size: var(--_body-size);
1148
+ line-height: var(--_body-line-height);
1149
+ overflow-wrap: break-word;
1150
+ }
1151
+ @media (max-width: 639.99px) {
1152
+ .aranova-blog-post {
1153
+ padding-inline: 1.25rem;
1154
+ }
1155
+ }
1156
+
1157
+ /* --- Headings ------------------------------------------------------------ */
1158
+ .aranova-blog-post :where(h1, h2, h3, h4, h5, h6) {
1159
+ font-family: var(--ara-blog-type-heading-family);
1160
+ font-weight: var(--ara-blog-type-heading-weight);
1161
+ line-height: 1.25;
1162
+ margin-block: 0;
1163
+ color: var(--_heading-color);
1164
+ scroll-margin-top: 5rem;
1165
+ }
1166
+ .aranova-blog-post :where(h1, h2, h3) {
1167
+ /* \`pretty\` fills each line greedily (only the LAST line is massaged to avoid
1168
+ * an orphan word) \u2014 so a heading uses the full measure. \`balance\` was leaving
1169
+ * short first lines. */
1170
+ text-wrap: pretty;
1171
+ }
1172
+ .aranova-blog-post :where(h1) {
1173
+ font-size: var(--_h1-size);
1174
+ letter-spacing: var(--ara-blog-type-heading-letter-spacing);
1175
+ }
1176
+ .aranova-blog-post :where(h2) {
1177
+ font-size: var(--_h2-size);
1178
+ letter-spacing: var(--ara-blog-type-heading-letter-spacing);
1179
+ }
1180
+ .aranova-blog-post :where(h3) {
1181
+ font-size: var(--_h3-size);
1182
+ letter-spacing: calc(var(--ara-blog-type-heading-letter-spacing) * 0.5);
1183
+ }
1184
+ .aranova-blog-post :where(h4) {
1185
+ font-size: var(--_h4-size);
1186
+ font-weight: 600;
1187
+ letter-spacing: calc(var(--ara-blog-type-heading-letter-spacing) * 0.5);
1188
+ }
1189
+ .aranova-blog-post :where(h5) {
1190
+ font-size: var(--_body-size);
1191
+ font-weight: 600;
1192
+ }
1193
+ .aranova-blog-post :where(h6) {
1194
+ font-size: var(--_small-size);
1195
+ font-weight: 600;
1196
+ text-transform: uppercase;
1197
+ letter-spacing: 0.05em;
1198
+ color: var(--ara-blog-color-text-muted);
1199
+ }
1200
+
1201
+ /* --- Text ---------------------------------------------------------------- */
1202
+ .aranova-blog-post :where(p) {
1203
+ margin-block: 0;
1204
+ }
1205
+ /* Lead paragraph: the one directly after the post title. */
1206
+ .aranova-blog-post :where(h1) + :where(p) {
1207
+ font-size: 1.15em;
1208
+ color: color-mix(in srgb, var(--ara-blog-color-text) 72%, var(--ara-blog-color-text-muted));
1209
+ }
1210
+ .aranova-blog-post :where(a) {
1211
+ color: var(--ara-blog-color-link);
1212
+ text-decoration: underline;
1213
+ text-underline-offset: 0.15em;
1214
+ }
1215
+ .aranova-blog-post :where(a):focus-visible {
1216
+ outline: 2px solid var(--ara-blog-color-link);
1217
+ outline-offset: 2px;
1218
+ border-radius: 2px;
1219
+ }
1220
+
1221
+ /* --- Lists ---------------------------------------------------------------- */
1222
+ .aranova-blog-post :where(ul, ol) {
1223
+ margin-block: 0;
1224
+ padding-inline-start: 1.5em;
1225
+ }
1226
+ .aranova-blog-post :where(ul) {
1227
+ list-style-type: disc;
1228
+ }
1229
+ .aranova-blog-post :where(ul ul) {
1230
+ list-style-type: circle;
1231
+ }
1232
+ .aranova-blog-post :where(ul ul ul) {
1233
+ list-style-type: square;
1234
+ }
1235
+ .aranova-blog-post :where(ol) {
1236
+ list-style-type: decimal;
1237
+ }
1238
+ .aranova-blog-post :where(ol ol) {
1239
+ list-style-type: lower-alpha;
1240
+ }
1241
+ .aranova-blog-post :where(ul) > li::marker {
1242
+ color: var(--ara-blog-color-text-muted);
1243
+ }
1244
+ .aranova-blog-post :where(ol) > li::marker {
1245
+ font-weight: 600;
1246
+ color: var(--_heading-color);
1247
+ }
1248
+ .aranova-blog-post :where(li + li) {
1249
+ margin-block-start: 0.35em;
1250
+ }
1251
+ .aranova-blog-post :where(li > ul, li > ol) {
1252
+ margin-block-start: 0.35em;
1253
+ }
1254
+
1255
+ /* --- Blockquote ------------------------------------------------------------ */
1256
+ .aranova-blog-post :where(blockquote) {
1257
+ margin: 0;
1258
+ padding-block: 0.25em;
1259
+ padding-inline: 1.25em 0.5em;
1260
+ border-inline-start: 3px solid var(--ara-blog-color-primary);
1261
+ color: var(--ara-blog-color-text-muted);
1262
+ font-style: italic;
1263
+ }
1264
+
1265
+ /* --- Code ------------------------------------------------------------------ */
1266
+ .aranova-blog-post :where(code) {
1267
+ background: var(--ara-blog-color-code-background);
1268
+ border-radius: calc(var(--_radius) / 2);
1269
+ padding: 0.15em 0.35em;
1270
+ font-size: 0.9em;
1271
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1272
+ }
1273
+ .aranova-blog-post :where(pre) {
1274
+ margin: 0;
1275
+ background: var(--ara-blog-color-code-background);
1276
+ border: 1px solid var(--ara-blog-color-border);
1277
+ border-radius: var(--_radius);
1278
+ padding: 1em;
1279
+ overflow-x: auto;
1280
+ line-height: 1.5;
1281
+ }
1282
+ .aranova-blog-post :where(pre code) {
1283
+ background: none;
1284
+ border-radius: 0;
1285
+ padding: 0;
1286
+ font-size: var(--_small-size);
1287
+ }
1288
+
1289
+ /* --- Media ------------------------------------------------------------------ */
1290
+ .aranova-blog-post :where(img) {
1291
+ max-width: 100%;
1292
+ height: auto;
1293
+ display: block;
1294
+ border: 1px solid var(--ara-blog-color-border);
1295
+ border-radius: var(--_radius);
1296
+ }
1297
+ .aranova-blog-post :where(figure) {
1298
+ margin: 0;
1299
+ }
1300
+ .aranova-blog-post :where(figcaption) {
1301
+ margin-block-start: 0.5em;
1302
+ font-size: var(--_small-size);
1303
+ color: var(--ara-blog-color-text-muted);
1304
+ text-align: var(--ara-blog-type-caption-align, center);
1305
+ }
1306
+
1307
+ /* --- Tables ------------------------------------------------------------------ */
1308
+ .aranova-blog-post .aranova-blog-table-wrap {
1309
+ overflow-x: auto;
1310
+ border: 1px solid var(--ara-blog-color-border);
1311
+ border-radius: var(--_radius);
1312
+ }
1313
+ .aranova-blog-post :where(table) {
1314
+ border-collapse: collapse;
1315
+ width: 100%;
1316
+ font-size: var(--_small-size);
1317
+ }
1318
+ .aranova-blog-post :where(th, td) {
1319
+ border: 0;
1320
+ border-block-end: 1px solid var(--ara-blog-color-border);
1321
+ padding: 0.6em 0.85em;
1322
+ text-align: left;
1323
+ }
1324
+ .aranova-blog-post :where(th) {
1325
+ background: var(--ara-blog-color-surface);
1326
+ border-block-end: 2px solid var(--ara-blog-color-border);
1327
+ font-weight: 600;
1328
+ }
1329
+ .aranova-blog-post :where(tbody tr:nth-child(even) td) {
1330
+ background: color-mix(
1331
+ in srgb,
1332
+ var(--ara-blog-color-surface) 55%,
1333
+ var(--ara-blog-color-background)
1334
+ );
1335
+ }
1336
+ .aranova-blog-post :where(tbody tr:last-child) :where(th, td) {
1337
+ border-block-end: 0;
1338
+ }
1339
+
1340
+ /* --- Section break ------------------------------------------------------------ */
1341
+ .aranova-blog-post :where(hr) {
1342
+ border: none;
1343
+ border-top: 2px solid var(--ara-blog-color-border);
1344
+ width: 4rem;
1345
+ margin-inline: auto;
1346
+ }
1347
+
1348
+ /* --- Callout ------------------------------------------------------------------ */
1349
+ .aranova-blog-post .aranova-blog-callout {
1350
+ background: var(--ara-blog-color-surface);
1351
+ border: 1px solid var(--ara-blog-color-border);
1352
+ border-inline-start: 4px solid var(--ara-blog-color-primary);
1353
+ border-radius: var(--_radius);
1354
+ padding: 0.85em 1.1em;
1355
+ }
1356
+ .aranova-blog-post .aranova-blog-callout > * + * {
1357
+ margin-block-start: var(--_flow-sm);
1358
+ }
1359
+
1360
+ /* --- CTA ------------------------------------------------------------------ */
1361
+ .aranova-blog-post .aranova-blog-cta {
1362
+ display: inline-block;
1363
+ background: var(--ara-blog-color-primary);
1364
+ color: var(--ara-blog-color-primary-foreground);
1365
+ border-radius: var(--_radius);
1366
+ padding: 0.6em 1.2em;
1367
+ font-weight: 600;
1368
+ text-decoration: none;
1369
+ transition:
1370
+ filter 150ms ease,
1371
+ transform 150ms ease;
1372
+ }
1373
+ .aranova-blog-post .aranova-blog-cta:hover {
1374
+ filter: brightness(1.08);
1375
+ transform: translateY(-1px);
1376
+ }
1377
+ .aranova-blog-post .aranova-blog-cta:active {
1378
+ filter: none;
1379
+ transform: none;
1380
+ }
1381
+ .aranova-blog-post .aranova-blog-cta:focus-visible {
1382
+ outline: 2px solid var(--ara-blog-color-primary);
1383
+ outline-offset: 2px;
1384
+ }
1385
+
1386
+ /* --- FAQ (details/summary accordion) ------------------------------------------ */
1387
+ .aranova-blog-post .aranova-blog-faq-item {
1388
+ border-block-start: 1px solid var(--ara-blog-color-border);
1389
+ padding-block: calc(var(--_flow) * 0.75);
1390
+ }
1391
+ .aranova-blog-post .aranova-blog-faq-question {
1392
+ cursor: pointer;
1393
+ list-style: none;
1394
+ position: relative;
1395
+ padding-inline-end: 1.75em;
1396
+ }
1397
+ .aranova-blog-post .aranova-blog-faq-question::-webkit-details-marker {
1398
+ display: none;
1399
+ }
1400
+ /* Rotating chevron (border trick \u2014 colors via theme vars only). */
1401
+ .aranova-blog-post .aranova-blog-faq-question::after {
1402
+ content: "";
1403
+ position: absolute;
1404
+ inset-block-start: 50%;
1405
+ inset-inline-end: 0.35em;
1406
+ width: 0.45em;
1407
+ height: 0.45em;
1408
+ border-inline-end: 2px solid var(--ara-blog-color-text-muted);
1409
+ border-block-end: 2px solid var(--ara-blog-color-text-muted);
1410
+ transform: translateY(-70%) rotate(45deg);
1411
+ transition: transform 150ms ease;
1412
+ }
1413
+ .aranova-blog-post .aranova-blog-faq-item[open] > .aranova-blog-faq-question::after {
1414
+ transform: translateY(-30%) rotate(225deg);
1415
+ }
1416
+ .aranova-blog-post .aranova-blog-faq-question:focus-visible {
1417
+ outline: 2px solid var(--ara-blog-color-link);
1418
+ outline-offset: 2px;
1419
+ border-radius: 2px;
1420
+ }
1421
+ .aranova-blog-post .aranova-blog-faq-answer {
1422
+ padding-block-start: var(--_flow-sm);
1423
+ }
1424
+ .aranova-blog-post .aranova-blog-faq-answer > * + * {
1425
+ margin-block-start: var(--_flow-sm);
1426
+ }
1427
+ /* Progressive open/close animation where the platform supports it. */
1428
+ @supports (interpolate-size: allow-keywords) {
1429
+ .aranova-blog-post .aranova-blog-faq-item {
1430
+ interpolate-size: allow-keywords;
1431
+ }
1432
+ .aranova-blog-post .aranova-blog-faq-item::details-content {
1433
+ block-size: 0;
1434
+ overflow-y: clip;
1435
+ transition:
1436
+ block-size 200ms ease,
1437
+ content-visibility 200ms allow-discrete;
1438
+ }
1439
+ .aranova-blog-post .aranova-blog-faq-item[open]::details-content {
1440
+ block-size: auto;
1441
+ }
1442
+ }
1443
+
1444
+ /* --- Key takeaways ------------------------------------------------------------ */
1445
+ .aranova-blog-post .aranova-blog-key-takeaways {
1446
+ background: var(--ara-blog-color-surface);
1447
+ border: 1px solid var(--ara-blog-color-border);
1448
+ border-radius: var(--_radius);
1449
+ padding: 1em 1.25em;
1450
+ }
1451
+ .aranova-blog-post .aranova-blog-key-takeaways :where(ul) {
1452
+ margin-block: 0;
1453
+ }
1454
+ .aranova-blog-post .aranova-blog-key-takeaways :where(ul) > li::marker {
1455
+ color: var(--ara-blog-color-primary);
1456
+ }
1457
+
1458
+ /* --- Article layout (content column + optional aside rail) -----------------------
1459
+ * \`.aranova-blog-layout\` is a plain block on narrow viewports (the aside, when
1460
+ * present, stacks after the content). On wide viewports a grid opens ONLY when
1461
+ * an aside exists, and the article widens by exactly the rail + gap so the
1462
+ * content column keeps its \`content_max_width\`. */
1463
+ .aranova-blog-post .aranova-blog-layout {
1464
+ /* Flex column (not block) so the aside can be VISUALLY hoisted above the
1465
+ * content on narrow viewports while the content stays first in DOM
1466
+ * (reading order / SEO). */
1467
+ display: flex;
1468
+ flex-direction: column;
1469
+ }
1470
+ .aranova-blog-post .aranova-blog-layout > .aranova-blog-aside {
1471
+ order: -1;
1472
+ margin-block-end: var(--_flow-lg);
1473
+ }
1474
+ @media (min-width: 1100px) {
1475
+ .aranova-blog-post:has(.aranova-blog-aside) {
1476
+ max-width: calc(var(--_content-max-width) + 240px + 3rem);
1477
+ }
1478
+ .aranova-blog-post .aranova-blog-layout:has(.aranova-blog-aside) {
1479
+ display: grid;
1480
+ grid-template-columns: minmax(0, 1fr) 240px;
1481
+ column-gap: 3rem;
1482
+ align-items: start;
1483
+ }
1484
+ .aranova-blog-post .aranova-blog-layout:has(.aranova-blog-aside) > .aranova-blog-aside {
1485
+ /* Reset the mobile hoist: grid auto-placement honors \`order\`, so -1 would
1486
+ * drop the aside into the FIRST column. */
1487
+ order: 0;
1488
+ margin-block-end: 0;
1489
+ position: sticky;
1490
+ top: 5rem;
1491
+ align-self: start;
1492
+ max-height: calc(100vh - 6rem);
1493
+ overflow: auto;
1494
+ }
1495
+ /* Rail on the LEFT: flip the column order (content DOM stays first for
1496
+ * reading order / SEO; only the visual columns swap). */
1497
+ .aranova-blog-post .aranova-blog-layout[data-toc-side="left"]:has(.aranova-blog-aside) {
1498
+ grid-template-columns: 240px minmax(0, 1fr);
1499
+ }
1500
+ /* Horizontal collapse, CLOSED: the rail column shrinks to the compact toggle
1501
+ * (auto) and the article reflows into the reclaimed space. */
1502
+ .aranova-blog-post
1503
+ .aranova-blog-layout:has(
1504
+ .aranova-blog-toc-panel[data-collapse="horizontal"]:not([data-open="true"])
1505
+ ) {
1506
+ grid-template-columns: minmax(0, 1fr) auto;
1507
+ }
1508
+ .aranova-blog-post
1509
+ .aranova-blog-layout[data-toc-side="left"]:has(
1510
+ .aranova-blog-toc-panel[data-collapse="horizontal"]:not([data-open="true"])
1511
+ ) {
1512
+ grid-template-columns: auto minmax(0, 1fr);
1513
+ }
1514
+ .aranova-blog-post
1515
+ .aranova-blog-layout[data-toc-side="left"]:has(.aranova-blog-aside)
1516
+ > .aranova-blog-content {
1517
+ order: 2;
1518
+ }
1519
+ .aranova-blog-post
1520
+ .aranova-blog-layout[data-toc-side="left"]:has(.aranova-blog-aside)
1521
+ > .aranova-blog-aside {
1522
+ order: 1;
1523
+ }
1524
+ }
1525
+
1526
+ /* --- Byline / meta row (chrome) --------------------------------------------------- */
1527
+ .aranova-blog-post .aranova-blog-meta {
1528
+ display: flex;
1529
+ flex-wrap: wrap;
1530
+ align-items: center;
1531
+ gap: 0.35em 0.5em;
1532
+ font-size: var(--_small-size);
1533
+ color: var(--ara-blog-color-text-muted);
1534
+ padding-block-end: var(--_flow);
1535
+ border-block-end: 1px solid var(--ara-blog-color-border);
1536
+ }
1537
+ .aranova-blog-post .aranova-blog-meta-item {
1538
+ display: inline-flex;
1539
+ align-items: baseline;
1540
+ gap: 0.5em;
1541
+ white-space: nowrap;
1542
+ }
1543
+ .aranova-blog-post .aranova-blog-meta-tags {
1544
+ display: inline-flex;
1545
+ flex-wrap: wrap;
1546
+ gap: 0.35em;
1547
+ margin-inline-start: auto;
1548
+ }
1549
+ .aranova-blog-post .aranova-blog-meta-tag {
1550
+ background: var(--ara-blog-color-surface);
1551
+ border: 1px solid var(--ara-blog-color-border);
1552
+ border-radius: 999px;
1553
+ padding: 0.1em 0.7em;
1554
+ white-space: nowrap;
1555
+ }
1556
+
1557
+ /* --- Breadcrumbs (chrome) ---------------------------------------------------------- */
1558
+ .aranova-blog-post .aranova-blog-breadcrumbs {
1559
+ font-size: var(--_small-size);
1560
+ color: var(--ara-blog-color-text-muted);
1561
+ }
1562
+ .aranova-blog-post .aranova-blog-breadcrumbs ol {
1563
+ list-style: none;
1564
+ margin: 0;
1565
+ padding: 0;
1566
+ display: flex;
1567
+ flex-wrap: wrap;
1568
+ align-items: baseline;
1569
+ gap: 0.2em 0.5em;
1570
+ }
1571
+ .aranova-blog-post .aranova-blog-breadcrumbs li {
1572
+ display: inline-flex;
1573
+ align-items: baseline;
1574
+ gap: 0.5em;
1575
+ }
1576
+ .aranova-blog-post .aranova-blog-breadcrumbs a {
1577
+ color: var(--ara-blog-color-text-muted);
1578
+ text-decoration: none;
1579
+ }
1580
+ .aranova-blog-post .aranova-blog-breadcrumbs a:hover {
1581
+ color: var(--ara-blog-color-link);
1582
+ text-decoration: underline;
1583
+ text-underline-offset: 0.15em;
1584
+ }
1585
+ .aranova-blog-post .aranova-blog-breadcrumbs [aria-current="page"] {
1586
+ color: var(--ara-blog-color-text);
1587
+ }
1588
+
1589
+ /* --- Table of contents (chrome) -----------------------------------------------------
1590
+ * Zero-JS shell: a native details/summary disclosure. Chevron follows the FAQ
1591
+ * accordion's border-trick pattern (theme vars only, no raw colors). */
1592
+ .aranova-blog-post .aranova-blog-toc {
1593
+ font-size: var(--_small-size);
1594
+ background: var(--ara-blog-color-surface);
1595
+ border: 1px solid var(--ara-blog-color-border);
1596
+ border-radius: var(--_radius);
1597
+ padding: 0.85em 1.1em;
1598
+ }
1599
+ .aranova-blog-post .aranova-blog-toc-title {
1600
+ cursor: pointer;
1601
+ list-style: none;
1602
+ position: relative;
1603
+ padding-inline-end: 1.75em;
1604
+ font-weight: 600;
1605
+ color: var(--_heading-color);
1606
+ }
1607
+ .aranova-blog-post .aranova-blog-toc-title::-webkit-details-marker {
1608
+ display: none;
1609
+ }
1610
+ .aranova-blog-post .aranova-blog-toc-title::after {
1611
+ content: "";
1612
+ position: absolute;
1613
+ inset-block-start: 50%;
1614
+ inset-inline-end: 0.35em;
1615
+ width: 0.45em;
1616
+ height: 0.45em;
1617
+ border-inline-end: 2px solid var(--ara-blog-color-text-muted);
1618
+ border-block-end: 2px solid var(--ara-blog-color-text-muted);
1619
+ transform: translateY(-70%) rotate(45deg);
1620
+ transition: transform 150ms ease;
1621
+ }
1622
+ .aranova-blog-post .aranova-blog-toc[open] > .aranova-blog-toc-title::after {
1623
+ transform: translateY(-30%) rotate(225deg);
1624
+ }
1625
+ .aranova-blog-post .aranova-blog-toc-title:focus-visible {
1626
+ outline: 2px solid var(--ara-blog-color-link);
1627
+ outline-offset: 2px;
1628
+ border-radius: 2px;
1629
+ }
1630
+ .aranova-blog-post .aranova-blog-toc ol {
1631
+ list-style: none;
1632
+ margin: 0.75em 0 0;
1633
+ padding: 0;
1634
+ }
1635
+ .aranova-blog-post .aranova-blog-toc li {
1636
+ margin: 0;
1637
+ }
1638
+ .aranova-blog-post .aranova-blog-toc li + li {
1639
+ margin-block-start: 0.3em;
1640
+ }
1641
+ .aranova-blog-post .aranova-blog-toc li[data-depth="3"] {
1642
+ padding-inline-start: 0.9em;
1643
+ font-size: 0.94em;
1644
+ }
1645
+ .aranova-blog-post .aranova-blog-toc a {
1646
+ display: -webkit-box;
1647
+ -webkit-box-orient: vertical;
1648
+ -webkit-line-clamp: 2;
1649
+ overflow: hidden;
1650
+ line-height: 1.35;
1651
+ color: var(--ara-blog-color-text-muted);
1652
+ text-decoration: none;
1653
+ }
1654
+ .aranova-blog-post .aranova-blog-toc a:hover {
1655
+ color: var(--ara-blog-color-link);
1656
+ text-decoration: underline;
1657
+ text-underline-offset: 0.15em;
1658
+ }
1659
+
1660
+ /* --- Table of contents (client island) ------------------------------------------
1661
+ * Animated companion to the zero-JS shell above: same box, but a real <button>
1662
+ * header (aria-expanded) and a grid-template-rows 0fr\u21941fr collapse. The global
1663
+ * reduced-motion block at the end of this sheet disables the transition. */
1664
+ .aranova-blog-post .aranova-blog-toc-panel {
1665
+ font-size: var(--_small-size);
1666
+ background: var(--ara-blog-color-surface);
1667
+ border: 1px solid var(--ara-blog-color-border);
1668
+ border-radius: var(--_radius);
1669
+ padding: 0.85em 1.1em;
1670
+ }
1671
+ .aranova-blog-post .aranova-blog-toc-panel-title {
1672
+ /* Button reset \u2014 host sites may or may not run Tailwind preflight. */
1673
+ appearance: none;
1674
+ background: none;
1675
+ border: 0;
1676
+ margin: 0;
1677
+ padding: 0;
1678
+ padding-inline-end: 1.75em;
1679
+ width: 100%;
1680
+ text-align: start;
1681
+ font: inherit;
1682
+ cursor: pointer;
1683
+ position: relative;
1684
+ font-weight: 600;
1685
+ color: var(--_heading-color);
1686
+ }
1687
+ /* Rotating chevron (border trick \u2014 same pattern as the FAQ / zero-JS TOC). */
1688
+ .aranova-blog-post .aranova-blog-toc-panel-title::after {
1689
+ content: "";
1690
+ position: absolute;
1691
+ inset-block-start: 50%;
1692
+ inset-inline-end: 0.35em;
1693
+ width: 0.45em;
1694
+ height: 0.45em;
1695
+ border-inline-end: 2px solid var(--ara-blog-color-text-muted);
1696
+ border-block-end: 2px solid var(--ara-blog-color-text-muted);
1697
+ transform: translateY(-70%) rotate(45deg);
1698
+ transition: transform 250ms ease;
1699
+ }
1700
+ .aranova-blog-post
1701
+ .aranova-blog-toc-panel[data-open="true"]
1702
+ > .aranova-blog-toc-panel-title::after {
1703
+ transform: translateY(-30%) rotate(225deg);
1704
+ }
1705
+ .aranova-blog-post .aranova-blog-toc-panel-title:focus-visible {
1706
+ outline: 2px solid var(--ara-blog-color-link);
1707
+ outline-offset: 2px;
1708
+ border-radius: 2px;
1709
+ }
1710
+ /* Collapsible body: the grid row animates 0fr\u21941fr; the clip wrapper hides overflow. */
1711
+ .aranova-blog-post .aranova-blog-toc-panel-body {
1712
+ display: grid;
1713
+ grid-template-rows: 0fr;
1714
+ transition: grid-template-rows 250ms ease;
1715
+ }
1716
+ .aranova-blog-post .aranova-blog-toc-panel[data-open="true"] > .aranova-blog-toc-panel-body {
1717
+ grid-template-rows: 1fr;
1718
+ }
1719
+ /* Horizontal collapse: a clean binary toggle along the inline axis. Open = the
1720
+ * normal rail; closed = the whole panel is a compact chevron chip and the rail
1721
+ * grid column reclaims the space (via \`:has()\` on the layout, below). No width
1722
+ * animation: grid \`auto\` tracks can't interpolate, and a sliding clip either
1723
+ * overflows the sticky aside (horizontal scrollbar) or reflows the entries \u2014
1724
+ * instant + correct beats janky. */
1725
+ .aranova-blog-post
1726
+ .aranova-blog-toc-panel[data-collapse="horizontal"]
1727
+ > .aranova-blog-toc-panel-body {
1728
+ display: block;
1729
+ grid-template-rows: none;
1730
+ }
1731
+ .aranova-blog-post
1732
+ .aranova-blog-toc-panel[data-collapse="horizontal"]:not([data-open="true"])
1733
+ > .aranova-blog-toc-panel-body {
1734
+ display: none;
1735
+ }
1736
+ /* Closed panel compacts to a small rounded chevron chip: the label is clipped
1737
+ * out of a square toggle box and the panel hugs it (the rail's \`auto\` grid
1738
+ * column then shrinks to the chip). */
1739
+ .aranova-blog-post .aranova-blog-toc-panel[data-collapse="horizontal"]:not([data-open="true"]) {
1740
+ width: fit-content;
1741
+ padding: 0.6em;
1742
+ }
1743
+ .aranova-blog-post
1744
+ .aranova-blog-toc-panel[data-collapse="horizontal"]:not([data-open="true"])
1745
+ > .aranova-blog-toc-panel-title {
1746
+ display: block;
1747
+ width: 1.4em;
1748
+ height: 1.4em;
1749
+ overflow: hidden;
1750
+ text-indent: 150%;
1751
+ white-space: nowrap;
1752
+ padding-inline-end: 0;
1753
+ }
1754
+ /* Horizontal chevron points along the fold axis (\u2039/\u203A), flipping on open/close
1755
+ * per side, instead of the vertical \u2304/\u2303. */
1756
+ .aranova-blog-post
1757
+ .aranova-blog-toc-panel[data-collapse="horizontal"]
1758
+ > .aranova-blog-toc-panel-title::after {
1759
+ inset-inline: auto 0.5em;
1760
+ }
1761
+ .aranova-blog-post
1762
+ .aranova-blog-toc-panel[data-collapse="horizontal"]:not([data-open="true"])
1763
+ > .aranova-blog-toc-panel-title::after {
1764
+ inset-inline: 0;
1765
+ margin-inline: auto;
1766
+ }
1767
+ /* Left rail: open folds left (\u2039 = rotate 135deg), closed expands right (\u203A = -45deg). */
1768
+ .aranova-blog-post
1769
+ .aranova-blog-toc-panel[data-collapse="horizontal"][data-side="left"][data-open="true"]
1770
+ > .aranova-blog-toc-panel-title::after {
1771
+ transform: translateY(-30%) rotate(135deg);
1772
+ }
1773
+ .aranova-blog-post
1774
+ .aranova-blog-toc-panel[data-collapse="horizontal"][data-side="left"]:not([data-open="true"])
1775
+ > .aranova-blog-toc-panel-title::after {
1776
+ transform: translateY(-70%) rotate(-45deg);
1777
+ }
1778
+ /* Right rail: mirror \u2014 open folds right (\u203A), closed expands left (\u2039). */
1779
+ .aranova-blog-post
1780
+ .aranova-blog-toc-panel[data-collapse="horizontal"][data-side="right"][data-open="true"]
1781
+ > .aranova-blog-toc-panel-title::after {
1782
+ transform: translateY(-70%) rotate(-45deg);
1783
+ }
1784
+ .aranova-blog-post
1785
+ .aranova-blog-toc-panel[data-collapse="horizontal"][data-side="right"]:not([data-open="true"])
1786
+ > .aranova-blog-toc-panel-title::after {
1787
+ transform: translateY(-30%) rotate(135deg);
1788
+ }
1789
+ .aranova-blog-post .aranova-blog-toc-panel-clip {
1790
+ overflow: hidden;
1791
+ min-height: 0;
1792
+ }
1793
+ .aranova-blog-post .aranova-blog-toc-panel ol {
1794
+ list-style: none;
1795
+ margin: 0.75em 0 0;
1796
+ padding: 0;
1797
+ }
1798
+ .aranova-blog-post .aranova-blog-toc-panel li {
1799
+ margin: 0;
1800
+ }
1801
+ .aranova-blog-post .aranova-blog-toc-panel li + li {
1802
+ margin-block-start: 0.3em;
1803
+ }
1804
+ .aranova-blog-post .aranova-blog-toc-panel li[data-depth="3"] {
1805
+ padding-inline-start: 0.9em;
1806
+ font-size: 0.94em;
1807
+ }
1808
+ .aranova-blog-post .aranova-blog-toc-panel-link {
1809
+ display: -webkit-box;
1810
+ -webkit-box-orient: vertical;
1811
+ -webkit-line-clamp: 2;
1812
+ overflow: hidden;
1813
+ line-height: 1.35;
1814
+ color: var(--ara-blog-color-text-muted);
1815
+ text-decoration: none;
1816
+ border-inline-start: 2px solid transparent;
1817
+ padding-inline-start: 0.6em;
1818
+ margin-inline-start: -0.75em;
1819
+ transition: color 150ms ease;
1820
+ }
1821
+ .aranova-blog-post .aranova-blog-toc-panel-link:hover {
1822
+ color: var(--ara-blog-color-link);
1823
+ text-decoration: underline;
1824
+ text-underline-offset: 0.15em;
1825
+ }
1826
+ /* Scroll-spy active entry: primary color + inline-start accent. */
1827
+ .aranova-blog-post .aranova-blog-toc-panel-link[data-active="true"] {
1828
+ color: var(--ara-blog-color-primary);
1829
+ border-inline-start-color: var(--ara-blog-color-primary);
1830
+ font-weight: 600;
1831
+ }
1832
+
1833
+ /* --- Reading progress (client island) --------------------------------------------
1834
+ * Fixed strip along the top of the viewport; fill = scaleX(scrolled fraction),
1835
+ * driven directly from rAF (no transition \u2014 it tracks position, so the
1836
+ * reduced-motion block has nothing to disable and the bar stays useful). */
1837
+ .aranova-blog-post .aranova-blog-progress {
1838
+ position: fixed;
1839
+ inset-block-start: 0;
1840
+ inset-inline: 0;
1841
+ z-index: 60;
1842
+ background: var(--ara-blog-color-primary);
1843
+ transform: scaleX(0);
1844
+ transform-origin: 0 50%;
1845
+ pointer-events: none;
1846
+ }
1847
+
1848
+ /* --- Vertical rhythm ------------------------------------------------------------
1849
+ * Kept AFTER the element rules: the owl selector and the :where()-wrapped
1850
+ * element rules all sit at (0,1,0), so source order lets rhythm win over the
1851
+ * elements' margin resets, and the per-heading overrides below win over the
1852
+ * owl. \`:is(...)\` targets both today's direct-child layout and the later
1853
+ * \`.aranova-blog-content\` wrapper at class specificity. */
1854
+ :is(.aranova-blog-post, .aranova-blog-content) > * + * {
1855
+ margin-block-start: var(--_flow);
1856
+ }
1857
+ :is(.aranova-blog-post, .aranova-blog-content) > :where(h2) {
1858
+ margin-block-start: var(--_flow-section);
1859
+ }
1860
+ :is(.aranova-blog-post, .aranova-blog-content) > :where(h3, h4) {
1861
+ margin-block-start: var(--_flow-lg);
1862
+ }
1863
+ :is(.aranova-blog-post, .aranova-blog-content) > :where(figure) {
1864
+ margin-block: var(--_flow-lg);
1865
+ }
1866
+ :is(.aranova-blog-post, .aranova-blog-content) > :where(hr) {
1867
+ margin-block: var(--_flow-section);
1868
+ }
1869
+
1870
+ /* --- Motion ------------------------------------------------------------------
1871
+ * Opt-in: a later phase stamps data-motion="subtle" on the article; without
1872
+ * the attribute nothing animates. */
1873
+ @media (prefers-reduced-motion: no-preference) {
1874
+ .aranova-blog-post[data-motion="subtle"] {
1875
+ animation: aranova-blog-fade-up 300ms ease-out both;
1876
+ }
1877
+ }
1878
+ @keyframes aranova-blog-fade-up {
1879
+ from {
1880
+ opacity: 0;
1881
+ transform: translateY(8px);
1882
+ }
1883
+ to {
1884
+ opacity: 1;
1885
+ transform: none;
1886
+ }
1887
+ }
1888
+ @media (prefers-reduced-motion: reduce) {
1889
+ .aranova-blog-post,
1890
+ .aranova-blog-post *,
1891
+ .aranova-blog-post *::before,
1892
+ .aranova-blog-post *::after {
1893
+ animation: none !important;
1894
+ transition: none !important;
1895
+ }
1896
+ }
1897
+ `;
1898
+
1899
+ // ../tracking-core/src/resources/blog/view/blog-post-view.tsx
1900
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1901
+ async function BlogPostView({
1902
+ mdx,
1903
+ theme = BASE_BLOG_THEME,
1904
+ components,
1905
+ header,
1906
+ aside,
1907
+ motion = "none",
1908
+ classNames,
1909
+ requireLeadingH1 = true,
1910
+ tocSide = "right"
1911
+ }) {
1912
+ const style = blogThemeToCssVars(theme);
1913
+ const merged = { ...createDefaultComponents([], classNames), ...components };
1914
+ const validation = validateBlogMdx(mdx, { requireLeadingH1 });
1915
+ if (!validation.ok) {
1916
+ const rules = Array.from(new Set(validation.violations.map((v) => v.rule))).join(", ");
1917
+ return /* @__PURE__ */ jsxs2(
1918
+ "article",
1919
+ {
1920
+ className: joinClassNames("aranova-blog-post", classNames?.article),
1921
+ "data-error": "mdx-validation-failed",
1922
+ style,
1923
+ children: [
1924
+ /* @__PURE__ */ jsx2("style", { children: BLOG_BASE_CSS }),
1925
+ /* @__PURE__ */ jsxs2("p", { children: [
1926
+ "This post could not be displayed. (blocked: ",
1927
+ rules,
1928
+ ")"
1929
+ ] })
1930
+ ]
1931
+ }
1932
+ );
1933
+ }
1934
+ let content;
1935
+ try {
1936
+ const compiled = await compileMDX({
1937
+ source: mdx,
1938
+ components: merged,
1939
+ options: { mdxOptions: { rehypePlugins: [rehypeSlug] } }
1940
+ });
1941
+ content = compiled.content;
1942
+ } catch {
1943
+ return /* @__PURE__ */ jsxs2(
1944
+ "article",
1945
+ {
1946
+ className: joinClassNames("aranova-blog-post", classNames?.article),
1947
+ "data-error": "mdx-render-failed",
1948
+ style,
1949
+ children: [
1950
+ /* @__PURE__ */ jsx2("style", { children: BLOG_BASE_CSS }),
1951
+ /* @__PURE__ */ jsx2("p", { children: "This post could not be displayed." })
1952
+ ]
1953
+ }
1954
+ );
1955
+ }
1956
+ return /* @__PURE__ */ jsxs2(
1957
+ "article",
1958
+ {
1959
+ className: joinClassNames("aranova-blog-post", classNames?.article),
1960
+ "data-motion": motion,
1961
+ style,
1962
+ children: [
1963
+ /* @__PURE__ */ jsx2("style", { children: BLOG_BASE_CSS }),
1964
+ header,
1965
+ /* @__PURE__ */ jsxs2(
1966
+ "div",
1967
+ {
1968
+ className: joinClassNames("aranova-blog-layout", classNames?.layout),
1969
+ "data-toc-side": tocSide,
1970
+ children: [
1971
+ /* @__PURE__ */ jsx2("div", { className: joinClassNames("aranova-blog-content", classNames?.content), children: content }),
1972
+ aside ? /* @__PURE__ */ jsx2("aside", { className: joinClassNames("aranova-blog-aside", classNames?.aside), children: aside }) : null
1973
+ ]
1974
+ }
1975
+ )
1976
+ ]
1977
+ }
1978
+ );
1979
+ }
1980
+
1981
+ // ../tracking-core/src/resources/blog/view/chrome/breadcrumbs.tsx
1982
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1983
+ function BlogBreadcrumbs({
1984
+ items,
1985
+ currentLabel,
1986
+ className
1987
+ }) {
1988
+ return /* @__PURE__ */ jsx3("nav", { "aria-label": "Breadcrumbs", className: joinClassNames("aranova-blog-breadcrumbs", className), children: /* @__PURE__ */ jsxs3("ol", { children: [
1989
+ items.map((item) => /* @__PURE__ */ jsxs3("li", { children: [
1990
+ /* @__PURE__ */ jsx3("a", { href: item.href, children: item.label }),
1991
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", className: "aranova-blog-breadcrumbs-separator", children: "/" })
1992
+ ] }, `${item.href} ${item.label}`)),
1993
+ /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsx3("span", { "aria-current": "page", children: currentLabel }) })
1994
+ ] }) });
1995
+ }
1996
+
1997
+ // ../tracking-core/src/resources/blog/view/chrome/meta.tsx
1998
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1999
+ var DEFAULT_BLOG_LOCALE = "en-CA";
2000
+ function formatBlogDate(iso, locale) {
2001
+ if (!iso) return null;
2002
+ const date = new Date(iso);
2003
+ if (Number.isNaN(date.getTime())) return null;
2004
+ let formatter;
2005
+ try {
2006
+ formatter = new Intl.DateTimeFormat(locale, { dateStyle: "long", timeZone: "UTC" });
2007
+ } catch {
2008
+ formatter = new Intl.DateTimeFormat(DEFAULT_BLOG_LOCALE, {
2009
+ dateStyle: "long",
2010
+ timeZone: "UTC"
2011
+ });
2012
+ }
2013
+ return { dateTime: date.toISOString().slice(0, 10), label: formatter.format(date) };
2014
+ }
2015
+ function BlogPostMeta({
2016
+ publishedAt,
2017
+ updatedAt,
2018
+ readingTimeMinutes,
2019
+ tags,
2020
+ locale = DEFAULT_BLOG_LOCALE,
2021
+ className
2022
+ }) {
2023
+ const published = formatBlogDate(publishedAt, locale);
2024
+ const updated = formatBlogDate(updatedAt, locale);
2025
+ const showUpdated = updated !== null && updated.dateTime !== published?.dateTime;
2026
+ const showReadingTime = typeof readingTimeMinutes === "number" && readingTimeMinutes > 0;
2027
+ const shownTags = (tags ?? []).filter((tag) => tag.length > 0);
2028
+ const items = [];
2029
+ if (published) {
2030
+ items.push({
2031
+ key: "published",
2032
+ node: /* @__PURE__ */ jsx4("time", { dateTime: published.dateTime, children: published.label })
2033
+ });
2034
+ }
2035
+ if (showUpdated && updated) {
2036
+ items.push({
2037
+ key: "updated",
2038
+ node: /* @__PURE__ */ jsxs4(Fragment, { children: [
2039
+ "Updated ",
2040
+ /* @__PURE__ */ jsx4("time", { dateTime: updated.dateTime, children: updated.label })
2041
+ ] })
2042
+ });
2043
+ }
2044
+ if (showReadingTime) {
2045
+ items.push({ key: "reading-time", node: /* @__PURE__ */ jsxs4(Fragment, { children: [
2046
+ readingTimeMinutes,
2047
+ " min read"
2048
+ ] }) });
2049
+ }
2050
+ if (items.length === 0 && shownTags.length === 0) return null;
2051
+ return /* @__PURE__ */ jsxs4("div", { className: joinClassNames("aranova-blog-meta", className), children: [
2052
+ items.map((item, index) => /* @__PURE__ */ jsxs4("span", { className: "aranova-blog-meta-item", children: [
2053
+ index > 0 ? /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "aranova-blog-meta-separator", children: "\xB7" }) : null,
2054
+ item.node
2055
+ ] }, item.key)),
2056
+ shownTags.length > 0 ? /* @__PURE__ */ jsx4("span", { className: "aranova-blog-meta-tags", children: shownTags.map((tag) => /* @__PURE__ */ jsx4("span", { className: "aranova-blog-meta-tag", children: tag }, tag)) }) : null
2057
+ ] });
2058
+ }
2059
+
2060
+ // ../tracking-core/src/resources/blog/view/chrome/toc.tsx
2061
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2062
+ function BlogTocShell({
2063
+ headings,
2064
+ title = "On this page",
2065
+ className
2066
+ }) {
2067
+ if (headings.length === 0) return null;
2068
+ return /* @__PURE__ */ jsxs5("details", { open: true, className: joinClassNames("aranova-blog-toc", className), children: [
2069
+ /* @__PURE__ */ jsx5("summary", { className: "aranova-blog-toc-title", children: title }),
2070
+ /* @__PURE__ */ jsx5("nav", { "aria-label": "Table of contents", children: /* @__PURE__ */ jsx5("ol", { children: headings.map((heading) => /* @__PURE__ */ jsx5("li", { "data-depth": heading.depth, children: /* @__PURE__ */ jsx5("a", { href: `#${heading.id}`, title: heading.text, children: heading.text }) }, heading.id)) }) })
2071
+ ] });
2072
+ }
2073
+
2074
+ // ../tracking-core/src/resources/blog/view/blog-post.tsx
2075
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2076
+ async function BlogPost({
2077
+ slug,
2078
+ client,
2079
+ defaultTheme,
2080
+ components,
2081
+ notFound,
2082
+ showMeta = false,
2083
+ showToc = false,
2084
+ tocSide = "right",
2085
+ showBreadcrumbs = false,
2086
+ breadcrumbs,
2087
+ locale,
2088
+ tocSlot,
2089
+ progressSlot,
2090
+ classNames
2091
+ }) {
2092
+ const post = await client.getPost(slug);
2093
+ if (!post) {
2094
+ return notFound ?? /* @__PURE__ */ jsx6("article", { className: "aranova-blog-post", "data-error": "not-found", children: /* @__PURE__ */ jsx6("p", { children: "This post could not be found." }) });
2095
+ }
2096
+ const theme = resolveBlogTheme(
2097
+ sanitizeBlogTheme(defaultTheme),
2098
+ sanitizeBlogTheme(post.theme_overrides)
2099
+ );
2100
+ const merged = { ...createDefaultComponents(post.images, classNames), ...components };
2101
+ const crumbs = showBreadcrumbs && breadcrumbs && breadcrumbs.length > 0 ? /* @__PURE__ */ jsx6(
2102
+ BlogBreadcrumbs,
2103
+ {
2104
+ items: breadcrumbs,
2105
+ currentLabel: post.title,
2106
+ className: classNames?.breadcrumbs
2107
+ }
2108
+ ) : null;
2109
+ const meta = showMeta ? /* @__PURE__ */ jsx6(
2110
+ BlogPostMeta,
2111
+ {
2112
+ publishedAt: post.published_at,
2113
+ updatedAt: post.updated_at,
2114
+ readingTimeMinutes: estimateReadingTime(post.mdx),
2115
+ locale,
2116
+ className: classNames?.meta
2117
+ }
2118
+ ) : null;
2119
+ const progress = progressSlot ? progressSlot() : null;
2120
+ const header = progress || crumbs || meta ? /* @__PURE__ */ jsxs6(Fragment2, { children: [
2121
+ progress,
2122
+ crumbs,
2123
+ meta
2124
+ ] }) : void 0;
2125
+ const headings = showToc ? extractBlogHeadings(post.mdx) : [];
2126
+ const aside = headings.length > 0 ? tocSlot?.(headings) ?? /* @__PURE__ */ jsx6(BlogTocShell, { headings, className: classNames?.toc }) : void 0;
2127
+ return /* @__PURE__ */ jsx6(
2128
+ BlogPostView,
2129
+ {
2130
+ mdx: post.mdx,
2131
+ theme,
2132
+ components: merged,
2133
+ header,
2134
+ aside,
2135
+ classNames,
2136
+ tocSide
2137
+ }
2138
+ );
2139
+ }
2140
+
2141
+ // src/blog-post.tsx
2142
+ import { ReadingProgress, TocPanel } from "@aranova/tracking-next/blog-client";
2143
+ import { jsx as jsx7 } from "react/jsx-runtime";
2144
+ function BlogPost2({
2145
+ showProgress = false,
2146
+ tocCollapse = "vertical",
2147
+ ...props
2148
+ }) {
2149
+ const tocSide = props.tocSide ?? "right";
2150
+ return /* @__PURE__ */ jsx7(
2151
+ BlogPost,
2152
+ {
2153
+ ...props,
2154
+ tocSlot: (headings) => /* @__PURE__ */ jsx7(
2155
+ TocPanel,
2156
+ {
2157
+ headings,
2158
+ className: props.classNames?.toc,
2159
+ collapse: tocCollapse,
2160
+ side: tocSide
2161
+ }
2162
+ ),
2163
+ progressSlot: showProgress ? () => /* @__PURE__ */ jsx7(ReadingProgress, { className: props.classNames?.progress }) : void 0
2164
+ }
2165
+ );
2166
+ }
2167
+ export {
2168
+ BASE_BLOG_THEME,
2169
+ BLOG_BASE_CSS,
2170
+ BLOG_BREAKPOINTS,
2171
+ BLOG_BREAKPOINT_MIN_WIDTHS_PX,
2172
+ BLOG_MDX_ALLOWED_COMPONENTS,
2173
+ BLOG_MDX_COMPONENT_GRAMMAR,
2174
+ BLOG_MDX_MAX_DEPTH,
2175
+ BLOG_MDX_MAX_NODES,
2176
+ BLOG_MDX_MAX_SIZE,
2177
+ BLOG_THEME_PRESETS,
2178
+ BlogBreadcrumbs,
2179
+ BlogPost2 as BlogPost,
2180
+ BlogPostMeta,
2181
+ BlogPostView,
2182
+ BlogTocShell,
2183
+ DEFAULT_BLOG_LOCALE,
2184
+ blogHeroImageSchema,
2185
+ blogImageSchema,
2186
+ blogManifestEntrySchema,
2187
+ blogManifestSchema,
2188
+ blogManifestWireSchema,
2189
+ blogPostContentSchema,
2190
+ blogPostContentWireSchema,
2191
+ blogSeoSchema,
2192
+ blogThemeColorsSchema,
2193
+ blogThemeOverrideSchema,
2194
+ blogThemeSchema,
2195
+ blogThemeSpacingSchema,
2196
+ blogThemeToCssVars,
2197
+ blogThemeTypographySchema,
2198
+ buildBlogPostMetadata,
2199
+ buildBlogPostingJsonLd,
2200
+ buildBlogSitemapEntries,
2201
+ buildFaqJsonLd,
2202
+ createBlogClient,
2203
+ createBlogFontTheme,
2204
+ createDefaultComponents,
2205
+ estimateReadingTime,
2206
+ extractBlogHeadings,
2207
+ extractFaqPairs,
2208
+ joinClassNames,
2209
+ nullishField,
2210
+ parseBlogManifest,
2211
+ parseBlogPostContent,
2212
+ resolveBlogTheme,
2213
+ responsive,
2214
+ sanitizeBlogTheme,
2215
+ serializeJsonLd,
2216
+ toBlogBreakpointMap,
2217
+ validateBlogMdx
2218
+ };
2219
+ //# sourceMappingURL=blog-server.mjs.map