@aranova/tracking-next 0.23.0 → 0.23.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.
- package/dist/blog-server.js +8 -1
- package/dist/blog-server.js.map +1 -1
- package/dist/blog-server.mjs +8 -1
- package/dist/blog-server.mjs.map +1 -1
- package/dist/blog.d.mts +8 -1
- package/dist/blog.d.ts +8 -1
- package/dist/blog.js +8 -1
- package/dist/blog.js.map +1 -1
- package/dist/blog.mjs +8 -1
- package/dist/blog.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/blog-server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/blog-server.ts","../../tracking-core/src/capabilities.ts","../../tracking-core/src/resources/blog/schema.ts","../../tracking-core/src/resources/blog/theme.ts","../../tracking-core/src/resources/blog/client.ts","../../tracking-core/src/resources/blog/presets.ts","../../tracking-core/src/resources/blog/view/class-names.ts","../../tracking-core/src/resources/blog/seo.ts","../../tracking-core/src/resources/blog/toc.ts","../../tracking-core/src/resources/blog/validate-mdx.ts","../../tracking-core/src/resources/blog/view/blog-post-view.tsx","../../tracking-core/src/resources/blog/view/components.tsx","../../tracking-core/src/resources/blog/view/styles.ts","../../tracking-core/src/resources/blog/view/chrome/breadcrumbs.tsx","../../tracking-core/src/resources/blog/view/chrome/meta.tsx","../../tracking-core/src/resources/blog/view/chrome/toc.tsx","../../tracking-core/src/resources/blog/view/blog-post.tsx","../src/blog-post.tsx"],"sourcesContent":["import \"server-only\";\n\n// Server-only blog entry — `@aranova/tracking-next/blog-server`. Everything on\n// `./blog` PLUS the async RSC renderers (`<BlogPost/>`, `<BlogPostView/>`),\n// which compile MDX via `next-mdx-remote/rsc` and must never reach a client\n// bundle (enforced by the `server-only` import above).\n// The surface is authored once in tracking-core (see blog-server-public.ts);\n// this file exists as the per-package tsup build entry PLUS the one override\n// below: the consumer-facing `BlogPost` is the enhanced wrapper that pre-wires\n// the animated client islands (TocPanel scroll-spy TOC, ReadingProgress bar)\n// into core's injection slots. Explicit exports win over `export *`, so this\n// entry still exposes exactly ONE BlogPost. Core's un-enhanced renderer stays\n// reachable for compositions that want the zero-JS shell (`BlogPostView` +\n// `BlogTocShell` directly).\nexport * from \"../../tracking-core/src/blog-server-public\";\nexport { BlogPost, type BlogPostProps } from \"./blog-post\";\n","// Capability self-report registry.\n//\n// Every SDK construct registers itself here at construction time, and the\n// registered set rides out on `TrackingClientContext.capabilities`, which the\n// backend persists on the session row. The dashboard derives what a client site\n// ACTUALLY has installed from those sessions, rather than trusting an operator\n// checkbox — see contracts/tracking-capabilities.json, which pins this id list\n// against the backend enum and the dashboard type union.\n//\n// Browser-only by design: a server-side client (calendar bookings) emits no\n// session, and its artifact is better evidence than a self-report anyway.\n//\n// This module deliberately imports nothing: it is the one thing every other\n// resource can import without adding an edge that could close a cycle.\n\n/**\n * Every capability an install can report. Pinned by\n * `contracts/tracking-capabilities.json` — adding one here without a catalog\n * entry fails the drift test.\n */\nexport const TRACKING_CAPABILITIES = [\n \"ad_tags_google\",\n \"ad_tags_meta\",\n \"base_tracking\",\n \"blog_rendering\",\n \"calendar_read\",\n \"calendar_write\",\n \"consent_controls\",\n \"conversion_goals_auto\",\n \"conversion_goals_manual\",\n \"cta_click_capture\",\n \"form_capture\",\n \"phone_click_capture\",\n \"phone_fields\",\n] as const;\n\nexport type TrackingCapability = (typeof TRACKING_CAPABILITIES)[number];\n\n/**\n * DOM attribute a server-rendered surface stamps to report itself.\n *\n * Blog rendering never calls the Aranova API — it reads the CDN directly — so\n * neither the ingest payload nor the request-header path can see it. The\n * renderer stamps this attribute instead and the browser context collector\n * picks it up on the next batch.\n */\nexport const CAPABILITY_DOM_ATTRIBUTE = \"data-aranova-capability\";\n\nconst registered = new Set<TrackingCapability>();\n\n/**\n * Record that a capability is present in this runtime.\n *\n * Idempotent and side-effect free — safe to call on every construction,\n * including React strict-mode double mounts.\n */\nexport function registerCapability(capability: TrackingCapability): void {\n registered.add(capability);\n}\n\n/**\n * Capabilities registered so far in this runtime, sorted for a stable wire\n * value (so the backend can hash the array to skip redundant writes).\n *\n * Also collects `data-aranova-capability` markers from the DOM, which is how\n * server-rendered surfaces that never call our API report themselves.\n */\nexport function getRegisteredCapabilities(): TrackingCapability[] {\n const all = new Set<TrackingCapability>(registered);\n for (const marker of readDomMarkers()) all.add(marker);\n return Array.from(all).sort();\n}\n\nfunction readDomMarkers(): TrackingCapability[] {\n if (typeof document === \"undefined\") return [];\n const known = new Set<string>(TRACKING_CAPABILITIES);\n const found: TrackingCapability[] = [];\n // Unknown values are dropped rather than forwarded: the backend rejects ids\n // outside its enum, and a stray attribute on a client's page must never be\n // able to fail an otherwise-valid ingest batch.\n for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {\n const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);\n if (value && known.has(value)) found.push(value as TrackingCapability);\n }\n return found;\n}\n\n/** Test-only: drop everything registered so far. */\nexport function resetRegisteredCapabilitiesForTests(): void {\n registered.clear();\n}\n","import { z } from \"zod\";\n\nimport { blogThemeOverrideSchema, nullishField } from \"./theme\";\n\n/**\n * Blog wire schemas — the client-side source of truth for the CDN-published\n * blog objects (ENG-31 Area C).\n *\n * {cdnBaseUrl}/blogs/v1/{namespace}/manifest.json (mutable)\n * {cdnBaseUrl}/blogs/v1/{namespace}/posts/{slug}/{content_hash}.json (immutable)\n *\n * Mirrored by the backend publisher and checked by the schema-drift contract\n * (the `resources` section of `events.schema.json`). Field names are FROZEN.\n *\n * These are TOLERANT readers (same philosophy as `conversion-config.ts`): the\n * backend may add fields freely — unknown fields are stripped, malformed\n * optional fields fall back to a safe default via `.catch()`, and only a\n * structurally unusable object makes the parse helpers return `null`. They\n * never throw.\n */\n\n// The wire major, pinned to the version this reader understands. Within v1\n// the contract is additive-only (unknown fields are stripped), but a future\n// v2 object is by definition not readable under v1 assumptions — the literal\n// makes the whole-object parse fail, so `parseBlog*` returns `null` (the\n// tolerant \"unusable object\" path) instead of misreading it.\nconst schemaVersionSchema = z.literal(1);\n\nexport const blogHeroImageSchema = z.object({\n url: z.string(),\n alt: z.string().catch(\"\"),\n width: z.number().int(),\n height: z.number().int(),\n});\n\nexport const blogManifestEntrySchema = z.object({\n slug: z.string(),\n title: z.string(),\n description: z.string().catch(\"\"),\n content_hash: z.string(),\n content_key: z.string(),\n published_at: z.string(),\n updated_at: z.string(),\n // Estimated read time in whole minutes (computed at publish from the post\n // body). 0 when unknown; index cards render \"N min read\".\n reading_minutes: z.number().int().catch(0),\n // A malformed tags array / hero_image degrades gracefully instead of\n // dropping the whole entry.\n tags: z.array(z.string()).catch([]),\n hero_image: blogHeroImageSchema.nullable().catch(null),\n});\n\n// WIRE schema — what the backend publisher actually writes (emitted into\n// `events.schema.json` and drift-checked against the Pydantic `BlogManifest`).\n// The runtime reader below layers per-entry drop-tolerance on top of it: a\n// writer never emits `null` entries, so that tolerance is a READER refinement,\n// deliberately kept out of the wire contract.\nexport const blogManifestWireSchema = z.object({\n schema_version: schemaVersionSchema,\n namespace: z.string(),\n generated_at: z.string(),\n posts: z.array(blogManifestEntrySchema).catch([]),\n});\n\nexport const blogManifestSchema = blogManifestWireSchema.extend({\n // A single malformed entry is dropped; the rest of the manifest survives.\n posts: z\n .array(blogManifestEntrySchema.nullable().catch(null))\n .transform((entries) => entries.filter((e): e is BlogManifestEntry => e !== null))\n .catch([]),\n});\n\nexport const blogImageSchema = z.object({\n id: z.string(),\n url: z.string(),\n alt: z.string().catch(\"\"),\n caption: z.string().nullable().catch(null),\n role: z.enum([\"hero\", \"inline\"]).catch(\"inline\"),\n width: z.number().int(),\n height: z.number().int(),\n mime: z.string().catch(\"\"),\n});\n\nexport const blogSeoSchema = z.object({\n meta_title: z.string().catch(\"\"),\n meta_description: z.string().catch(\"\"),\n canonical_path: z.string().catch(\"\"),\n og_image: z.string().nullable().catch(null),\n});\n\n// WIRE schema — see `blogManifestWireSchema` for the wire/reader split.\nexport const blogPostContentWireSchema = z.object({\n schema_version: schemaVersionSchema,\n slug: z.string(),\n title: z.string(),\n description: z.string().catch(\"\"),\n mdx: z.string(),\n images: z.array(blogImageSchema).catch([]),\n // Already schema-validated at publish time, but re-sanitized on read (and\n // again before rendering) — the override schema itself drops invalid values.\n // Nullish: the backend serializes an unset override as an explicit `null`\n // (Pydantic `model_dump()`); it folds into `undefined` for TS consumers.\n theme_overrides: nullishField(blogThemeOverrideSchema).catch(undefined),\n seo: blogSeoSchema.catch({\n meta_title: \"\",\n meta_description: \"\",\n canonical_path: \"\",\n og_image: null,\n }),\n jsonld: z.record(z.unknown()).catch({}),\n published_at: z.string(),\n // The applied_at of the version this content was published from; `null`\n // when the backend has no separate modified date. Nullish on the wire\n // (Pydantic `model_dump()` emits explicit `null`s), folded to `null` for a\n // single TS sentinel; a malformed value degrades to `null` instead of\n // dropping the post.\n updated_at: z\n .string()\n .nullish()\n .transform((v) => v ?? null)\n .catch(null),\n version_number: z.number().int().catch(0),\n content_hash: z.string(),\n});\n\nexport const blogPostContentSchema = blogPostContentWireSchema.extend({\n // A single malformed image record is dropped; the post survives.\n images: z\n .array(blogImageSchema.nullable().catch(null))\n .transform((images) => images.filter((i): i is BlogImage => i !== null))\n .catch([]),\n});\n\nexport type BlogHeroImage = z.infer<typeof blogHeroImageSchema>;\nexport type BlogManifestEntry = z.infer<typeof blogManifestEntrySchema>;\nexport type BlogManifest = z.infer<typeof blogManifestSchema>;\nexport type BlogImage = z.infer<typeof blogImageSchema>;\nexport type BlogSeo = z.infer<typeof blogSeoSchema>;\nexport type BlogPostContent = z.infer<typeof blogPostContentSchema>;\n\n/** Tolerant parse of a raw manifest object — `null` only when unusable. Never throws. */\nexport function parseBlogManifest(raw: unknown): BlogManifest | null {\n const parsed = blogManifestSchema.safeParse(raw);\n return parsed.success ? parsed.data : null;\n}\n\n/** Tolerant parse of a raw post-content object — `null` only when unusable. Never throws. */\nexport function parseBlogPostContent(raw: unknown): BlogPostContent | null {\n const parsed = blogPostContentSchema.safeParse(raw);\n return parsed.success ? parsed.data : null;\n}\n","import { z } from \"zod\";\n\n/**\n * Blog theme — the typed styling contract for CDN-published blog posts.\n *\n * Three layers resolve into one fully-populated {@link BlogTheme}\n * (later layers win):\n *\n * 1. {@link BASE_BLOG_THEME} — neutral editorial defaults (always complete)\n * 2. the client-registered default theme (client code, per site)\n * 3. per-post `theme_overrides` from the content JSON (backend-authored)\n *\n * Because layers 2 and 3 cross a trust boundary (they end up in inline\n * `style` attributes and CSS custom properties), **sanitization lives in the\n * zod schemas themselves**: every value is validated against a strict grammar\n * (hex/functional colors, unit lengths, clamped weights, token-list font\n * families) and any invalid individual value is DROPPED via `.catch(undefined)`\n * — never passed through to CSS.\n */\n\n// ---------------------------------------------------------------------------\n// Responsive values (Chakra-style breakpoint maps)\n// ---------------------------------------------------------------------------\n\n/**\n * Fixed, SDK-owned breakpoints (mobile-first `min-width` px). Ordered smallest\n * to largest — CSS emission and any future style build must iterate in this\n * order so later (wider) breakpoints cascade over earlier ones.\n */\nexport const BLOG_BREAKPOINTS = [\"sm\", \"md\", \"lg\", \"xl\", \"2xl\", \"3xl\"] as const;\n\nexport type BlogBreakpoint = (typeof BLOG_BREAKPOINTS)[number];\n\nexport const BLOG_BREAKPOINT_MIN_WIDTHS_PX = {\n sm: 640,\n md: 768,\n lg: 1024,\n xl: 1280,\n \"2xl\": 1536,\n \"3xl\": 1920,\n} as const satisfies Record<BlogBreakpoint, number>;\n\n/** A breakpoint map: `base` is required, wider breakpoints are optional. */\nexport type BlogBreakpointMap<T = string> = { base: T } & {\n [K in BlogBreakpoint]?: T | undefined;\n};\n\n/**\n * A dimensional theme token: a plain scalar, or a breakpoint map that\n * resolves mobile-first (`base` below `sm`, each key from its min-width up).\n */\nexport type ResponsiveValue<T = string> = T | BlogBreakpointMap<T>;\n\n/**\n * Normalize a {@link ResponsiveValue} to its map form. The ONE place the\n * scalar-vs-map distinction is handled — downstream consumers (CSS var\n * emission, the style build) work off the map and never `typeof`-sniff.\n */\nexport function toBlogBreakpointMap<T extends string | number>(\n value: ResponsiveValue<T>,\n): BlogBreakpointMap<T> {\n return typeof value === \"object\" ? value : { base: value };\n}\n\n// ---------------------------------------------------------------------------\n// Value grammars (the sanitizer)\n// ---------------------------------------------------------------------------\n\nconst HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;\nconst FUNCTIONAL_COLOR = /^(rgb|rgba|hsl|hsla|oklch)\\([0-9a-zA-Z ,.%/-]*\\)$/;\nconst LENGTH = /^-?\\d+(\\.\\d+)?(px|rem|em|%|ch|vw)$/;\n// line-height additionally allows a unitless multiplier (\"1.7\").\nconst LINE_HEIGHT = /^-?\\d+(\\.\\d+)?(px|rem|em|%|ch|vw)?$/;\n// One font-family token: optionally quoted word characters/spaces/hyphens.\n// Rejects `url(`, `;`, `{`, `}`, `<`, `>` and every other CSS escape vector.\nconst FONT_FAMILY_TOKEN = /^['\"]?[a-zA-Z0-9 -]+['\"]?$/;\n// A single `var(--name)` reference — how next/font sites expose their real\n// (build-mangled) family names, e.g. `var(--font-sans)`. Exactly one balanced\n// paren pair and a custom-property name; no other characters can sneak in.\nconst FONT_FAMILY_VAR_TOKEN = /^var\\(--[a-zA-Z0-9-]+\\)$/;\n\nconst colorValueSchema = z.string().refine((v) => HEX_COLOR.test(v) || FUNCTIONAL_COLOR.test(v), {\n message: \"must be a #hex or rgb()/rgba()/hsl()/hsla()/oklch() color\",\n});\n\nconst lengthValueSchema = z.string().regex(LENGTH, {\n message: \"must be a CSS length (px|rem|em|%|ch|vw)\",\n});\n\nconst lineHeightValueSchema = z.string().regex(LINE_HEIGHT, {\n message: \"must be a CSS length or unitless line-height\",\n});\n\nconst fontWeightSchema = z.number().int().min(100).max(1000);\n\n// Text-alignment keyword (figcaption today). Scalar, non-responsive.\nconst alignValueSchema = z.enum([\"left\", \"center\", \"right\"]);\n\nconst fontFamilySchema = z.string().refine(\n (v) => {\n const tokens = v.split(\",\").map((t) => t.trim());\n return (\n tokens.length > 0 &&\n tokens.every((t) => FONT_FAMILY_TOKEN.test(t) || FONT_FAMILY_VAR_TOKEN.test(t))\n );\n },\n { message: \"must be a comma-separated list of font-family or var(--custom-property) tokens\" },\n);\n\n// The wire truth for every optional field is Pydantic's `model_dump()`, which\n// serializes `Type | None = None` fields as explicit `null`s (never omits\n// them). So every optional field here must be NULL-tolerant, not just\n// absent-tolerant: `.nullish()` + a transform that folds `null` back into\n// `undefined` so downstream TS consumers keep a single \"absent\" sentinel.\n// zod-to-json-schema emits the transform's INPUT schema (nullable), which is\n// exactly what the backend drift test compares against the Pydantic mirror.\ntype NullishField<S extends z.ZodTypeAny> = z.ZodEffects<\n z.ZodOptional<z.ZodNullable<S>>,\n z.output<S> | undefined,\n z.input<S> | null | undefined\n>;\n\nexport function nullishField<S extends z.ZodTypeAny>(schema: S): NullishField<S> {\n return schema.nullish().transform((v) => v ?? undefined) as NullishField<S>;\n}\n\n// A scalar CSS value schema (the tokens' grammars are all string-valued).\ntype ScalarValueSchema = z.ZodType<string, z.ZodTypeDef, string>;\n\ntype BreakpointMapSchema = z.ZodType<\n BlogBreakpointMap<string>,\n z.ZodTypeDef,\n { base: string } & { [K in BlogBreakpoint]?: string | null | undefined }\n>;\n\n// Breakpoint map for one token. `base` is required and validated with the\n// token's own grammar; each breakpoint key is optional, null-tolerant (wire\n// nulls) and individually DROPPED when invalid (`.catch(undefined)`) — the\n// same per-value sanitization philosophy as the rest of this file. Unknown\n// keys are stripped by zod's default object behavior.\nfunction breakpointMapSchema(valueSchema: ScalarValueSchema): BreakpointMapSchema {\n return z.object({\n base: valueSchema,\n sm: nullishField(valueSchema).catch(undefined),\n md: nullishField(valueSchema).catch(undefined),\n lg: nullishField(valueSchema).catch(undefined),\n xl: nullishField(valueSchema).catch(undefined),\n \"2xl\": nullishField(valueSchema).catch(undefined),\n \"3xl\": nullishField(valueSchema).catch(undefined),\n }) as unknown as BreakpointMapSchema;\n}\n\ntype ResponsiveSchema = z.ZodUnion<[ScalarValueSchema, BreakpointMapSchema]>;\n\n/**\n * Schema factory for a responsive token: the token's scalar grammar OR a\n * {@link BlogBreakpointMap} of it. A map missing/failing `base` fails the\n * whole union (override parsing then drops the entire field via the\n * field-level `.catch`); an invalid non-base breakpoint value is dropped\n * per-value while the rest of the map survives.\n */\nexport function responsive(valueSchema: ScalarValueSchema): ResponsiveSchema {\n return z.union([valueSchema, breakpointMapSchema(valueSchema)]);\n}\n\n// ---------------------------------------------------------------------------\n// Full theme schemas (strict — every field required and valid)\n// ---------------------------------------------------------------------------\n\n// Flat color shape (every key → the plain color grammar). The full schema,\n// the deep-partial dark palette, and the override schema all derive from this\n// ONE shape so the optional `heading` never gets double-wrapped in nullish\n// layers (which would emit nested anyOfs in the JSON Schema snapshot).\nconst blogThemeColorsShape = {\n background: colorValueSchema,\n surface: colorValueSchema,\n text: colorValueSchema,\n text_muted: colorValueSchema,\n primary: colorValueSchema,\n primary_foreground: colorValueSchema,\n link: colorValueSchema,\n border: colorValueSchema,\n code_background: colorValueSchema,\n heading: colorValueSchema,\n} as const;\n\nexport const blogThemeColorsSchema = z.object({\n ...blogThemeColorsShape,\n // The ONE optional color in a resolved theme: when unset, headings fall back\n // to `text` in the stylesheet (`--_heading-color`), so `blogThemeToCssVars`\n // only emits `--ara-blog-color-heading` when a theme actually sets it.\n heading: nullishField(colorValueSchema),\n});\n\nexport const blogThemeTypographySchema = z.object({\n heading_family: fontFamilySchema,\n body_family: fontFamilySchema,\n heading_weight: fontWeightSchema,\n body_weight: fontWeightSchema,\n h1_size: responsive(lengthValueSchema),\n h2_size: responsive(lengthValueSchema),\n h3_size: responsive(lengthValueSchema),\n h4_size: responsive(lengthValueSchema),\n body_size: responsive(lengthValueSchema),\n body_line_height: responsive(lineHeightValueSchema),\n small_size: responsive(lengthValueSchema),\n // Scalar (non-responsive) length; the grammar allows negative em values.\n // h1/h2 consume it directly, h3/h4 at half strength (calc in the base CSS).\n heading_letter_spacing: lengthValueSchema,\n // Figure caption text alignment (left/center/right). Site-owner override;\n // default center in the base CSS if unset.\n caption_align: alignValueSchema,\n});\n\nexport const blogThemeSpacingSchema = z.object({\n content_max_width: responsive(lengthValueSchema),\n block_gap: responsive(lengthValueSchema),\n section_gap: responsive(lengthValueSchema),\n});\n\n// Deep-partial companion to `blogThemeColorsSchema` for the optional dark\n// palette: each color is individually optional AND null-tolerant (see\n// `nullishField`), mirroring the Pydantic side's `str | None = None` fields.\nconst blogThemeColorsPartialSchema = z.object(\n Object.fromEntries(\n Object.keys(blogThemeColorsShape).map((key) => [key, nullishField(colorValueSchema)]),\n ) as { [K in keyof typeof blogThemeColorsShape]: NullishField<typeof colorValueSchema> },\n);\n\nexport const blogThemeSchema = z.object({\n colors: blogThemeColorsSchema,\n colors_dark: nullishField(blogThemeColorsPartialSchema),\n typography: blogThemeTypographySchema,\n spacing: blogThemeSpacingSchema,\n radius: responsive(lengthValueSchema),\n});\n\nexport type BlogThemeColors = z.infer<typeof blogThemeColorsSchema>;\nexport type BlogThemeTypography = z.infer<typeof blogThemeTypographySchema>;\nexport type BlogThemeSpacing = z.infer<typeof blogThemeSpacingSchema>;\nexport type BlogTheme = z.infer<typeof blogThemeSchema>;\n\n// ---------------------------------------------------------------------------\n// Override schema (deep-partial + per-field drop-on-invalid)\n// ---------------------------------------------------------------------------\n\n// Each field is nullish (`null` on the wire — the backend's `model_dump()`\n// emits explicit nulls for unset fields — folds into `undefined`) +\n// `.catch(undefined)`: an invalid individual value is dropped (falls back to\n// the layer below at resolve time) without failing the group or the whole\n// override. A non-object group likewise drops to undefined.\nfunction dropInvalidFields<T extends z.ZodRawShape>(\n shape: T,\n): { [K in keyof T]: z.ZodCatch<NullishField<T[K]>> } {\n const out: Record<string, z.ZodTypeAny> = {};\n for (const [key, schema] of Object.entries(shape)) {\n out[key] = nullTolerant(schema).catch(undefined);\n }\n return out as { [K in keyof T]: z.ZodCatch<NullishField<T[K]>> };\n}\n\n// Null-tolerant wrapper that keeps responsive unions FLAT on the wire.\n// Wrapping a union in `.nullable()` would emit nested `anyOf`s\n// (`anyOf: [anyOf: [scalar, map], null]`) in the JSON Schema snapshot, while\n// the Pydantic mirror's `str | Map | None` emits a flat three-branch `anyOf`\n// — so for unions we splice `z.null()` into the union itself instead.\nfunction nullTolerant(schema: z.ZodTypeAny): z.ZodTypeAny {\n if (schema instanceof z.ZodUnion) {\n const options = (schema as z.ZodUnion<[z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]>).options;\n return z\n .union([...options, z.null()])\n .optional()\n .transform((v) => v ?? undefined);\n }\n return nullishField(schema);\n}\n\n// Built from the flat shape (NOT blogThemeColorsSchema.shape) so the optional\n// `heading` gets exactly one nullish layer, like every other override field.\nconst blogThemeColorsOverrideSchema = z.object(dropInvalidFields(blogThemeColorsShape));\nconst blogThemeTypographyOverrideSchema = z.object(\n dropInvalidFields(blogThemeTypographySchema.shape),\n);\nconst blogThemeSpacingOverrideSchema = z.object(dropInvalidFields(blogThemeSpacingSchema.shape));\n\nexport const blogThemeOverrideSchema = z.object(\n dropInvalidFields({\n colors: blogThemeColorsOverrideSchema,\n colors_dark: blogThemeColorsOverrideSchema,\n typography: blogThemeTypographyOverrideSchema,\n spacing: blogThemeSpacingOverrideSchema,\n radius: responsive(lengthValueSchema),\n }),\n);\n\nexport type BlogThemeOverride = z.infer<typeof blogThemeOverrideSchema>;\n\n// ---------------------------------------------------------------------------\n// Base theme — neutral editorial defaults\n// ---------------------------------------------------------------------------\n\nexport const BASE_BLOG_THEME: BlogTheme = {\n colors: {\n background: \"#ffffff\",\n surface: \"#f5f5f5\",\n text: \"#171717\",\n text_muted: \"#6b6b6b\",\n primary: \"#171717\",\n primary_foreground: \"#ffffff\",\n link: \"#1d4ed8\",\n border: \"#e5e5e5\",\n code_background: \"#f5f5f5\",\n },\n typography: {\n heading_family: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n body_family: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n // Responsive defaults: comfortable mobile sizes, larger from `md` up.\n // This replaces the hardcoded mobile media query the base CSS used to need.\n h1_size: { base: \"1.9rem\", md: \"2.25rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.75rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1rem\",\n body_line_height: \"1.7\",\n small_size: \"0.875rem\",\n heading_letter_spacing: \"-0.02em\",\n caption_align: \"center\",\n },\n spacing: {\n content_max_width: \"42rem\",\n block_gap: \"1.25rem\",\n section_gap: \"3rem\",\n },\n radius: \"8px\",\n};\n\n// ---------------------------------------------------------------------------\n// Functions\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitize an untrusted theme(-override) object. Invalid individual values are\n * dropped; total garbage (non-object, arrays, …) yields `{}`. Never throws.\n */\nexport function sanitizeBlogTheme(raw: unknown): BlogThemeOverride {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const parsed = blogThemeOverrideSchema.safeParse(raw);\n return parsed.success ? parsed.data : {};\n}\n\nfunction mergeGroup<T extends Record<string, unknown>>(base: T, over: Partial<T> | undefined): T {\n if (!over) return base;\n const out = { ...base };\n for (const [key, value] of Object.entries(over)) {\n if (value !== undefined) (out as Record<string, unknown>)[key] = value;\n }\n return out;\n}\n\n/**\n * Deep-merge override layers onto {@link BASE_BLOG_THEME}. Later layers win;\n * `null`/`undefined` layers and `undefined` fields are skipped, so the result\n * is always a fully-populated {@link BlogTheme}.\n *\n * Responsive tokens merge at TOKEN granularity: a later layer's value —\n * scalar or breakpoint map — REPLACES the whole token. There is deliberately\n * no per-breakpoint merge (a layer overriding `h1_size: \"2rem\"` on top of a\n * base map must not inherit the base's `md` entry).\n */\nexport function resolveBlogTheme(\n ...layers: Array<BlogThemeOverride | null | undefined>\n): BlogTheme {\n let resolved: BlogTheme = {\n colors: { ...BASE_BLOG_THEME.colors },\n colors_dark: BASE_BLOG_THEME.colors_dark ? { ...BASE_BLOG_THEME.colors_dark } : undefined,\n typography: { ...BASE_BLOG_THEME.typography },\n spacing: { ...BASE_BLOG_THEME.spacing },\n radius: BASE_BLOG_THEME.radius,\n };\n for (const layer of layers) {\n if (!layer) continue;\n resolved = {\n colors: mergeGroup(resolved.colors, layer.colors),\n colors_dark:\n layer.colors_dark || resolved.colors_dark\n ? mergeGroup(resolved.colors_dark ?? {}, layer.colors_dark)\n : undefined,\n typography: mergeGroup(resolved.typography, layer.typography),\n spacing: mergeGroup(resolved.spacing, layer.spacing),\n radius: layer.radius ?? resolved.radius,\n };\n }\n return resolved;\n}\n\n// (split/join instead of String#replaceAll — the package lib target is ES2020.)\nfunction kebab(key: string): string {\n return key.split(\"_\").join(\"-\");\n}\n\n// Emit one (possibly responsive) token: the base var carries the scalar (or\n// the map's `base`), and each PRESENT breakpoint key adds a `-<bp>` suffixed\n// var (e.g. `--ara-blog-type-h1-size-md`). Absent breakpoints emit nothing —\n// the stylesheet's media queries fall back to the base var.\nfunction emitResponsiveVar(\n vars: Record<string, string>,\n name: string,\n value: ResponsiveValue<string | number>,\n): void {\n const map = toBlogBreakpointMap(value);\n vars[name] = String(map.base);\n for (const bp of BLOG_BREAKPOINTS) {\n const bpValue = map[bp];\n if (bpValue !== undefined) vars[`${name}-${bp}`] = String(bpValue);\n }\n}\n\n/**\n * Flatten a resolved theme into `--ara-blog-*` CSS custom properties for an\n * inline `style` attribute. When `colors_dark` is present, color values are\n * emitted as `light-dark(lightVal, darkVal)` and `color-scheme: light dark`\n * is included so the browser picks the active side. Responsive tokens emit\n * their base var plus `-sm`/`-md`/… suffixed vars for present breakpoints.\n */\nexport function blogThemeToCssVars(theme: BlogTheme): Record<string, string> {\n const vars: Record<string, string> = {};\n const dark = theme.colors_dark;\n const hasDark = !!dark && Object.values(dark).some((v) => v !== undefined);\n\n // Union of light + dark keys: an optional color (only `heading` today) set\n // ONLY on the dark side still needs its var emitted.\n const colorKeys = new Set([...Object.keys(theme.colors), ...(dark ? Object.keys(dark) : [])]);\n for (const key of colorKeys) {\n const light = theme.colors[key as keyof BlogThemeColors];\n const darkValue = hasDark ? dark?.[key as keyof BlogThemeColors] : undefined;\n // Optional colors (only `heading` today) emit nothing when unset — the\n // stylesheet's fallback (`--_heading-color` → text) takes over. A theme\n // that sets ONLY the dark side still gets a var: the light side falls\n // back to `text`, which is what the stylesheet would use anyway.\n const lightValue = light ?? (darkValue !== undefined ? theme.colors.text : undefined);\n if (lightValue === undefined) continue;\n vars[`--ara-blog-color-${kebab(key)}`] = darkValue\n ? `light-dark(${lightValue}, ${darkValue})`\n : lightValue;\n }\n // camelCase: these vars land in a React `style` prop, which rejects kebab\n // names for real (non-custom) CSS properties.\n if (hasDark) vars.colorScheme = \"light dark\";\n\n for (const [key, value] of Object.entries(theme.typography)) {\n emitResponsiveVar(vars, `--ara-blog-type-${kebab(key)}`, value);\n }\n for (const [key, value] of Object.entries(theme.spacing)) {\n emitResponsiveVar(vars, `--ara-blog-space-${kebab(key)}`, value);\n }\n emitResponsiveVar(vars, \"--ara-blog-radius\", theme.radius);\n return vars;\n}\n","import { registerCapability } from \"../../capabilities\";\nimport {\n parseBlogManifest,\n parseBlogPostContent,\n type BlogManifest,\n type BlogManifestEntry,\n type BlogPostContent,\n} from \"./schema\";\n\n/**\n * Blog CDN client — read-only fetchers over the published blog objects.\n *\n * Designed for Next.js server components but framework-free: the Next-specific\n * `next.revalidate` fetch extension is expressed as a local structural type\n * ({@link NextFetchInit}) so we never import `next` types and a plain\n * `fetch` (Node, tests) simply ignores the extra property.\n *\n * Error policy: reads are best-effort. Network failures, non-2xx responses,\n * and unparseable bodies all resolve to `null` / `[]` — a blog page must\n * degrade, never crash the host site.\n */\n\n/** `RequestInit` + the Next.js data-cache extension (structural — no `next` import). */\nexport type NextFetchInit = RequestInit & {\n next?: { revalidate?: number | false; tags?: string[] };\n};\n\nexport interface BlogClientConfig {\n /**\n * Reserved for future authenticated reads (draft previews fetched from the\n * API instead of the public CDN). Published content is world-readable, so\n * the key is currently NOT sent — but it is part of the config contract so\n * client sites don't need a breaking config change when draft reads ship.\n */\n apiKey: string;\n /** CDN origin, e.g. `https://cdn.aranova.io` (trailing slash tolerated). */\n cdnBaseUrl: string;\n /** Opaque per-business namespace under `blogs/v1/`. */\n namespace: string;\n /** Manifest ISR revalidate window (seconds). Default 300. */\n manifestRevalidateSeconds?: number;\n /** Injectable for tests / non-global-fetch runtimes. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface BlogClient {\n /** The mutable manifest (index of published posts), or `null` on any miss. */\n getManifest(): Promise<BlogManifest | null>;\n /** Manifest entries sorted `published_at` desc. `[]` on any miss. */\n listPosts(): Promise<BlogManifestEntry[]>;\n /** Full post content for a slug, or `null` when unknown/unfetchable. */\n getPost(slug: string): Promise<BlogPostContent | null>;\n /** `generateStaticParams()`-shaped slug list. */\n getStaticParams(): Promise<Array<{ slug: string }>>;\n}\n\nexport function createBlogClient(config: BlogClientConfig): BlogClient {\n registerCapability(\"blog_rendering\");\n const base = config.cdnBaseUrl.replace(/\\/+$/, \"\");\n const doFetch: typeof fetch = (...args) => (config.fetchImpl ?? fetch)(...args);\n\n async function fetchJson(url: string, init: NextFetchInit): Promise<unknown | null> {\n try {\n const response = await doFetch(url, init);\n if (!response.ok) return null;\n return (await response.json()) as unknown;\n } catch {\n return null;\n }\n }\n\n async function getManifest(): Promise<BlogManifest | null> {\n // MUTABLE object (Cache-Control max-age=60 at the CDN) — cached through the\n // Next.js data cache with a revalidate window instead of force-cache.\n const init = {\n next: { revalidate: config.manifestRevalidateSeconds ?? 300 },\n } satisfies NextFetchInit;\n const raw = await fetchJson(`${base}/blogs/v1/${config.namespace}/manifest.json`, init);\n return raw === null ? null : parseBlogManifest(raw);\n }\n\n async function listPosts(): Promise<BlogManifestEntry[]> {\n const manifest = await getManifest();\n if (!manifest) return [];\n return [...manifest.posts].sort((a, b) => b.published_at.localeCompare(a.published_at));\n }\n\n async function getPost(slug: string): Promise<BlogPostContent | null> {\n const manifest = await getManifest();\n const entry = manifest?.posts.find((post) => post.slug === slug);\n if (!entry) return null;\n // IMMUTABLE object (hash-addressed, max-age=1y) — cache forever; a content\n // change publishes a new hash, which the manifest points at.\n const init = { cache: \"force-cache\" } satisfies NextFetchInit;\n const raw = await fetchJson(`${base}/${entry.content_key.replace(/^\\/+/, \"\")}`, init);\n return raw === null ? null : parseBlogPostContent(raw);\n }\n\n async function getStaticParams(): Promise<Array<{ slug: string }>> {\n const posts = await listPosts();\n return posts.map((post) => ({ slug: post.slug }));\n }\n\n return { getManifest, listPosts, getPost, getStaticParams };\n}\n","// Curated font/type-scale presets for the blog theme — React-free, part of\n// the `@aranova/tracking-next/blog` surface (re-exported via blog-public.ts).\n//\n// Each preset is a plain `BlogThemeOverride`: a font pairing with REAL\n// web-safe fallback stacks, a responsive type scale, weights, letter-spacing,\n// spacing, and radius. Colors are deliberately (almost entirely) UNSET so the\n// client site's own palette wins — the only exception is a subtle\n// `code_background` where the code surface is part of the preset's identity.\n//\n// Contract (tested in __tests__/presets.test.ts): every preset passes\n// `sanitizeBlogTheme(preset)` UNCHANGED — a preset must never rely on values\n// the sanitizer would drop.\n\nimport type { BlogThemeOverride } from \"./theme\";\n\nexport type BlogThemePresetName = \"editorial\" | \"modern\" | \"warm\" | \"technical\";\n\n/**\n * Named theme presets. Layer one under your site palette:\n *\n * ```ts\n * const theme = resolveBlogTheme(\n * createBlogFontTheme(BLOG_THEME_PRESETS.editorial, {\n * heading: \"var(--font-serif)\", // next/font variable\n * body: \"var(--font-sans)\",\n * }),\n * { colors: { primary: \"#0f766e\", link: \"#0f766e\" } },\n * );\n * ```\n */\nexport const BLOG_THEME_PRESETS: Record<BlogThemePresetName, BlogThemeOverride> = {\n /** Serif display headings over a humanist sans body — long-form magazine feel. */\n editorial: {\n typography: {\n heading_family: \"'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif\",\n body_family: \"'Source Sans 3', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n heading_weight: 600,\n body_weight: 400,\n h1_size: { base: \"2rem\", md: \"2.5rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.875rem\" },\n h3_size: \"1.3rem\",\n h4_size: \"1.125rem\",\n body_size: \"1.0625rem\",\n body_line_height: \"1.75\",\n heading_letter_spacing: \"-0.015em\",\n },\n spacing: {\n block_gap: \"1.35rem\",\n section_gap: \"3.25rem\",\n },\n radius: \"6px\",\n },\n /** Geometric sans pairing (Inter-first) — tight, contemporary product voice. */\n modern: {\n typography: {\n heading_family: \"'Inter', 'SF Pro Display', 'Segoe UI', Roboto, sans-serif\",\n body_family: \"'Inter', 'SF Pro Text', 'Segoe UI', Roboto, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n h1_size: { base: \"2.1rem\", md: \"2.75rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.75rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1rem\",\n body_line_height: \"1.7\",\n heading_letter_spacing: \"-0.03em\",\n },\n spacing: {\n block_gap: \"1.25rem\",\n section_gap: \"3rem\",\n },\n radius: \"12px\",\n },\n /** Rounded-friendly pairing with a softer scale — approachable, human tone. */\n warm: {\n typography: {\n heading_family: \"'Nunito', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif\",\n body_family: \"'Nunito Sans', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n h1_size: { base: \"1.9rem\", md: \"2.35rem\" },\n h2_size: { base: \"1.45rem\", md: \"1.7rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1.0625rem\",\n body_line_height: \"1.75\",\n heading_letter_spacing: \"-0.01em\",\n },\n spacing: {\n block_gap: \"1.35rem\",\n section_gap: \"3.25rem\",\n },\n radius: \"16px\",\n },\n /** Workmanlike sans with a prominent code surface — docs/engineering feel. */\n technical: {\n colors: {\n // The one preset that ships a color: a cool code surface is part of the\n // \"technical\" identity. Everything else stays on the site palette.\n code_background: \"#eef1f5\",\n },\n colors_dark: {\n code_background: \"#1b1f27\",\n },\n typography: {\n heading_family: \"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n body_family: \"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n heading_weight: 600,\n body_weight: 400,\n h1_size: { base: \"1.85rem\", md: \"2.25rem\" },\n h2_size: { base: \"1.45rem\", md: \"1.65rem\" },\n h3_size: \"1.2rem\",\n h4_size: \"1.0625rem\",\n body_size: \"1rem\",\n body_line_height: \"1.65\",\n heading_letter_spacing: \"-0.01em\",\n },\n spacing: {\n block_gap: \"1.15rem\",\n section_gap: \"2.75rem\",\n },\n radius: \"6px\",\n },\n};\n\n/** Real font sources for a preset: next/font CSS variables (or literal family names). */\nexport interface BlogFontSources {\n /** e.g. `\"var(--font-serif)\"` from `next/font`'s `variable` option. */\n heading?: string;\n /** e.g. `\"var(--font-sans)\"`. */\n body?: string;\n}\n\n// Prepend a caller-supplied family (usually a next/font `var(--…)`) onto the\n// preset's fallback stack. The caller value may itself be a comma-separated\n// stack; either way the preset's web-safe fallbacks ride behind it.\nfunction prependFamily(front: string | undefined, stack: string | undefined): string | undefined {\n if (!front) return stack;\n return stack ? `${front}, ${stack}` : front;\n}\n\n/**\n * Wire a site's real fonts into a preset: returns a NEW override (no\n * mutation) whose heading/body families are `\"<font>, <preset fallback\n * stack>\"` — pass your `next/font` variable names and the preset's web-safe\n * fallbacks keep covering the pre-hydration/fallback paint.\n *\n * ```ts\n * const serif = Source_Serif_4({ subsets: [\"latin\"], variable: \"--font-serif\" });\n * const sans = Inter({ subsets: [\"latin\"], variable: \"--font-sans\" });\n * const override = createBlogFontTheme(BLOG_THEME_PRESETS.editorial, {\n * heading: \"var(--font-serif)\",\n * body: \"var(--font-sans)\",\n * });\n * ```\n */\nexport function createBlogFontTheme(\n preset: BlogThemeOverride,\n fonts: BlogFontSources,\n): BlogThemeOverride {\n const heading_family = prependFamily(fonts.heading, preset.typography?.heading_family);\n const body_family = prependFamily(fonts.body, preset.typography?.body_family);\n return {\n ...preset,\n typography: {\n ...preset.typography,\n ...(heading_family !== undefined ? { heading_family } : {}),\n ...(body_family !== undefined ? { body_family } : {}),\n },\n };\n}\n","// Client-side className overrides for the blog view — the escape hatch for\n// sites that want to hook a Tailwind/utility class onto a specific surface\n// without replacing the whole component (see `components` for that) or\n// writing global CSS against the fixed `.aranova-blog-*` selectors (still the\n// simplest option and requires no API — these classes are stable and public).\n//\n// `joinClassNames` is a plain append, not a Tailwind-merge: every base class\n// here is an SDK-internal selector (`aranova-blog-callout`, …), never a\n// utility class, so there is nothing for an override to conflict with —\n// dropping a `tailwind-merge` dependency into a framework-free package for a\n// non-conflict is unnecessary weight.\nexport function joinClassNames(base: string, extra?: string): string {\n return extra ? `${base} ${extra}` : base;\n}\n\n/**\n * One optional override per styleable surface. Every key merges onto its\n * fixed base class (never replaces it), so the `.aranova-blog-*` selector\n * still applies — this is additive, not a restyle mechanism.\n */\nexport interface BlogClassNameOverrides {\n /** The `<article>` root. */\n article?: string;\n /** The content/aside grid wrapper. */\n layout?: string;\n /** The content column. */\n content?: string;\n /** The aside rail (table of contents). */\n aside?: string;\n /** `<Callout>` MDX component. */\n callout?: string;\n /** `<Cta>` MDX component. */\n cta?: string;\n /** `<BlogImage>` MDX component's `<figure>` wrapper. */\n figure?: string;\n /** `<FaqItem>` MDX component's `<details>` wrapper. */\n faqItem?: string;\n /** `<FaqItem>`'s `<summary>`. */\n faqQuestion?: string;\n /** `<FaqItem>`'s answer `<div>`. */\n faqAnswer?: string;\n /** `<KeyTakeaways>` MDX component. */\n keyTakeaways?: string;\n /** The `<table>` overflow wrapper `<div>`. */\n tableWrap?: string;\n /** `BlogPostMeta` byline row. */\n meta?: string;\n /** `BlogBreadcrumbs` nav. */\n breadcrumbs?: string;\n /** `BlogTocShell` / the animated `TocPanel` island. */\n toc?: string;\n /** The `ReadingProgress` island. */\n progress?: string;\n}\n","import type { BlogManifestEntry, BlogPostContent } from \"./schema\";\n\n/**\n * SEO builders — React-free helpers that turn post content into Next.js\n * `Metadata`-shaped objects, BlogPosting JSON-LD, and sitemap entries.\n *\n * Deliberately does NOT import `next` types: {@link BlogPostMetadata} is a\n * local structural interface whose shape is spread-compatible with Next's\n * `Metadata`, so the helpers stay usable from any framework (and from the\n * internal dashboard's preview renderer).\n */\n\nexport interface BlogSeoOptions {\n /** Site origin, e.g. `https://client.example.com`. */\n siteUrl: string;\n /** Route the blog is mounted at. Default `/blog`. */\n basePath?: string;\n}\n\nexport interface BlogJsonLdOptions extends BlogSeoOptions {\n /** Emits `publisher: { \"@type\": \"Organization\", name }` when given. */\n publisherName?: string;\n}\n\nexport interface BlogPostMetadataImage {\n url: string;\n width?: number;\n height?: number;\n alt?: string;\n}\n\n/** Structural subset of Next.js `Metadata` — spread it into `generateMetadata`. */\nexport interface BlogPostMetadata {\n title: string;\n description: string;\n alternates: { canonical: string };\n openGraph: {\n title: string;\n description: string;\n type: \"article\";\n url: string;\n images?: BlogPostMetadataImage[];\n publishedTime?: string;\n };\n}\n\nexport interface BlogSitemapEntry {\n url: string;\n lastModified: string;\n}\n\n/** Join URL segments without doubling or dropping slashes. */\nfunction joinUrl(base: string, ...paths: Array<string | undefined>): string {\n let out = base.replace(/\\/+$/, \"\");\n for (const path of paths) {\n const segment = path?.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (segment) out += `/${segment}`;\n }\n return out;\n}\n\n// The canonical URL is ALWAYS derived from the client site's own routing\n// (`siteUrl` + `basePath` + slug) — the client mounts the route, so client\n// routing wins. The backend's `seo.canonical_path` is informational only: it\n// assumes a `/blog` mount and would emit a canonical pointing at a\n// non-existent path on sites mounted elsewhere (e.g. `/news`). This also\n// keeps metadata, JSON-LD, and sitemap URLs derived from one rule.\nfunction canonicalUrl(post: BlogPostContent, opts: BlogSeoOptions): string {\n return joinUrl(opts.siteUrl, opts.basePath ?? \"/blog\", post.slug);\n}\n\nfunction ogImages(post: BlogPostContent): BlogPostMetadataImage[] | undefined {\n if (post.seo.og_image) return [{ url: post.seo.og_image }];\n const hero = post.images.find((image) => image.role === \"hero\");\n if (!hero) return undefined;\n return [{ url: hero.url, width: hero.width, height: hero.height, alt: hero.alt }];\n}\n\n/** Build a Next `Metadata`-shaped object for a post's `generateMetadata`. */\nexport function buildBlogPostMetadata(\n post: BlogPostContent,\n opts: BlogSeoOptions,\n): BlogPostMetadata {\n const url = canonicalUrl(post, opts);\n const title = post.seo.meta_title || post.title;\n const description = post.seo.meta_description || post.description;\n const images = ogImages(post);\n return {\n title,\n description,\n alternates: { canonical: url },\n openGraph: {\n title,\n description,\n type: \"article\",\n url,\n ...(images ? { images } : {}),\n ...(post.published_at ? { publishedTime: post.published_at } : {}),\n },\n };\n}\n\n/**\n * Build a `BlogPosting` JSON-LD object. When the backend shipped a non-empty\n * `post.jsonld`, its values are spread over the locally derived defaults so\n * backend-authored enrichment (author, keywords, …) wins — EXCEPT the pinned\n * fields spread after it: `@context`/`@type` (type identity of the emitted\n * document) and `url`/`mainEntityOfPage` (client-domain truths derived from\n * the site's own routing) can never be overridden by backend data.\n */\nexport function buildBlogPostingJsonLd(\n post: BlogPostContent,\n opts: BlogJsonLdOptions,\n): Record<string, unknown> {\n const url = canonicalUrl(post, opts);\n const images = post.images.map((image) => image.url);\n const doc: Record<string, unknown> = {\n headline: post.title,\n description: post.seo.meta_description || post.description,\n datePublished: post.published_at,\n dateModified: post.updated_at ?? post.published_at,\n ...(images.length > 0 ? { image: images } : {}),\n ...(opts.publisherName\n ? { publisher: { \"@type\": \"Organization\", name: opts.publisherName } }\n : {}),\n };\n const backend = post.jsonld;\n const merged =\n backend && typeof backend === \"object\" && Object.keys(backend).length > 0\n ? { ...doc, ...backend }\n : doc;\n return {\n ...merged,\n \"@context\": \"https://schema.org\",\n \"@type\": \"BlogPosting\",\n url,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": url },\n };\n}\n\nexport interface BlogFaqPair {\n question: string;\n answer: string;\n}\n\n// `<FaqItem question=\"…\">…</FaqItem>` (or '…') with the answer captured lazily.\nconst FAQ_ITEM_RE = /<FaqItem\\s+question=(?:\"([^\"]*)\"|'([^']*)')\\s*>([\\s\\S]*?)<\\/FaqItem>/g;\n// Fenced code blocks — FaqItem text inside a fence is code, not an FAQ.\nconst CODE_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[^\\n]*\\n[\\s\\S]*?(?:^ {0,3}\\1`* *$|(?![\\s\\S]))/gm;\n// Component tags + inline code markers stripped from answer prose.\nconst ANSWER_TAG_RE = /<[^>]*>/g;\n\n/**\n * Extract `<FaqItem question>` question/answer pairs from a post's MDX.\n *\n * Deliberately a light regex, not a parser: it runs on documents that have\n * ALREADY passed `validateBlogMdx` at both the persist and render boundaries,\n * where `<FaqItem>` is guaranteed to carry exactly one quoted-string\n * `question` attribute and flow-markdown children. On anything else it simply\n * extracts fewer/no pairs — it never renders content, so there is no\n * injection surface. Fenced code blocks are dropped first so documentation\n * ABOUT the component is not read as an FAQ. Answers are flattened to plain\n * text (tags stripped, markdown markers left as-is, whitespace collapsed) —\n * Google accepts plain-text `acceptedAnswer.text`.\n */\nexport function extractFaqPairs(mdx: string): BlogFaqPair[] {\n const withoutCode = mdx.replaceAll(CODE_FENCE_RE, \"\");\n const pairs: BlogFaqPair[] = [];\n for (const match of withoutCode.matchAll(FAQ_ITEM_RE)) {\n const question = (match[1] ?? match[2] ?? \"\").trim();\n const answer = match[3].replaceAll(ANSWER_TAG_RE, \" \").replaceAll(/\\s+/g, \" \").trim();\n if (question && answer) pairs.push({ question, answer });\n }\n return pairs;\n}\n\n/**\n * Build a schema.org `FAQPage` JSON-LD object from extracted FAQ pairs\n * (see {@link extractFaqPairs}). Returns `null` when there are no pairs —\n * an empty `FAQPage` is invalid structured data, so callers can gate the\n * `<script type=\"application/ld+json\">` emission on the return value.\n */\nexport function buildFaqJsonLd(pairs: BlogFaqPair[]): Record<string, unknown> | null {\n if (pairs.length === 0) return null;\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: pairs.map((pair) => ({\n \"@type\": \"Question\",\n name: pair.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: pair.answer },\n })),\n };\n}\n\nconst JSONLD_ESCAPES: Record<string, string> = {\n \"<\": \"\\\\u003c\",\n \">\": \"\\\\u003e\",\n \"&\": \"\\\\u0026\",\n \"\\u2028\": \"\\\\u2028\",\n \"\\u2029\": \"\\\\u2029\",\n};\n\n/**\n * Serialize a JSON-LD document for embedding in a\n * `<script type=\"application/ld+json\">` tag. Escapes `<`, `>`, `&` and the\n * U+2028/U+2029 line separators as `\\uXXXX` sequences (valid JSON, identical\n * parse result) so backend-influenced content can never break out of the\n * script context (`</script>` injection) or produce invalid JS source.\n */\nexport function serializeJsonLd(data: Record<string, unknown>): string {\n return JSON.stringify(data).replaceAll(/[<>&\\u2028\\u2029]/g, (ch) => JSONLD_ESCAPES[ch] ?? ch);\n}\n\n/** Sitemap rows for every published post (shape matches Next's `MetadataRoute.Sitemap`). */\nexport function buildBlogSitemapEntries(\n posts: BlogManifestEntry[],\n opts: BlogSeoOptions,\n): BlogSitemapEntry[] {\n return posts.map((post) => ({\n url: joinUrl(opts.siteUrl, opts.basePath ?? \"/blog\", post.slug),\n lastModified: post.updated_at,\n }));\n}\n","// React-free table-of-contents + reading-time helpers for blog MDX. Pure mdast\n// walk over the SAME front-end the validator uses (remark-parse + remark-mdx),\n// so heading extraction can never disagree with what the renderer compiles.\n//\n// Slug parity contract: ids are produced with a fresh `GithubSlugger` per\n// document, walked in document order over h2/h3 — EXACTLY what `rehype-slug`\n// does at render time in `BlogPostView`... almost. rehype-slug slugs EVERY\n// heading rank (h1–h6), and duplicate-slug `-N` suffixes are allocated from\n// that full sequence. To keep extracted ids byte-identical to rendered ids,\n// the walk below feeds every heading through the slugger but only REPORTS\n// h2/h3 (the ranks a table of contents links to). Verified by the corpus\n// parity test in `__tests__/toc.test.ts`.\n\nimport GithubSlugger from \"github-slugger\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\n\n/** One table-of-contents entry: an h2/h3 with its rendered anchor id. */\nexport interface BlogHeading {\n /** Heading rank — a TOC only links section (h2) and subsection (h3) heads. */\n depth: 2 | 3;\n /** Plain-text heading content (formatting stripped, code/emphasis flattened). */\n text: string;\n /** The `id` rehype-slug stamps on the rendered heading element. */\n id: string;\n}\n\n// Minimal structural view of the mdast nodes we walk (same approach as\n// validate-mdx.ts — keeps the public d.ts free of @types/mdast).\ninterface MdastNode {\n type: string;\n depth?: number;\n value?: string;\n children?: MdastNode[];\n}\n\n/** Textual leaf types whose `value` contributes to a heading's visible text. */\nconst TEXT_LEAF_TYPES = new Set([\"text\", \"inlineCode\"]);\n\nfunction headingText(node: MdastNode): string {\n let text = \"\";\n const stack: MdastNode[] = [];\n const roots = node.children ?? [];\n for (let i = roots.length - 1; i >= 0; i--) {\n stack.push(roots[i]);\n }\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current) break;\n if (TEXT_LEAF_TYPES.has(current.type) && typeof current.value === \"string\") {\n text += current.value;\n }\n if (Array.isArray(current.children)) {\n for (let i = current.children.length - 1; i >= 0; i--) {\n stack.push(current.children[i]);\n }\n }\n }\n return text;\n}\n\n/**\n * Extract the h2/h3 outline of a blog MDX document with the exact anchor ids\n * `BlogPostView` renders (rehype-slug parity — see the module header). Returns\n * `[]` for unparseable input; feed it the same `mdx` the view receives.\n */\nexport function extractBlogHeadings(mdx: string): BlogHeading[] {\n let tree: MdastNode;\n try {\n tree = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMdx)\n .parse(mdx) as unknown as MdastNode;\n } catch {\n return [];\n }\n\n const slugger = new GithubSlugger();\n const headings: BlogHeading[] = [];\n\n // Document-order walk. Headings are flow content — they appear at the root\n // or inside containers (blockquote, list, Callout) — and rehype-slug slugs\n // them wherever they are, so the walk must descend everywhere.\n const stack: MdastNode[] = [tree];\n while (stack.length > 0) {\n const node = stack.pop();\n if (!node) break;\n if (node.type === \"heading\" && typeof node.depth === \"number\") {\n const text = headingText(node);\n // Every rank consumes a slug (dedupe-counter parity with rehype-slug),\n // but only h2/h3 land in the TOC.\n const id = slugger.slug(text);\n if (node.depth === 2 || node.depth === 3) {\n headings.push({ depth: node.depth, text, id });\n }\n }\n if (Array.isArray(node.children)) {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n return headings;\n}\n\n/** Average adult reading speed used for the estimate, in words per minute. */\nconst WORDS_PER_MINUTE = 220;\n\n// Crude MDX-syntax strip for word counting: JSX tags, code fences/backticks,\n// heading/list/quote markers, link targets. Word-count precision beyond this\n// is noise at a \"N min read\" granularity.\nconst MDX_SYNTAX_RE = /<[^>]*>|```[^`]*```|`|^[#>\\-*+\\s]+|\\]\\([^)]*\\)|[[\\]!]/gm;\n\n/**\n * Estimated reading time of a blog MDX document, in whole minutes (>= 1).\n * Deliberately crude — strips MDX/markdown syntax and counts words at\n * {@link WORDS_PER_MINUTE}.\n */\nexport function estimateReadingTime(mdx: string): number {\n const words = mdx\n .replaceAll(MDX_SYNTAX_RE, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 0).length;\n return Math.max(1, Math.round(words / WORDS_PER_MINUTE));\n}\n","// Mechanical MDX grammar enforcement for generated blog posts — Layer 1 (the\n// authoritative render boundary). Pure and framework-free: no React, no\n// next-mdx-remote. `BlogPostView` runs this unconditionally BEFORE compileMDX;\n// the API mirrors the same grammar at persist time\n// (apps/api/src/services/blog_mdx_validation.py).\n//\n// Why parse here at all: next-mdx-remote v6 strips expression *evaluation*,\n// but intrinsic JSX/HTML elements (<script src>, <iframe>, event-handler\n// attributes, javascript: URLs) flow straight into served HTML — stored XSS\n// on customer domains. The component allow-list passed to the renderer is not\n// a boundary (intrinsics bypass it), and prompt rules are only instructions.\n//\n// No parser differential by construction: we parse with the SAME mdast\n// front-end @mdx-js/mdx uses internally (remark-parse + remark-mdx, versions\n// aligned with what next-mdx-remote resolves), so whatever the compiler would\n// see, this validator saw first. The grammar is default-deny: any node type\n// not explicitly whitelisted rejects the document.\n\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\n\nexport interface BlogMdxViolation {\n /** Stable machine-readable rule id (safe to surface — never raw content). */\n rule: string;\n message: string;\n line?: number;\n}\n\nexport type BlogMdxValidationResult = { ok: true } | { ok: false; violations: BlogMdxViolation[] };\n\n/** DoS guard: maximum accepted MDX source size, in UTF-16 code units. */\nexport const BLOG_MDX_MAX_SIZE = 500_000;\n/** DoS guard: maximum AST nesting depth (realistic posts stay under ~10). */\nexport const BLOG_MDX_MAX_DEPTH = 64;\n/** DoS guard: maximum total AST node count. */\nexport const BLOG_MDX_MAX_NODES = 100_000;\n/** Cap on reported violations so a hostile document can't flood the caller. */\nconst MAX_VIOLATIONS = 50;\n\n/**\n * One allowed component of the blog MDX grammar, in serializable form.\n * `attrs` is the EXACT attribute set — every attribute is required, nothing\n * else allowed; `selfClosing` components must have no children.\n *\n * `cssClass`/`promptUsage`/`promptDescription` exist SOLELY so this one\n * record can be the entire cross-language/cross-surface registry — see the\n * grammar doc comment below for what derives from each field.\n */\nexport interface BlogMdxComponentGrammarEntry {\n name: string;\n attrs: readonly string[];\n urlAttrs: readonly string[];\n selfClosing: boolean;\n /** Base CSS class the default renderer applies, when the component is styled. */\n cssClass?: string;\n /** Example JSX usage shown to the drafting/editing agents. */\n promptUsage: string;\n /** One-line description of when/how to use it, shown to the agents. */\n promptDescription: string;\n}\n\n/**\n * The component grammar — the SINGLE source of truth for the closed\n * component set (name/attrs/urlAttrs/selfClosing/cssClass/prompt copy). This\n * is the only place to touch when adding, removing, or changing a component;\n * everything else derives from it instead of hand-mirroring it:\n *\n * - the MDX validator specs below (grammar enforcement),\n * - the `BlogComponents` renderer contract (a mapped-type assertion in\n * `view/components.tsx` makes an entry without a renderer a compile error),\n * - the `resources.blog_mdx_grammar` snapshot in `events.schema.json`\n * (emitted verbatim by `src/events/build-schema.ts`), which\n * `apps/api/src/services/blog_mdx_components.py` is GENERATED from\n * (`apps/api/scripts/generate_blog_mdx_grammar.py`) — the backend has no\n * hand-typed mirror to drift out of sync,\n * - the agent prompt templates (`blog/_mdx_grammar.j2`, via the generated\n * Python module's `prompt_usage`/`prompt_description` fields),\n * - `styles.css` (a completeness test asserts every `cssClass` here has a\n * rule), and the MDX conformance corpus (a completeness test asserts every\n * `name` here has at least one accept-case fixture).\n */\nexport const BLOG_MDX_COMPONENT_GRAMMAR = [\n {\n name: \"Callout\",\n attrs: [],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-callout\",\n promptUsage: \"<Callout>...</Callout>\",\n promptDescription: \"a highlighted tip/note block; no attributes.\",\n },\n {\n name: \"Cta\",\n attrs: [\"href\", \"label\"],\n urlAttrs: [\"href\"],\n selfClosing: true,\n cssClass: \"aranova-blog-cta\",\n promptUsage: '<Cta href=\"...\" label=\"...\" />',\n promptDescription: \"a call-to-action button (self-closing).\",\n },\n {\n name: \"BlogImage\",\n attrs: [\"id\"],\n urlAttrs: [],\n selfClosing: true,\n promptUsage: '<BlogImage id=\"...\" />',\n promptDescription:\n \"a generated image placeholder (self-closing). The `id` references an \" +\n \"image brief id from the plan; the renderer resolves it.\",\n },\n {\n name: \"FaqItem\",\n attrs: [\"question\"],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-faq-item\",\n promptUsage: '<FaqItem question=\"...\">...</FaqItem>',\n promptDescription:\n \"one FAQ entry: the required `question` attribute carries the question, \" +\n \"the children are the prose answer. Group consecutive `<FaqItem>`s \" +\n \"under an FAQ heading near the end of the post when the topic suits a Q&A section.\",\n },\n {\n name: \"KeyTakeaways\",\n attrs: [],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-key-takeaways\",\n promptUsage: \"<KeyTakeaways>...</KeyTakeaways>\",\n promptDescription:\n \"a TL;DR box containing a standard markdown bullet list of 3-5 takeaways; \" +\n \"place it right after the H1 (and hero image).\",\n },\n] as const satisfies readonly BlogMdxComponentGrammarEntry[];\n\n/** Union of the allowed component names, derived from the grammar. */\nexport type BlogMdxComponentName = (typeof BLOG_MDX_COMPONENT_GRAMMAR)[number][\"name\"];\n\n/** The closed component set of the blog grammar (mirrors the agent prompts). */\nexport const BLOG_MDX_ALLOWED_COMPONENTS: readonly BlogMdxComponentName[] =\n BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => entry.name);\n\n/**\n * Standard-markdown mdast node types the grammar accepts. Everything else —\n * including `html`, the MDX expression/ESM node types, and any type added by\n * a future plugin — is rejected by default.\n */\nconst ALLOWED_NODE_TYPES = new Set([\n \"root\",\n \"heading\",\n \"paragraph\",\n \"text\",\n \"emphasis\",\n \"strong\",\n \"delete\",\n \"link\",\n \"image\",\n \"linkReference\",\n \"imageReference\",\n \"definition\",\n \"list\",\n \"listItem\",\n \"blockquote\",\n \"code\",\n \"inlineCode\",\n \"table\",\n \"tableRow\",\n \"tableCell\",\n \"thematicBreak\",\n \"break\",\n]);\n\n/** Named rejections for the node types that are the actual attack surface. */\nconst FORBIDDEN_NODE_TYPES: Record<string, { rule: string; message: string }> = {\n html: { rule: \"raw-html\", message: \"Raw HTML is not allowed\" },\n mdxjsEsm: { rule: \"esm\", message: \"import/export statements are not allowed\" },\n mdxFlowExpression: { rule: \"expression\", message: \"JSX expressions ({…}) are not allowed\" },\n mdxTextExpression: { rule: \"expression\", message: \"JSX expressions ({…}) are not allowed\" },\n};\n\ninterface ComponentSpec {\n /** Exact attribute set — every attribute is required, nothing else allowed. */\n attrs: readonly string[];\n /** Attributes whose (string) value must pass the URL policy. */\n urlAttrs: readonly string[];\n /** Self-closing components must have no children. */\n allowChildren: boolean;\n}\n\nconst COMPONENT_SPECS: Record<string, ComponentSpec> = Object.fromEntries(\n BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => [\n entry.name,\n { attrs: entry.attrs, urlAttrs: entry.urlAttrs, allowChildren: !entry.selfClosing },\n ]),\n);\n\nconst URL_SCHEME_RE = /^([A-Za-z][A-Za-z0-9+.-]*):/;\nconst ALLOWED_URL_SCHEMES = new Set([\"http\", \"https\", \"mailto\"]);\n\n/**\n * Scheme whitelist: http(s)/mailto/relative (schemeless) only. ASCII control\n * characters and whitespace are stripped BEFORE scheme detection — browsers\n * ignore them when parsing URLs, so `java\\tscript:` must not slip through.\n * Returns the offending scheme, or null when the URL is acceptable.\n */\nfunction forbiddenUrlScheme(rawUrl: string): string | null {\n // Intentional control-character class — the whole point is stripping the\n // characters browsers ignore in URLs before the scheme check.\n // eslint-disable-next-line no-control-regex\n const cleaned = rawUrl.replaceAll(/[\\u0000-\\u0020\\u007F]/g, \"\");\n const match = URL_SCHEME_RE.exec(cleaned);\n if (!match) return null; // relative / fragment / protocol-relative\n const scheme = match[1].toLowerCase();\n return ALLOWED_URL_SCHEMES.has(scheme) ? null : scheme;\n}\n\n// Loose structural view of the mdast/MDX node shapes we inspect. Kept local\n// (not the full @types/mdast surface) so the public d.ts stays dependency-free.\ninterface MdxAstAttribute {\n type: string; // \"mdxJsxAttribute\" | \"mdxJsxExpressionAttribute\"\n name?: string;\n value?: unknown; // string | null | { type: \"mdxJsxAttributeValueExpression\" }\n}\n\ninterface MdxAstNode {\n type: string;\n children?: MdxAstNode[];\n url?: string;\n name?: string | null;\n attributes?: MdxAstAttribute[];\n position?: { start?: { line?: number } };\n depth?: number;\n}\n\nfunction nodeLine(node: MdxAstNode): number | undefined {\n return node.position?.start?.line;\n}\n\nfunction checkComponent(node: MdxAstNode, violations: BlogMdxViolation[]): void {\n const line = nodeLine(node);\n const name = node.name;\n\n if (!name) {\n violations.push({\n rule: \"unknown-component\",\n message: \"JSX fragments (<>…</>) are not allowed\",\n line,\n });\n return;\n }\n\n const spec = COMPONENT_SPECS[name];\n if (!spec) {\n violations.push({\n rule: \"unknown-component\",\n message: `<${name}> is not an allowed element (allowed: ${BLOG_MDX_ALLOWED_COMPONENTS.join(\", \")})`,\n line,\n });\n return;\n }\n\n const seen = new Set<string>();\n for (const attr of node.attributes ?? []) {\n if (attr.type !== \"mdxJsxAttribute\" || typeof attr.name !== \"string\") {\n // Spread attributes ({...props}) or anything unrecognized.\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> has a non-literal attribute`,\n line,\n });\n continue;\n }\n if (!spec.attrs.includes(attr.name)) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> does not accept attribute \"${attr.name}\"`,\n line,\n });\n continue;\n }\n if (typeof attr.value !== \"string\") {\n // null = bare attribute; object = {expression} value.\n violations.push({\n rule: \"component-attribute\",\n message: `<${name} ${attr.name}> must be a quoted string literal`,\n line,\n });\n continue;\n }\n seen.add(attr.name);\n if (spec.urlAttrs.includes(attr.name)) {\n const scheme = forbiddenUrlScheme(attr.value);\n if (scheme !== null) {\n violations.push({\n rule: \"url-scheme\",\n message: `<${name} ${attr.name}> uses forbidden URL scheme \"${scheme}:\"`,\n line,\n });\n }\n }\n }\n\n for (const required of spec.attrs) {\n if (!seen.has(required)) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> requires attribute \"${required}\"`,\n line,\n });\n }\n }\n\n if (!spec.allowChildren && (node.children?.length ?? 0) > 0) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> must be self-closing (no children)`,\n line,\n });\n }\n}\n\nexport interface BlogMdxValidateOptions {\n /**\n * Reject any content before the H1 (frontmatter, a stray metadata block,\n * prose) — only meaningful for a whole post. Off by default because the\n * cross-language conformance corpus exercises single components in\n * isolation, not full documents; the render boundary (BlogPostView, the\n * only production caller) always validates a whole post and passes `true`.\n */\n requireLeadingH1?: boolean;\n}\n\n/**\n * Validate a generated blog MDX document against the frozen content grammar:\n * standard markdown + exactly the components in\n * {@link BLOG_MDX_COMPONENT_GRAMMAR} (`<Callout>`, `<Cta href label/>`,\n * `<BlogImage id/>`, `<FaqItem question>`, `<KeyTakeaways>`). Rejects ESM,\n * JSX expressions, raw HTML, every other JSX/HTML element, non-literal\n * attributes, and non-http(s)/mailto/relative URLs. Pure and synchronous —\n * safe anywhere (RSC, node, edge).\n */\nexport function validateBlogMdx(\n mdx: string,\n options?: BlogMdxValidateOptions,\n): BlogMdxValidationResult {\n const violations: BlogMdxViolation[] = [];\n\n if (typeof mdx !== \"string\" || mdx.length > BLOG_MDX_MAX_SIZE) {\n return {\n ok: false,\n violations: [\n {\n rule: \"size-limit\",\n message: `Document exceeds the ${BLOG_MDX_MAX_SIZE}-character limit`,\n },\n ],\n };\n }\n\n let tree: MdxAstNode;\n try {\n tree = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMdx)\n .parse(mdx) as unknown as MdxAstNode;\n } catch (error) {\n const line = (error as { line?: number }).line;\n return {\n ok: false,\n violations: [\n {\n rule: \"parse-error\",\n message: \"Document is not valid MDX\",\n ...(typeof line === \"number\" ? { line } : {}),\n },\n ],\n };\n }\n\n if (options?.requireLeadingH1) {\n const firstChild = tree.children?.[0];\n if (!firstChild || firstChild.type !== \"heading\" || firstChild.depth !== 1) {\n violations.push({\n rule: \"leading-h1\",\n message:\n \"Document must start with the H1 title (`# ...`) — no frontmatter or metadata block before it\",\n line: nodeLine(firstChild ?? tree),\n });\n }\n }\n\n // Iterative default-deny walk with explicit depth tracking (DoS guard —\n // never recurse on attacker-shaped input).\n let nodeCount = 0;\n const stack: Array<{ node: MdxAstNode; depth: number }> = [{ node: tree, depth: 0 }];\n\n while (stack.length > 0 && violations.length < MAX_VIOLATIONS) {\n const item = stack.pop();\n if (!item) break;\n const { node, depth } = item;\n\n nodeCount += 1;\n if (nodeCount > BLOG_MDX_MAX_NODES) {\n violations.push({ rule: \"size-limit\", message: \"Document has too many nodes\" });\n break;\n }\n if (depth > BLOG_MDX_MAX_DEPTH) {\n violations.push({ rule: \"depth-limit\", message: \"Document nests too deeply\" });\n break;\n }\n\n const forbidden = FORBIDDEN_NODE_TYPES[node.type];\n if (forbidden) {\n violations.push({ ...forbidden, line: nodeLine(node) });\n continue; // never descend into forbidden subtrees\n }\n\n if (node.type === \"mdxJsxFlowElement\" || node.type === \"mdxJsxTextElement\") {\n checkComponent(node, violations);\n // Descend regardless: Callout children are flow content that must obey\n // the same grammar, and even rejected subtrees should report every rule\n // they break (useful pipeline feedback; the doc is already invalid).\n } else if (!ALLOWED_NODE_TYPES.has(node.type)) {\n violations.push({\n rule: \"disallowed-node\",\n message: `Markdown construct \"${node.type}\" is not allowed`,\n line: nodeLine(node),\n });\n continue;\n }\n\n if (\n (node.type === \"link\" || node.type === \"image\" || node.type === \"definition\") &&\n typeof node.url === \"string\"\n ) {\n const scheme = forbiddenUrlScheme(node.url);\n if (scheme !== null) {\n violations.push({\n rule: \"url-scheme\",\n message: `${node.type} uses forbidden URL scheme \"${scheme}:\"`,\n line: nodeLine(node),\n });\n }\n }\n\n if (Array.isArray(node.children)) {\n for (const child of node.children) {\n stack.push({ node: child, depth: depth + 1 });\n }\n }\n }\n\n return violations.length === 0 ? { ok: true } : { ok: false, violations };\n}\n","// Async React Server Component — no 'use client'. Rendered on the server (or\n// at build time) by Next.js App Router pages and by the internal dashboard's\n// draft preview. Never ships MDX compilation to the browser.\n\nimport { compileMDX } from \"next-mdx-remote/rsc\";\nimport rehypeSlug from \"rehype-slug\";\nimport remarkGfm from \"remark-gfm\";\nimport type { CSSProperties, ReactNode } from \"react\";\n\nimport { CAPABILITY_DOM_ATTRIBUTE } from \"../../../capabilities\";\nimport { BASE_BLOG_THEME, blogThemeToCssVars, type BlogTheme } from \"../theme\";\nimport { validateBlogMdx } from \"../validate-mdx\";\nimport { type BlogClassNameOverrides, joinClassNames } from \"./class-names\";\nimport { createDefaultComponents, type BlogComponents } from \"./components\";\nimport { BLOG_BASE_CSS } from \"./styles\";\n\n// Blog rendering happens entirely server-side against the CDN, so it never\n// calls the Aranova API and neither the ingest payload nor the request-header\n// path can observe it. Stamping the rendered article is how the capability\n// crosses to the browser, where the tracking client picks it up on its next\n// batch. Applied to the error paths too: a failed post still proves the\n// renderer is wired up.\nconst CAPABILITY_MARKER = { [CAPABILITY_DOM_ATTRIBUTE]: \"blog_rendering\" } as const;\n\nexport interface BlogPostViewProps {\n /** Raw MDX source (the `mdx` field of the post content JSON). */\n mdx: string;\n /** A fully-RESOLVED theme (see `resolveBlogTheme`). Default: `BASE_BLOG_THEME`. */\n theme?: BlogTheme;\n /** Overrides merged over the default component set. */\n components?: Partial<BlogComponents>;\n /** Article chrome rendered above the content/aside layout (breadcrumbs, meta, …). */\n header?: ReactNode;\n /** Sidebar chrome (e.g. `BlogTocShell`); omitted entirely when not provided. */\n aside?: ReactNode;\n /** Entrance-motion opt-in, stamped as `data-motion` for the base CSS. Default `\"none\"`. */\n motion?: \"none\" | \"subtle\";\n /** Site-supplied classes merged onto the fixed base classes (additive, per surface). */\n classNames?: BlogClassNameOverrides;\n /**\n * Reject content before the H1 (frontmatter, a stray metadata block, prose)\n * — only meaningful for a whole post. Default `true`; direct-fragment\n * callers (component-grammar tests) pass `false`.\n */\n requireLeadingH1?: boolean;\n /** Which side the aside rail sits on at desktop width. Default `\"right\"`. */\n tocSide?: \"left\" | \"right\";\n}\n\n/**\n * Renders MDX inside a themed `<article class=\"aranova-blog-post\">`. The theme\n * flows in as sanitized `--ara-blog-*` CSS variables on the article element;\n * `BLOG_BASE_CSS` (a static constant — nothing interpolated) consumes them.\n *\n * Structure: `{header}` chrome first, then a `.aranova-blog-layout` holding the\n * `.aranova-blog-content` column and (when provided) an `.aranova-blog-aside`\n * rail — the base CSS turns the layout into a sticky-aside grid on wide\n * viewports and keeps it a plain block on mobile.\n *\n * MDX is compiled via `compileMDX` — the awaitable form of `<MDXRemote/>` from\n * `next-mdx-remote/rsc` — because an element-level `<MDXRemote/>` compiles\n * during React's render, where a malformed document would throw past any\n * try/catch here. `rehype-slug` stamps GitHub-style anchor ids on every\n * heading (slug parity with `extractBlogHeadings` in ../toc.ts). On compile\n * failure a typed fallback article renders instead; this component never\n * throws into the host page.\n */\nexport async function BlogPostView({\n mdx,\n theme = BASE_BLOG_THEME,\n components,\n header,\n aside,\n motion = \"none\",\n classNames,\n requireLeadingH1 = true,\n tocSide = \"right\",\n}: BlogPostViewProps): Promise<ReactNode> {\n // Custom-property keys (\"--ara-blog-*\") are valid at runtime but not part of\n // the typed CSSProperties surface — a shape cast (not `any`) bridges that.\n const style = blogThemeToCssVars(theme) as CSSProperties;\n // Shadowed entirely by `components` when called via BlogPost (already a\n // complete, image-aware set) — only matters for a direct BlogPostView call.\n const merged = { ...createDefaultComponents([], classNames), ...components };\n\n // Unconditional grammar enforcement BEFORE any MDX compilation — the\n // component allow-list is not a security boundary (intrinsic <script>/\n // <iframe>/… render through it), so a document that violates the frozen\n // blog grammar never reaches compileMDX. Only rule names are surfaced;\n // the raw content is never echoed into the fallback.\n const validation = validateBlogMdx(mdx, { requireLeadingH1 });\n if (!validation.ok) {\n const rules = Array.from(new Set(validation.violations.map((v) => v.rule))).join(\", \");\n return (\n <article\n className={joinClassNames(\"aranova-blog-post\", classNames?.article)}\n data-error=\"mdx-validation-failed\"\n style={style}\n {...CAPABILITY_MARKER}\n >\n <style>{BLOG_BASE_CSS}</style>\n <p>This post could not be displayed. (blocked: {rules})</p>\n </article>\n );\n }\n\n let content: ReactNode;\n try {\n const compiled = await compileMDX({\n source: mdx,\n components: merged,\n options: { mdxOptions: { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSlug] } },\n });\n content = compiled.content;\n } catch {\n return (\n <article\n className={joinClassNames(\"aranova-blog-post\", classNames?.article)}\n data-error=\"mdx-render-failed\"\n style={style}\n {...CAPABILITY_MARKER}\n >\n <style>{BLOG_BASE_CSS}</style>\n <p>This post could not be displayed.</p>\n </article>\n );\n }\n\n return (\n <article\n className={joinClassNames(\"aranova-blog-post\", classNames?.article)}\n data-motion={motion}\n style={style}\n {...CAPABILITY_MARKER}\n >\n <style>{BLOG_BASE_CSS}</style>\n {header}\n <div\n className={joinClassNames(\"aranova-blog-layout\", classNames?.layout)}\n data-toc-side={tocSide}\n >\n <div className={joinClassNames(\"aranova-blog-content\", classNames?.content)}>{content}</div>\n {aside ? (\n <aside className={joinClassNames(\"aranova-blog-aside\", classNames?.aside)}>{aside}</aside>\n ) : null}\n </div>\n </article>\n );\n}\n","// Server-only MDX component set for the blog post view. No 'use client' — these\n// are plain (server-renderable) components; interactivity is deliberately out of\n// scope for published blog content.\n\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\n\nimport type { BlogImage } from \"../schema\";\nimport type { BlogMdxComponentName } from \"../validate-mdx\";\nimport { type BlogClassNameOverrides, joinClassNames } from \"./class-names\";\n\n/**\n * The CLOSED component interface the MDX renderer accepts. Generated MDX may\n * only reference these elements/components; anything else fails MDX rendering\n * (caught by the view's fallback). Callers may override any subset via the\n * `components` prop on `BlogPost` / `BlogPostView`.\n */\nexport interface BlogComponents {\n h1: (props: ComponentPropsWithoutRef<\"h1\">) => ReactNode;\n h2: (props: ComponentPropsWithoutRef<\"h2\">) => ReactNode;\n h3: (props: ComponentPropsWithoutRef<\"h3\">) => ReactNode;\n h4: (props: ComponentPropsWithoutRef<\"h4\">) => ReactNode;\n h5: (props: ComponentPropsWithoutRef<\"h5\">) => ReactNode;\n h6: (props: ComponentPropsWithoutRef<\"h6\">) => ReactNode;\n p: (props: ComponentPropsWithoutRef<\"p\">) => ReactNode;\n a: (props: ComponentPropsWithoutRef<\"a\">) => ReactNode;\n img: (props: ComponentPropsWithoutRef<\"img\">) => ReactNode;\n pre: (props: ComponentPropsWithoutRef<\"pre\">) => ReactNode;\n code: (props: ComponentPropsWithoutRef<\"code\">) => ReactNode;\n blockquote: (props: ComponentPropsWithoutRef<\"blockquote\">) => ReactNode;\n ul: (props: ComponentPropsWithoutRef<\"ul\">) => ReactNode;\n ol: (props: ComponentPropsWithoutRef<\"ol\">) => ReactNode;\n li: (props: ComponentPropsWithoutRef<\"li\">) => ReactNode;\n table: (props: ComponentPropsWithoutRef<\"table\">) => ReactNode;\n hr: (props: ComponentPropsWithoutRef<\"hr\">) => ReactNode;\n /** Styled aside for tips/notes: `<Callout>…</Callout>` in MDX. */\n Callout: (props: { children?: ReactNode }) => ReactNode;\n /** Primary-colored button-styled anchor: `<Cta href label/>` in MDX. */\n Cta: (props: { href: string; label: string }) => ReactNode;\n /** Renders an image from the post's image records by id: `<BlogImage id/>`. */\n BlogImage: (props: { id: string }) => ReactNode;\n /** One FAQ entry (question heading + prose answer children): `<FaqItem question>…</FaqItem>`. */\n FaqItem: (props: { question: string; children?: ReactNode }) => ReactNode;\n /** Styled TL;DR box holding a markdown bullet list: `<KeyTakeaways>…</KeyTakeaways>`. */\n KeyTakeaways: (props: { children?: ReactNode }) => ReactNode;\n}\n\n/**\n * Compile-time tie between the MDX component grammar and the renderer set:\n * indexing {@link BlogComponents} with every grammar component name fails to\n * typecheck if a component is added to `BLOG_MDX_COMPONENT_GRAMMAR` without a\n * corresponding renderer key here (spec-without-renderer = compile error).\n */\nexport type BlogGrammarComponentRenderers = {\n [Name in BlogMdxComponentName]: BlogComponents[Name];\n};\n\n/**\n * Default renderers. `images` is closed over so `<BlogImage id/>` in MDX can\n * resolve the post's image records without threading props through MDX.\n *\n * Images render as plain `<img>` (with intrinsic width/height + lazy loading),\n * NOT `next/image` — tracking-core is framework-free, and the CDN host would\n * otherwise need to be whitelisted in every consumer's `next.config`. A Next\n * site wanting `next/image` can override `img`/`BlogImage` via `components`.\n *\n * `classNames` merges a site-supplied class onto the fixed base class of each\n * styleable component (see {@link BlogClassNameOverrides}) — additive, never\n * a replacement of the `.aranova-blog-*` selector.\n */\nexport function createDefaultComponents(\n images: BlogImage[],\n classNames?: BlogClassNameOverrides,\n): BlogComponents {\n const imagesById = new Map(images.map((image) => [image.id, image]));\n\n return {\n // `children` is destructured explicitly so static a11y analysis\n // (heading-has-content, anchor-has-content) can see the content flow.\n h1: ({ children, ...props }) => <h1 {...props}>{children}</h1>,\n h2: ({ children, ...props }) => <h2 {...props}>{children}</h2>,\n h3: ({ children, ...props }) => <h3 {...props}>{children}</h3>,\n h4: ({ children, ...props }) => <h4 {...props}>{children}</h4>,\n h5: ({ children, ...props }) => <h5 {...props}>{children}</h5>,\n h6: ({ children, ...props }) => <h6 {...props}>{children}</h6>,\n p: ({ children, ...props }) => <p {...props}>{children}</p>,\n a: ({ children, ...props }) => <a {...props}>{children}</a>,\n img: ({ alt = \"\", ...props }) => <img loading=\"lazy\" alt={alt} {...props} />,\n pre: ({ children, ...props }) => <pre {...props}>{children}</pre>,\n code: ({ children, ...props }) => <code {...props}>{children}</code>,\n blockquote: ({ children, ...props }) => <blockquote {...props}>{children}</blockquote>,\n ul: ({ children, ...props }) => <ul {...props}>{children}</ul>,\n ol: ({ children, ...props }) => <ol {...props}>{children}</ol>,\n li: ({ children, ...props }) => <li {...props}>{children}</li>,\n // Wrapped so wide tables scroll horizontally inside the article instead of\n // overflowing the host page (the wrapper also carries border + radius).\n table: ({ children, ...props }) => (\n <div className={joinClassNames(\"aranova-blog-table-wrap\", classNames?.tableWrap)}>\n <table {...props}>{children}</table>\n </div>\n ),\n hr: (props) => <hr {...props} />,\n Callout: ({ children }) => (\n <aside className={joinClassNames(\"aranova-blog-callout\", classNames?.callout)}>\n {children}\n </aside>\n ),\n Cta: ({ href, label }) => (\n // `data-aranova-cta` opts this into the tracking SDK's cta_click\n // auto-capture (default selector `[data-aranova-cta]`) with the label as\n // the cta_name — zero site config. Inert if auto-capture is off.\n <a\n className={joinClassNames(\"aranova-blog-cta\", classNames?.cta)}\n href={href}\n data-aranova-cta={label}\n data-aranova-section=\"blog\"\n >\n {label}\n </a>\n ),\n BlogImage: ({ id }) => {\n const image = imagesById.get(id);\n if (!image) return null; // unknown id: degrade silently, never crash the page\n return (\n <figure className={classNames?.figure}>\n <img\n src={image.url}\n alt={image.alt}\n width={image.width}\n height={image.height}\n loading=\"lazy\"\n />\n {image.caption ? <figcaption>{image.caption}</figcaption> : null}\n </figure>\n );\n },\n // Native disclosure accordion — no JS, server-renderable. The h3 inside\n // <summary> is valid HTML and keeps the document outline / a11y heading.\n FaqItem: ({ question, children }) => (\n <details className={joinClassNames(\"aranova-blog-faq-item\", classNames?.faqItem)}>\n <summary className={joinClassNames(\"aranova-blog-faq-question\", classNames?.faqQuestion)}>\n <h3>{question}</h3>\n </summary>\n <div className={joinClassNames(\"aranova-blog-faq-answer\", classNames?.faqAnswer)}>\n {children}\n </div>\n </details>\n ),\n KeyTakeaways: ({ children }) => (\n <aside className={joinClassNames(\"aranova-blog-key-takeaways\", classNames?.keyTakeaways)}>\n {children}\n </aside>\n ),\n };\n}\n","// GENERATED FILE — do not edit. Authored source: styles.css\n// (+ the responsive-token block build-styles.ts generates from theme.ts).\n// Regenerate with `npm run build:css` (runs automatically in `npm run build`).\n\n/**\n * Static, author-controlled stylesheet for the blog post view. Scoped under\n * `.aranova-blog-post` and driven entirely by the `--ara-blog-*` CSS custom\n * properties emitted by `blogThemeToCssVars`.\n *\n * SECURITY: this string is a compile-time constant — NOTHING from post content\n * or theme data is ever interpolated into it. All per-post variation flows\n * through the sanitized CSS variables on the `<article>` element.\n */\nexport const BLOG_BASE_CSS = `/* Aranova blog post base stylesheet — authored source of truth.\n *\n * Scoped under \\`.aranova-blog-post\\` and driven entirely by the sanitized\n * \\`--ara-blog-*\\` CSS custom properties emitted by \\`blogThemeToCssVars\\` on the\n * article element. Host sites usually run Tailwind preflight, so:\n *\n * - element rules use \\`.aranova-blog-post :where(el)\\` — one class of\n * specificity (0,1,0), enough to beat preflight's bare element resets\n * while every rule in this sheet stays flat and order-resolvable;\n * - component classes (\\`.aranova-blog-callout\\`, …) keep their natural\n * class-on-class specificity.\n *\n * Responsive-capable tokens (h1/h2/h3/h4/body/small sizes, body line-height,\n * content max-width, block gap, section gap, radius) are NEVER consumed via their raw\n * \\`--ara-blog-*\\` vars here — the generated block below re-points internal\n * \\`--_*\\` vars per breakpoint, and all rules consume those.\n *\n * Rhythm rules near the end target BOTH \\`.aranova-blog-post\\` (content is a\n * direct child of the article today) and \\`.aranova-blog-content\\` (a later\n * phase wraps content in that div) so that phase needs no CSS changes.\n *\n * No raw colors anywhere: every color flows through the theme vars, which\n * already carry \\`light-dark(...)\\` values when a dark palette is present.\n */\n\n/* Responsive token plumbing — GENERATED by build-styles.ts from theme.ts\n (BLOG_BREAKPOINT_MIN_WIDTHS_PX + the responsive() token set). Mobile-first:\n each breakpoint falls back through every narrower key down to the base var.\n Authored rules consume ONLY the --_* internal vars for these tokens. */\n.aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size);\n --_h2-size: var(--ara-blog-type-h2-size);\n --_h3-size: var(--ara-blog-type-h3-size);\n --_h4-size: var(--ara-blog-type-h4-size);\n --_body-size: var(--ara-blog-type-body-size);\n --_body-line-height: var(--ara-blog-type-body-line-height);\n --_small-size: var(--ara-blog-type-small-size);\n --_content-max-width: var(--ara-blog-space-content-max-width);\n --_block-gap: var(--ara-blog-space-block-gap);\n --_section-gap: var(--ara-blog-space-section-gap);\n --_radius: var(--ara-blog-radius);\n}\n@media (min-width: 640px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size));\n --_h2-size: var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size));\n --_h3-size: var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size));\n --_h4-size: var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size));\n --_body-size: var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size));\n --_body-line-height: var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height));\n --_small-size: var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size));\n --_content-max-width: var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width));\n --_block-gap: var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap));\n --_section-gap: var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap));\n --_radius: var(--ara-blog-radius-sm, var(--ara-blog-radius));\n }\n}\n@media (min-width: 768px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size)));\n --_h2-size: var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size)));\n --_h3-size: var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size)));\n --_h4-size: var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size)));\n --_body-size: var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size)));\n --_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)));\n --_small-size: var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size)));\n --_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)));\n --_block-gap: var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap)));\n --_section-gap: var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap)));\n --_radius: var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius)));\n }\n}\n@media (min-width: 1024px) {\n .aranova-blog-post {\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_radius: var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius))));\n }\n}\n@media (min-width: 1280px) {\n .aranova-blog-post {\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n }\n}\n@media (min-width: 1536px) {\n .aranova-blog-post {\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n }\n}\n@media (min-width: 1920px) {\n .aranova-blog-post {\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n }\n}\n\n.aranova-blog-post {\n /* Internal spacing scale, derived from the responsive block gap. */\n --_flow: var(--_block-gap);\n --_flow-sm: calc(var(--_block-gap) * 0.5);\n --_flow-lg: calc(var(--_block-gap) * 1.75);\n --_flow-section: var(--_section-gap);\n /* Optional heading color token — themes that don't set it fall back to text. */\n --_heading-color: var(--ara-blog-color-heading, var(--ara-blog-color-text));\n\n max-width: var(--_content-max-width);\n margin-inline: auto;\n background: var(--ara-blog-color-background);\n color: var(--ara-blog-color-text);\n font-family: var(--ara-blog-type-body-family);\n font-weight: var(--ara-blog-type-body-weight);\n font-size: var(--_body-size);\n line-height: var(--_body-line-height);\n overflow-wrap: break-word;\n}\n@media (max-width: 639.99px) {\n .aranova-blog-post {\n padding-inline: 1.25rem;\n }\n}\n\n/* --- Headings ------------------------------------------------------------ */\n.aranova-blog-post :where(h1, h2, h3, h4, h5, h6) {\n font-family: var(--ara-blog-type-heading-family);\n font-weight: var(--ara-blog-type-heading-weight);\n line-height: 1.25;\n margin-block: 0;\n color: var(--_heading-color);\n scroll-margin-top: 5rem;\n}\n.aranova-blog-post :where(h1, h2, h3) {\n /* \\`pretty\\` fills each line greedily (only the LAST line is massaged to avoid\n * an orphan word) — so a heading uses the full measure. \\`balance\\` was leaving\n * short first lines. */\n text-wrap: pretty;\n}\n.aranova-blog-post :where(h1) {\n font-size: var(--_h1-size);\n letter-spacing: var(--ara-blog-type-heading-letter-spacing);\n}\n.aranova-blog-post :where(h2) {\n font-size: var(--_h2-size);\n letter-spacing: var(--ara-blog-type-heading-letter-spacing);\n}\n.aranova-blog-post :where(h3) {\n font-size: var(--_h3-size);\n letter-spacing: calc(var(--ara-blog-type-heading-letter-spacing) * 0.5);\n}\n.aranova-blog-post :where(h4) {\n font-size: var(--_h4-size);\n font-weight: 600;\n letter-spacing: calc(var(--ara-blog-type-heading-letter-spacing) * 0.5);\n}\n.aranova-blog-post :where(h5) {\n font-size: var(--_body-size);\n font-weight: 600;\n}\n.aranova-blog-post :where(h6) {\n font-size: var(--_small-size);\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--ara-blog-color-text-muted);\n}\n\n/* --- Text ---------------------------------------------------------------- */\n.aranova-blog-post :where(p) {\n margin-block: 0;\n}\n/* Lead paragraph: the one directly after the post title. */\n.aranova-blog-post :where(h1) + :where(p) {\n font-size: 1.15em;\n color: color-mix(in srgb, var(--ara-blog-color-text) 72%, var(--ara-blog-color-text-muted));\n}\n.aranova-blog-post :where(a) {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n.aranova-blog-post :where(a):focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n/* --- Lists ---------------------------------------------------------------- */\n.aranova-blog-post :where(ul, ol) {\n margin-block: 0;\n padding-inline-start: 1.5em;\n}\n.aranova-blog-post :where(ul) {\n list-style-type: disc;\n}\n.aranova-blog-post :where(ul ul) {\n list-style-type: circle;\n}\n.aranova-blog-post :where(ul ul ul) {\n list-style-type: square;\n}\n.aranova-blog-post :where(ol) {\n list-style-type: decimal;\n}\n.aranova-blog-post :where(ol ol) {\n list-style-type: lower-alpha;\n}\n.aranova-blog-post :where(ul) > li::marker {\n color: var(--ara-blog-color-text-muted);\n}\n.aranova-blog-post :where(ol) > li::marker {\n font-weight: 600;\n color: var(--_heading-color);\n}\n.aranova-blog-post :where(li + li) {\n margin-block-start: 0.35em;\n}\n.aranova-blog-post :where(li > ul, li > ol) {\n margin-block-start: 0.35em;\n}\n\n/* --- Blockquote ------------------------------------------------------------ */\n.aranova-blog-post :where(blockquote) {\n margin: 0;\n padding-block: 0.25em;\n padding-inline: 1.25em 0.5em;\n border-inline-start: 3px solid var(--ara-blog-color-primary);\n color: var(--ara-blog-color-text-muted);\n font-style: italic;\n}\n\n/* --- Code ------------------------------------------------------------------ */\n.aranova-blog-post :where(code) {\n background: var(--ara-blog-color-code-background);\n border-radius: calc(var(--_radius) / 2);\n padding: 0.15em 0.35em;\n font-size: 0.9em;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n.aranova-blog-post :where(pre) {\n margin: 0;\n background: var(--ara-blog-color-code-background);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 1em;\n overflow-x: auto;\n line-height: 1.5;\n}\n.aranova-blog-post :where(pre code) {\n background: none;\n border-radius: 0;\n padding: 0;\n font-size: var(--_small-size);\n}\n\n/* --- Media ------------------------------------------------------------------ */\n.aranova-blog-post :where(img) {\n max-width: 100%;\n height: auto;\n display: block;\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n}\n.aranova-blog-post :where(figure) {\n margin: 0;\n}\n.aranova-blog-post :where(figcaption) {\n margin-block-start: 0.5em;\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n text-align: var(--ara-blog-type-caption-align, center);\n}\n\n/* --- Tables ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-table-wrap {\n overflow-x: auto;\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n}\n.aranova-blog-post :where(table) {\n border-collapse: collapse;\n width: 100%;\n font-size: var(--_small-size);\n}\n.aranova-blog-post :where(th, td) {\n border: 0;\n border-block-end: 1px solid var(--ara-blog-color-border);\n padding: 0.6em 0.85em;\n text-align: left;\n}\n.aranova-blog-post :where(th) {\n background: var(--ara-blog-color-surface);\n border-block-end: 2px solid var(--ara-blog-color-border);\n font-weight: 600;\n}\n.aranova-blog-post :where(tbody tr:nth-child(even) td) {\n background: color-mix(\n in srgb,\n var(--ara-blog-color-surface) 55%,\n var(--ara-blog-color-background)\n );\n}\n.aranova-blog-post :where(tbody tr:last-child) :where(th, td) {\n border-block-end: 0;\n}\n\n/* --- Section break ------------------------------------------------------------ */\n.aranova-blog-post :where(hr) {\n border: none;\n border-top: 2px solid var(--ara-blog-color-border);\n width: 4rem;\n margin-inline: auto;\n}\n\n/* --- Callout ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-callout {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-inline-start: 4px solid var(--ara-blog-color-primary);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-callout > * + * {\n margin-block-start: var(--_flow-sm);\n}\n\n/* --- CTA ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-cta {\n display: inline-block;\n background: var(--ara-blog-color-primary);\n color: var(--ara-blog-color-primary-foreground);\n border-radius: var(--_radius);\n padding: 0.6em 1.2em;\n font-weight: 600;\n text-decoration: none;\n transition:\n filter 150ms ease,\n transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-cta:hover {\n filter: brightness(1.08);\n transform: translateY(-1px);\n}\n.aranova-blog-post .aranova-blog-cta:active {\n filter: none;\n transform: none;\n}\n.aranova-blog-post .aranova-blog-cta:focus-visible {\n outline: 2px solid var(--ara-blog-color-primary);\n outline-offset: 2px;\n}\n\n/* --- FAQ (details/summary accordion) ------------------------------------------ */\n.aranova-blog-post .aranova-blog-faq-item {\n border-block-start: 1px solid var(--ara-blog-color-border);\n padding-block: calc(var(--_flow) * 0.75);\n}\n.aranova-blog-post .aranova-blog-faq-question {\n cursor: pointer;\n list-style: none;\n position: relative;\n padding-inline-end: 1.75em;\n}\n.aranova-blog-post .aranova-blog-faq-question::-webkit-details-marker {\n display: none;\n}\n/* Rotating chevron (border trick — colors via theme vars only). */\n.aranova-blog-post .aranova-blog-faq-question::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-faq-item[open] > .aranova-blog-faq-question::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-faq-question:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n.aranova-blog-post .aranova-blog-faq-answer {\n padding-block-start: var(--_flow-sm);\n}\n.aranova-blog-post .aranova-blog-faq-answer > * + * {\n margin-block-start: var(--_flow-sm);\n}\n/* Progressive open/close animation where the platform supports it. */\n@supports (interpolate-size: allow-keywords) {\n .aranova-blog-post .aranova-blog-faq-item {\n interpolate-size: allow-keywords;\n }\n .aranova-blog-post .aranova-blog-faq-item::details-content {\n block-size: 0;\n overflow-y: clip;\n transition:\n block-size 200ms ease,\n content-visibility 200ms allow-discrete;\n }\n .aranova-blog-post .aranova-blog-faq-item[open]::details-content {\n block-size: auto;\n }\n}\n\n/* --- Key takeaways ------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-key-takeaways {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 1em 1.25em;\n}\n.aranova-blog-post .aranova-blog-key-takeaways :where(ul) {\n margin-block: 0;\n}\n.aranova-blog-post .aranova-blog-key-takeaways :where(ul) > li::marker {\n color: var(--ara-blog-color-primary);\n}\n\n/* --- Article layout (content column + optional aside rail) -----------------------\n * \\`.aranova-blog-layout\\` is a plain block on narrow viewports (the aside, when\n * present, stacks after the content). On wide viewports a grid opens ONLY when\n * an aside exists, and the article widens by exactly the rail + gap so the\n * content column keeps its \\`content_max_width\\`. */\n.aranova-blog-post .aranova-blog-layout {\n /* Flex column (not block) so the aside can be VISUALLY hoisted above the\n * content on narrow viewports while the content stays first in DOM\n * (reading order / SEO). */\n display: flex;\n flex-direction: column;\n}\n.aranova-blog-post .aranova-blog-layout > .aranova-blog-aside {\n order: -1;\n margin-block-end: var(--_flow-lg);\n}\n@media (min-width: 1100px) {\n .aranova-blog-post:has(.aranova-blog-aside) {\n max-width: calc(var(--_content-max-width) + 240px + 3rem);\n }\n .aranova-blog-post .aranova-blog-layout:has(.aranova-blog-aside) {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 240px;\n column-gap: 3rem;\n align-items: start;\n }\n .aranova-blog-post .aranova-blog-layout:has(.aranova-blog-aside) > .aranova-blog-aside {\n /* Reset the mobile hoist: grid auto-placement honors \\`order\\`, so -1 would\n * drop the aside into the FIRST column. */\n order: 0;\n margin-block-end: 0;\n position: sticky;\n top: 5rem;\n align-self: start;\n max-height: calc(100vh - 6rem);\n overflow: auto;\n }\n /* Rail on the LEFT: flip the column order (content DOM stays first for\n * reading order / SEO; only the visual columns swap). */\n .aranova-blog-post .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside) {\n grid-template-columns: 240px minmax(0, 1fr);\n }\n /* Horizontal collapse, CLOSED: the rail column shrinks to the compact toggle\n * (auto) and the article reflows into the reclaimed space. */\n .aranova-blog-post\n .aranova-blog-layout:has(\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n ) {\n grid-template-columns: minmax(0, 1fr) auto;\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n ) {\n grid-template-columns: auto minmax(0, 1fr);\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside)\n > .aranova-blog-content {\n order: 2;\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside)\n > .aranova-blog-aside {\n order: 1;\n }\n}\n\n/* --- Byline / meta row (chrome) --------------------------------------------------- */\n.aranova-blog-post .aranova-blog-meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.35em 0.5em;\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n padding-block-end: var(--_flow);\n border-block-end: 1px solid var(--ara-blog-color-border);\n}\n.aranova-blog-post .aranova-blog-meta-item {\n display: inline-flex;\n align-items: baseline;\n gap: 0.5em;\n white-space: nowrap;\n}\n.aranova-blog-post .aranova-blog-meta-tags {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 0.35em;\n margin-inline-start: auto;\n}\n.aranova-blog-post .aranova-blog-meta-tag {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: 999px;\n padding: 0.1em 0.7em;\n white-space: nowrap;\n}\n\n/* --- Breadcrumbs (chrome) ---------------------------------------------------------- */\n.aranova-blog-post .aranova-blog-breadcrumbs {\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n}\n.aranova-blog-post .aranova-blog-breadcrumbs ol {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0.2em 0.5em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs li {\n display: inline-flex;\n align-items: baseline;\n gap: 0.5em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs a {\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs a:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs [aria-current=\"page\"] {\n color: var(--ara-blog-color-text);\n}\n\n/* --- Table of contents (chrome) -----------------------------------------------------\n * Zero-JS shell: a native details/summary disclosure. Chevron follows the FAQ\n * accordion's border-trick pattern (theme vars only, no raw colors). */\n.aranova-blog-post .aranova-blog-toc {\n font-size: var(--_small-size);\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-toc-title {\n cursor: pointer;\n list-style: none;\n position: relative;\n padding-inline-end: 1.75em;\n font-weight: 600;\n color: var(--_heading-color);\n}\n.aranova-blog-post .aranova-blog-toc-title::-webkit-details-marker {\n display: none;\n}\n.aranova-blog-post .aranova-blog-toc-title::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-toc[open] > .aranova-blog-toc-title::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-toc-title:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n.aranova-blog-post .aranova-blog-toc ol {\n list-style: none;\n margin: 0.75em 0 0;\n padding: 0;\n}\n.aranova-blog-post .aranova-blog-toc li {\n margin: 0;\n}\n.aranova-blog-post .aranova-blog-toc li + li {\n margin-block-start: 0.3em;\n}\n.aranova-blog-post .aranova-blog-toc li[data-depth=\"3\"] {\n padding-inline-start: 0.9em;\n font-size: 0.94em;\n}\n.aranova-blog-post .aranova-blog-toc a {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n line-height: 1.35;\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n}\n.aranova-blog-post .aranova-blog-toc a:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n\n/* --- Table of contents (client island) ------------------------------------------\n * Animated companion to the zero-JS shell above: same box, but a real <button>\n * header (aria-expanded) and a grid-template-rows 0fr↔1fr collapse. The global\n * reduced-motion block at the end of this sheet disables the transition. */\n.aranova-blog-post .aranova-blog-toc-panel {\n font-size: var(--_small-size);\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-toc-panel-title {\n /* Button reset — host sites may or may not run Tailwind preflight. */\n appearance: none;\n background: none;\n border: 0;\n margin: 0;\n padding: 0;\n padding-inline-end: 1.75em;\n width: 100%;\n text-align: start;\n font: inherit;\n cursor: pointer;\n position: relative;\n font-weight: 600;\n color: var(--_heading-color);\n}\n/* Rotating chevron (border trick — same pattern as the FAQ / zero-JS TOC). */\n.aranova-blog-post .aranova-blog-toc-panel-title::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 250ms ease;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-toc-panel-title:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n/* Collapsible body: the grid row animates 0fr↔1fr; the clip wrapper hides overflow. */\n.aranova-blog-post .aranova-blog-toc-panel-body {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 250ms ease;\n}\n.aranova-blog-post .aranova-blog-toc-panel[data-open=\"true\"] > .aranova-blog-toc-panel-body {\n grid-template-rows: 1fr;\n}\n/* Horizontal collapse: a clean binary toggle along the inline axis. Open = the\n * normal rail; closed = the whole panel is a compact chevron chip and the rail\n * grid column reclaims the space (via \\`:has()\\` on the layout, below). No width\n * animation: grid \\`auto\\` tracks can't interpolate, and a sliding clip either\n * overflows the sticky aside (horizontal scrollbar) or reflows the entries —\n * instant + correct beats janky. */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]\n > .aranova-blog-toc-panel-body {\n display: block;\n grid-template-rows: none;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-body {\n display: none;\n}\n/* Closed panel compacts to a small rounded chevron chip: the label is clipped\n * out of a square toggle box and the panel hugs it (the rail's \\`auto\\` grid\n * column then shrinks to the chip). */\n.aranova-blog-post .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"]) {\n width: fit-content;\n padding: 0.6em;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title {\n display: block;\n width: 1.4em;\n height: 1.4em;\n overflow: hidden;\n text-indent: 150%;\n white-space: nowrap;\n padding-inline-end: 0;\n}\n/* Horizontal chevron points along the fold axis (‹/›), flipping on open/close\n * per side, instead of the vertical ⌄/⌃. */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]\n > .aranova-blog-toc-panel-title::after {\n inset-inline: auto 0.5em;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n inset-inline: 0;\n margin-inline: auto;\n}\n/* Left rail: open folds left (‹ = rotate 135deg), closed expands right (› = -45deg). */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"left\"][data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(135deg);\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"left\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-70%) rotate(-45deg);\n}\n/* Right rail: mirror — open folds right (›), closed expands left (‹). */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"right\"][data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-70%) rotate(-45deg);\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"right\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(135deg);\n}\n.aranova-blog-post .aranova-blog-toc-panel-clip {\n overflow: hidden;\n min-height: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel ol {\n list-style: none;\n margin: 0.75em 0 0;\n padding: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel li {\n margin: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel li + li {\n margin-block-start: 0.3em;\n}\n.aranova-blog-post .aranova-blog-toc-panel li[data-depth=\"3\"] {\n padding-inline-start: 0.9em;\n font-size: 0.94em;\n}\n.aranova-blog-post .aranova-blog-toc-panel-link {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n line-height: 1.35;\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n border-inline-start: 2px solid transparent;\n padding-inline-start: 0.6em;\n margin-inline-start: -0.75em;\n transition: color 150ms ease;\n}\n.aranova-blog-post .aranova-blog-toc-panel-link:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n/* Scroll-spy active entry: primary color + inline-start accent. */\n.aranova-blog-post .aranova-blog-toc-panel-link[data-active=\"true\"] {\n color: var(--ara-blog-color-primary);\n border-inline-start-color: var(--ara-blog-color-primary);\n font-weight: 600;\n}\n\n/* --- Reading progress (client island) --------------------------------------------\n * Fixed strip along the top of the viewport; fill = scaleX(scrolled fraction),\n * driven directly from rAF (no transition — it tracks position, so the\n * reduced-motion block has nothing to disable and the bar stays useful). */\n.aranova-blog-post .aranova-blog-progress {\n position: fixed;\n inset-block-start: 0;\n inset-inline: 0;\n z-index: 60;\n background: var(--ara-blog-color-primary);\n transform: scaleX(0);\n transform-origin: 0 50%;\n pointer-events: none;\n}\n\n/* --- Vertical rhythm ------------------------------------------------------------\n * Kept AFTER the element rules: the owl selector and the :where()-wrapped\n * element rules all sit at (0,1,0), so source order lets rhythm win over the\n * elements' margin resets, and the per-heading overrides below win over the\n * owl. \\`:is(...)\\` targets both today's direct-child layout and the later\n * \\`.aranova-blog-content\\` wrapper at class specificity. */\n:is(.aranova-blog-post, .aranova-blog-content) > * + * {\n margin-block-start: var(--_flow);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(h2) {\n margin-block-start: var(--_flow-section);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(h3, h4) {\n margin-block-start: var(--_flow-lg);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(figure) {\n margin-block: var(--_flow-lg);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(hr) {\n margin-block: var(--_flow-section);\n}\n\n/* --- Motion ------------------------------------------------------------------\n * Opt-in: a later phase stamps data-motion=\"subtle\" on the article; without\n * the attribute nothing animates. */\n@media (prefers-reduced-motion: no-preference) {\n .aranova-blog-post[data-motion=\"subtle\"] {\n animation: aranova-blog-fade-up 300ms ease-out both;\n }\n}\n@keyframes aranova-blog-fade-up {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: none;\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .aranova-blog-post,\n .aranova-blog-post *,\n .aranova-blog-post *::before,\n .aranova-blog-post *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n`;\n","// Server-only article chrome — no 'use client'. A breadcrumb trail rendered\n// into BlogPostView's `header` slot, above the post title.\n\nimport type { ReactNode } from \"react\";\n\nimport { joinClassNames } from \"../class-names\";\n\nexport interface BlogBreadcrumbItem {\n label: string;\n href: string;\n}\n\nexport interface BlogBreadcrumbsProps {\n /** Ancestor links, outermost first (e.g. Home → Blog). */\n items: readonly BlogBreadcrumbItem[];\n /** The current page (usually the post title) — rendered as text, not a link. */\n currentLabel: string;\n /** Site-supplied class merged onto the fixed `.aranova-blog-breadcrumbs` class. */\n className?: string;\n}\n\n/**\n * Accessible breadcrumb trail: ancestor links separated by a decorative `/`,\n * ending in the current page as `<span aria-current=\"page\">`. Styled by the\n * `.aranova-blog-breadcrumbs` section of the base CSS.\n */\nexport function BlogBreadcrumbs({\n items,\n currentLabel,\n className,\n}: BlogBreadcrumbsProps): ReactNode {\n return (\n <nav aria-label=\"Breadcrumbs\" className={joinClassNames(\"aranova-blog-breadcrumbs\", className)}>\n <ol>\n {items.map((item) => (\n <li key={`${item.href} ${item.label}`}>\n <a href={item.href}>{item.label}</a>\n <span aria-hidden=\"true\" className=\"aranova-blog-breadcrumbs-separator\">\n /\n </span>\n </li>\n ))}\n <li>\n <span aria-current=\"page\">{currentLabel}</span>\n </li>\n </ol>\n </nav>\n );\n}\n","// Server-only article chrome — no 'use client'. A muted byline row (dates,\n// reading time, tag pills) rendered into BlogPostView's `header` slot.\n\nimport type { ReactNode } from \"react\";\n\nimport { joinClassNames } from \"../class-names\";\n\n/** Fallback locale for date formatting (the agency's home market). */\nexport const DEFAULT_BLOG_LOCALE = \"en-CA\";\n\nexport interface BlogPostMetaProps {\n /** ISO date(-time) string; invalid/absent values render nothing. */\n publishedAt?: string;\n /** ISO date(-time) string; only shown when it differs from `publishedAt`'s day. */\n updatedAt?: string | null;\n /** Whole minutes (see `estimateReadingTime`); non-positive values render nothing. */\n readingTimeMinutes?: number;\n tags?: readonly string[];\n /** BCP-47 locale for date formatting. Default {@link DEFAULT_BLOG_LOCALE}. */\n locale?: string;\n /** Site-supplied class merged onto the fixed `.aranova-blog-meta` class. */\n className?: string;\n}\n\ninterface FormattedDate {\n /** Machine-readable value for the `datetime` attribute (the date part). */\n dateTime: string;\n /** Human-readable long-form date. */\n label: string;\n}\n\n// Locale/date inputs come from operator-controlled config and the published\n// content object, but stay defensive anyway: a bad locale falls back, a bad\n// date renders nothing (never throws into the host page).\nfunction formatBlogDate(iso: string | undefined | null, locale: string): FormattedDate | null {\n if (!iso) return null;\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return null;\n let formatter: Intl.DateTimeFormat;\n try {\n formatter = new Intl.DateTimeFormat(locale, { dateStyle: \"long\", timeZone: \"UTC\" });\n } catch {\n formatter = new Intl.DateTimeFormat(DEFAULT_BLOG_LOCALE, {\n dateStyle: \"long\",\n timeZone: \"UTC\",\n });\n }\n return { dateTime: date.toISOString().slice(0, 10), label: formatter.format(date) };\n}\n\n/**\n * Post byline row: published date (+ \"Updated …\" when meaningfully different),\n * \"· N min read\", and tag pills. Renders `null` when there is nothing to show.\n * Styled by the `.aranova-blog-meta` section of the base CSS (muted small row\n * with a bottom divider).\n */\nexport function BlogPostMeta({\n publishedAt,\n updatedAt,\n readingTimeMinutes,\n tags,\n locale = DEFAULT_BLOG_LOCALE,\n className,\n}: BlogPostMetaProps): ReactNode {\n const published = formatBlogDate(publishedAt, locale);\n const updated = formatBlogDate(updatedAt, locale);\n // Same-day updates are noise — only surface a real revision.\n const showUpdated = updated !== null && updated.dateTime !== published?.dateTime;\n const showReadingTime = typeof readingTimeMinutes === \"number\" && readingTimeMinutes > 0;\n const shownTags = (tags ?? []).filter((tag) => tag.length > 0);\n\n const items: Array<{ key: string; node: ReactNode }> = [];\n if (published) {\n items.push({\n key: \"published\",\n node: <time dateTime={published.dateTime}>{published.label}</time>,\n });\n }\n if (showUpdated && updated) {\n items.push({\n key: \"updated\",\n node: (\n <>\n Updated <time dateTime={updated.dateTime}>{updated.label}</time>\n </>\n ),\n });\n }\n if (showReadingTime) {\n items.push({ key: \"reading-time\", node: <>{readingTimeMinutes} min read</> });\n }\n\n if (items.length === 0 && shownTags.length === 0) return null;\n\n return (\n <div className={joinClassNames(\"aranova-blog-meta\", className)}>\n {items.map((item, index) => (\n <span key={item.key} className=\"aranova-blog-meta-item\">\n {index > 0 ? (\n <span aria-hidden=\"true\" className=\"aranova-blog-meta-separator\">\n ·\n </span>\n ) : null}\n {item.node}\n </span>\n ))}\n {shownTags.length > 0 ? (\n <span className=\"aranova-blog-meta-tags\">\n {shownTags.map((tag) => (\n <span key={tag} className=\"aranova-blog-meta-tag\">\n {tag}\n </span>\n ))}\n </span>\n ) : null}\n </div>\n );\n}\n","// Server-only article chrome — no 'use client'. The zero-JS table-of-contents\n// shell rendered into BlogPostView's `aside` slot. A later phase ships an\n// animated client-island TOC; because the aside slot takes any ReactNode, that\n// island simply replaces this shell in the composition — same anchor ids\n// (extractBlogHeadings ↔ rehype-slug parity), no renderer changes.\n\nimport type { ReactNode } from \"react\";\n\nimport type { BlogHeading } from \"../../toc\";\nimport { joinClassNames } from \"../class-names\";\n\nexport interface BlogTocShellProps {\n /** The post outline — see `extractBlogHeadings`. Empty array renders nothing. */\n headings: readonly BlogHeading[];\n /** Disclosure summary label. Default `\"On this page\"`. */\n title?: string;\n /** Site-supplied class merged onto the fixed `.aranova-blog-toc` class. */\n className?: string;\n}\n\n/**\n * No-JS table of contents: a native `<details open>` disclosure holding an\n * anchor list (h3 entries indented under their h2). Styled by the\n * `.aranova-blog-toc` section of the base CSS (chevron consistent with the\n * FAQ disclosure pattern).\n */\nexport function BlogTocShell({\n headings,\n title = \"On this page\",\n className,\n}: BlogTocShellProps): ReactNode {\n if (headings.length === 0) return null;\n return (\n <details open className={joinClassNames(\"aranova-blog-toc\", className)}>\n <summary className=\"aranova-blog-toc-title\">{title}</summary>\n <nav aria-label=\"Table of contents\">\n <ol>\n {headings.map((heading) => (\n <li key={heading.id} data-depth={heading.depth}>\n <a href={`#${heading.id}`} title={heading.text}>\n {heading.text}\n </a>\n </li>\n ))}\n </ol>\n </nav>\n </details>\n );\n}\n","// Async React Server Component — no 'use client'. The one-line integration for\n// client sites AND the internal dashboard's draft preview (same component, a\n// different BlogClient/content source).\n\nimport type { ReactNode } from \"react\";\n\nimport type { BlogClient } from \"../client\";\nimport { resolveBlogTheme, sanitizeBlogTheme, type BlogThemeOverride } from \"../theme\";\nimport { estimateReadingTime, extractBlogHeadings, type BlogHeading } from \"../toc\";\nimport { BlogPostView } from \"./blog-post-view\";\nimport type { BlogClassNameOverrides } from \"./class-names\";\nimport { BlogBreadcrumbs, type BlogBreadcrumbItem } from \"./chrome/breadcrumbs\";\nimport { BlogPostMeta } from \"./chrome/meta\";\nimport { BlogTocShell } from \"./chrome/toc\";\nimport { createDefaultComponents, type BlogComponents } from \"./components\";\n\nexport interface BlogPostProps {\n slug: string;\n client: BlogClient;\n /** Client-registered default theme layer (sits between the base theme and per-post overrides). */\n defaultTheme?: BlogThemeOverride;\n /** Overrides merged over the default component set. */\n components?: Partial<BlogComponents>;\n /** Rendered when the slug is unknown/unfetchable. Default: a minimal not-found article. */\n notFound?: ReactNode;\n /** Render the byline row (published/updated dates + reading time). Default `false`. */\n showMeta?: boolean;\n /** Render the sticky table-of-contents rail (omitted when the post has no h2/h3). Default `false`. */\n showToc?: boolean;\n /** Which side the ToC rail sits on at desktop width. Default `\"right\"`. */\n tocSide?: \"left\" | \"right\";\n /** Render the breadcrumb trail (requires `breadcrumbs`). Default `false`. */\n showBreadcrumbs?: boolean;\n /** Ancestor links for the breadcrumb trail, outermost first (e.g. Home → Blog). */\n breadcrumbs?: readonly BlogBreadcrumbItem[];\n /** BCP-47 locale for the byline dates. Default `\"en-CA\"`. */\n locale?: string;\n /**\n * Component-injection slot for the table of contents: when provided (and\n * `showToc` finds an outline), it renders INSTEAD of the zero-JS\n * `BlogTocShell` default. This is how `@aranova/tracking-next`'s enhanced\n * `BlogPost` wires in the animated `TocPanel` client island — tracking-core\n * cannot import it back from tracking-next (core is the dependency), and the\n * island must reach server bundles only via the\n * `@aranova/tracking-next/blog-client` self-specifier so its `\"use client\"`\n * boundary survives `splitting: false` bundling.\n */\n tocSlot?: (headings: readonly BlogHeading[]) => ReactNode;\n /**\n * Component-injection slot rendered at the very top of the header chrome\n * (before breadcrumbs/byline). Same rationale as `tocSlot` — the enhanced\n * wrapper injects the `ReadingProgress` island here under `showProgress`.\n */\n progressSlot?: () => ReactNode;\n /**\n * Site-supplied classes merged onto the fixed base class of each styleable\n * surface (article/layout/MDX components/chrome) — additive, never a\n * replacement of the `.aranova-blog-*` selector. For a full component\n * rewrite instead, use `components`.\n */\n classNames?: BlogClassNameOverrides;\n}\n\n/**\n * Fetch-and-render a published post: `getPost(slug)` → theme resolution\n * (BASE < `defaultTheme` < per-post `theme_overrides`, every untrusted layer\n * re-sanitized) → {@link BlogPostView} with the post's image records wired\n * into the default components, plus opt-in article chrome (breadcrumbs +\n * byline in the header slot, table of contents in the aside slot).\n */\nexport async function BlogPost({\n slug,\n client,\n defaultTheme,\n components,\n notFound,\n showMeta = false,\n showToc = false,\n tocSide = \"right\",\n showBreadcrumbs = false,\n breadcrumbs,\n locale,\n tocSlot,\n progressSlot,\n classNames,\n}: BlogPostProps): Promise<ReactNode> {\n const post = await client.getPost(slug);\n if (!post) {\n return (\n notFound ?? (\n <article className=\"aranova-blog-post\" data-error=\"not-found\">\n <p>This post could not be found.</p>\n </article>\n )\n );\n }\n\n // theme_overrides is already parsed into the override shape, but sanitize\n // defensively anyway — resolveBlogTheme's output lands in inline styles.\n const theme = resolveBlogTheme(\n sanitizeBlogTheme(defaultTheme),\n sanitizeBlogTheme(post.theme_overrides),\n );\n const merged = { ...createDefaultComponents(post.images, classNames), ...components };\n\n // Header chrome: breadcrumbs above the byline. (Tags live on the manifest\n // entry, not the content object, so the byline carries dates + reading time.)\n const crumbs =\n showBreadcrumbs && breadcrumbs && breadcrumbs.length > 0 ? (\n <BlogBreadcrumbs\n items={breadcrumbs}\n currentLabel={post.title}\n className={classNames?.breadcrumbs}\n />\n ) : null;\n const meta = showMeta ? (\n <BlogPostMeta\n publishedAt={post.published_at}\n updatedAt={post.updated_at}\n readingTimeMinutes={estimateReadingTime(post.mdx)}\n locale={locale}\n className={classNames?.meta}\n />\n ) : null;\n const progress = progressSlot ? progressSlot() : null;\n const header =\n progress || crumbs || meta ? (\n <>\n {progress}\n {crumbs}\n {meta}\n </>\n ) : undefined;\n\n // Aside chrome: the injected TOC (client island) when a slot is provided,\n // the zero-JS shell otherwise. Omitted entirely (no empty rail, no grid\n // column) when the post has no h2/h3 outline.\n const headings = showToc ? extractBlogHeadings(post.mdx) : [];\n const aside =\n headings.length > 0\n ? (tocSlot?.(headings) ?? <BlogTocShell headings={headings} className={classNames?.toc} />)\n : undefined;\n\n return (\n <BlogPostView\n mdx={post.mdx}\n theme={theme}\n components={merged}\n header={header}\n aside={aside}\n classNames={classNames}\n tocSide={tocSide}\n />\n );\n}\n","// Enhanced BlogPost — the consumer-facing composition on\n// `@aranova/tracking-next/blog-server`. Server component (no 'use client').\n//\n// Composition design (ONE public BlogPost): tracking-core's BlogPost keeps its\n// zero-JS `BlogTocShell` default and exposes optional component-injection\n// slots (`tocSlot`, `progressSlot`); this wrapper pre-wires the animated\n// client islands into those slots. Core cannot import the islands from\n// tracking-next (core is the dependency), and this module must NOT import\n// their source relatively — under tsup's `splitting: false` that would inline\n// them into dist/blog-server.* and drop their `\"use client\"` directive. The\n// `@aranova/tracking-next/blog-client` self-specifier is marked external in\n// tsup (like `next-mdx-remote/rsc`), so the emitted blog-server bundle keeps a\n// real import across the server/client boundary and Next hydrates the islands.\n//\n// The islands are SSR'd like any client component, so `showToc` renders the\n// full anchor list server-side (no zero-JS shell needed here — the shell\n// remains core's default for compositions that don't inject a slot).\n\nimport type { ReactNode } from \"react\";\n\nimport { ReadingProgress, TocPanel } from \"@aranova/tracking-next/blog-client\";\nimport {\n BlogPost as BlogPostBase,\n type BlogPostProps as BlogPostBaseProps,\n} from \"../../tracking-core/src/blog-server-public\";\n\nexport interface BlogPostProps extends Omit<BlogPostBaseProps, \"tocSlot\" | \"progressSlot\"> {\n /** Render the fixed top-of-viewport reading progress bar. Default `false`. */\n showProgress?: boolean;\n /**\n * ToC collapse axis: `\"vertical\"` (default) folds up under the title,\n * `\"horizontal\"` folds sideways toward `tocSide`. Only affects the animated\n * `TocPanel` island rendered here.\n */\n tocCollapse?: \"vertical\" | \"horizontal\";\n}\n\n/**\n * `BlogPost` with the animated islands wired in: `showToc` renders the\n * collapsible scroll-spy `TocPanel` (instead of core's zero-JS shell) and\n * `showProgress` adds the `ReadingProgress` bar to the header chrome. All\n * other props pass straight through to tracking-core's `BlogPost`.\n */\nexport function BlogPost({\n showProgress = false,\n tocCollapse = \"vertical\",\n ...props\n}: BlogPostProps): ReactNode {\n // `tocSide` drives the layout column (in `...props` for core) AND the island's\n // horizontal-collapse direction, so read it here without consuming it.\n const tocSide = props.tocSide ?? \"right\";\n return (\n <BlogPostBase\n {...props}\n tocSlot={(headings) => (\n <TocPanel\n headings={headings}\n className={props.classNames?.toc}\n collapse={tocCollapse}\n side={tocSide}\n />\n )}\n progressSlot={\n showProgress ? () => <ReadingProgress className={props.classNames?.progress} /> : undefined\n }\n />\n );\n}\n"],"mappings":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AFuFrB,IAAAC,sBAAA;AA9ER,IAAM,oBAAoB,EAAE,CAAC,wBAAwB,GAAG,iBAAiB;AA6CzE,eAAsB,aAAa;AAAA,EACjC;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,mBAAmB;AAAA,EACnB,UAAU;AACZ,GAA0C;AAGxC,QAAM,QAAQ,mBAAmB,KAAK;AAGtC,QAAM,SAAS,EAAE,GAAG,wBAAwB,CAAC,GAAG,UAAU,GAAG,GAAG,WAAW;AAO3E,QAAM,aAAa,gBAAgB,KAAK,EAAE,iBAAiB,CAAC;AAC5D,MAAI,CAAC,WAAW,IAAI;AAClB,UAAM,QAAQ,MAAM,KAAK,IAAI,IAAI,WAAW,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE,KAAK,IAAI;AACrF,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,eAAe,qBAAqB,YAAY,OAAO;AAAA,QAClE,cAAW;AAAA,QACX;AAAA,QACC,GAAG;AAAA,QAEJ;AAAA,uDAAC,WAAO,yBAAc;AAAA,UACtB,8CAAC,OAAE;AAAA;AAAA,YAA6C;AAAA,YAAM;AAAA,aAAC;AAAA;AAAA;AAAA,IACzD;AAAA,EAEJ;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,WAAW,UAAM,uBAAW;AAAA,MAChC,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,SAAS,EAAE,YAAY,EAAE,eAAe,CAAC,mBAAAC,OAAS,GAAG,eAAe,CAAC,mBAAAC,OAAU,EAAE,EAAE;AAAA,IACrF,CAAC;AACD,cAAU,SAAS;AAAA,EACrB,QAAQ;AACN,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,eAAe,qBAAqB,YAAY,OAAO;AAAA,QAClE,cAAW;AAAA,QACX;AAAA,QACC,GAAG;AAAA,QAEJ;AAAA,uDAAC,WAAO,yBAAc;AAAA,UACtB,6CAAC,OAAE,+CAAiC;AAAA;AAAA;AAAA,IACtC;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,eAAe,qBAAqB,YAAY,OAAO;AAAA,MAClE,eAAa;AAAA,MACb;AAAA,MACC,GAAG;AAAA,MAEJ;AAAA,qDAAC,WAAO,yBAAc;AAAA,QACrB;AAAA,QACD;AAAA,UAAC;AAAA;AAAA,YACC,WAAW,eAAe,uBAAuB,YAAY,MAAM;AAAA,YACnE,iBAAe;AAAA,YAEf;AAAA,2DAAC,SAAI,WAAW,eAAe,wBAAwB,YAAY,OAAO,GAAI,mBAAQ;AAAA,cACrF,QACC,6CAAC,WAAM,WAAW,eAAe,sBAAsB,YAAY,KAAK,GAAI,iBAAM,IAChF;AAAA;AAAA;AAAA,QACN;AAAA;AAAA;AAAA,EACF;AAEJ;;;AGjHU,IAAAC,sBAAA;AATH,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAAoC;AAClC,SACE,6CAAC,SAAI,cAAW,eAAc,WAAW,eAAe,4BAA4B,SAAS,GAC3F,wDAAC,QACE;AAAA,UAAM,IAAI,CAAC,SACV,8CAAC,QACC;AAAA,mDAAC,OAAE,MAAM,KAAK,MAAO,eAAK,OAAM;AAAA,MAChC,6CAAC,UAAK,eAAY,QAAO,WAAU,sCAAqC,eAExE;AAAA,SAJO,GAAG,KAAK,IAAI,IAAI,KAAK,KAAK,EAKnC,CACD;AAAA,IACD,6CAAC,QACC,uDAAC,UAAK,gBAAa,QAAQ,wBAAa,GAC1C;AAAA,KACF,GACF;AAEJ;;;AC2BY,IAAAC,sBAAA;AAnEL,IAAM,sBAAsB;AA0BnC,SAAS,eAAe,KAAgC,QAAsC;AAC5F,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,IAAI,KAAK,GAAG;AACzB,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AACzC,MAAI;AACJ,MAAI;AACF,gBAAY,IAAI,KAAK,eAAe,QAAQ,EAAE,WAAW,QAAQ,UAAU,MAAM,CAAC;AAAA,EACpF,QAAQ;AACN,gBAAY,IAAI,KAAK,eAAe,qBAAqB;AAAA,MACvD,WAAW;AAAA,MACX,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACA,SAAO,EAAE,UAAU,KAAK,YAAY,EAAE,MAAM,GAAG,EAAE,GAAG,OAAO,UAAU,OAAO,IAAI,EAAE;AACpF;AAQO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AACF,GAAiC;AAC/B,QAAM,YAAY,eAAe,aAAa,MAAM;AACpD,QAAM,UAAU,eAAe,WAAW,MAAM;AAEhD,QAAM,cAAc,YAAY,QAAQ,QAAQ,aAAa,WAAW;AACxE,QAAM,kBAAkB,OAAO,uBAAuB,YAAY,qBAAqB;AACvF,QAAM,aAAa,QAAQ,CAAC,GAAG,OAAO,CAAC,QAAQ,IAAI,SAAS,CAAC;AAE7D,QAAM,QAAiD,CAAC;AACxD,MAAI,WAAW;AACb,UAAM,KAAK;AAAA,MACT,KAAK;AAAA,MACL,MAAM,6CAAC,UAAK,UAAU,UAAU,UAAW,oBAAU,OAAM;AAAA,IAC7D,CAAC;AAAA,EACH;AACA,MAAI,eAAe,SAAS;AAC1B,UAAM,KAAK;AAAA,MACT,KAAK;AAAA,MACL,MACE,8EAAE;AAAA;AAAA,QACQ,6CAAC,UAAK,UAAU,QAAQ,UAAW,kBAAQ,OAAM;AAAA,SAC3D;AAAA,IAEJ,CAAC;AAAA,EACH;AACA,MAAI,iBAAiB;AACnB,UAAM,KAAK,EAAE,KAAK,gBAAgB,MAAM,8EAAG;AAAA;AAAA,MAAmB;AAAA,OAAS,EAAI,CAAC;AAAA,EAC9E;AAEA,MAAI,MAAM,WAAW,KAAK,UAAU,WAAW,EAAG,QAAO;AAEzD,SACE,8CAAC,SAAI,WAAW,eAAe,qBAAqB,SAAS,GAC1D;AAAA,UAAM,IAAI,CAAC,MAAM,UAChB,8CAAC,UAAoB,WAAU,0BAC5B;AAAA,cAAQ,IACP,6CAAC,UAAK,eAAY,QAAO,WAAU,+BAA8B,kBAEjE,IACE;AAAA,MACH,KAAK;AAAA,SANG,KAAK,GAOhB,CACD;AAAA,IACA,UAAU,SAAS,IAClB,6CAAC,UAAK,WAAU,0BACb,oBAAU,IAAI,CAAC,QACd,6CAAC,UAAe,WAAU,yBACvB,iBADQ,GAEX,CACD,GACH,IACE;AAAA,KACN;AAEJ;;;ACpFI,IAAAC,sBAAA;AAPG,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAAiC;AAC/B,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,SACE,8CAAC,aAAQ,MAAI,MAAC,WAAW,eAAe,oBAAoB,SAAS,GACnE;AAAA,iDAAC,aAAQ,WAAU,0BAA0B,iBAAM;AAAA,IACnD,6CAAC,SAAI,cAAW,qBACd,uDAAC,QACE,mBAAS,IAAI,CAAC,YACb,6CAAC,QAAoB,cAAY,QAAQ,OACvC,uDAAC,OAAE,MAAM,IAAI,QAAQ,EAAE,IAAI,OAAO,QAAQ,MACvC,kBAAQ,MACX,KAHO,QAAQ,EAIjB,CACD,GACH,GACF;AAAA,KACF;AAEJ;;;AC2CU,IAAAC,sBAAA;AArBV,eAAsB,SAAS;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsC;AACpC,QAAM,OAAO,MAAM,OAAO,QAAQ,IAAI;AACtC,MAAI,CAAC,MAAM;AACT,WACE,YACE,6CAAC,aAAQ,WAAU,qBAAoB,cAAW,aAChD,uDAAC,OAAE,2CAA6B,GAClC;AAAA,EAGN;AAIA,QAAM,QAAQ;AAAA,IACZ,kBAAkB,YAAY;AAAA,IAC9B,kBAAkB,KAAK,eAAe;AAAA,EACxC;AACA,QAAM,SAAS,EAAE,GAAG,wBAAwB,KAAK,QAAQ,UAAU,GAAG,GAAG,WAAW;AAIpF,QAAM,SACJ,mBAAmB,eAAe,YAAY,SAAS,IACrD;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,MACP,cAAc,KAAK;AAAA,MACnB,WAAW,YAAY;AAAA;AAAA,EACzB,IACE;AACN,QAAM,OAAO,WACX;AAAA,IAAC;AAAA;AAAA,MACC,aAAa,KAAK;AAAA,MAClB,WAAW,KAAK;AAAA,MAChB,oBAAoB,oBAAoB,KAAK,GAAG;AAAA,MAChD;AAAA,MACA,WAAW,YAAY;AAAA;AAAA,EACzB,IACE;AACJ,QAAM,WAAW,eAAe,aAAa,IAAI;AACjD,QAAM,SACJ,YAAY,UAAU,OACpB,8EACG;AAAA;AAAA,IACA;AAAA,IACA;AAAA,KACH,IACE;AAKN,QAAM,WAAW,UAAU,oBAAoB,KAAK,GAAG,IAAI,CAAC;AAC5D,QAAM,QACJ,SAAS,SAAS,IACb,UAAU,QAAQ,KAAK,6CAAC,gBAAa,UAAoB,WAAW,YAAY,KAAK,IACtF;AAEN,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,KAAK;AAAA,MACV;AAAA,MACA,YAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;;;ACtIA,yBAA0C;AAmClC,IAAAC,sBAAA;AAZD,SAASC,UAAS;AAAA,EACvB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,GAAG;AACL,GAA6B;AAG3B,QAAM,UAAU,MAAM,WAAW;AACjC,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,SAAS,CAAC,aACR;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,WAAW,MAAM,YAAY;AAAA,UAC7B,UAAU;AAAA,UACV,MAAM;AAAA;AAAA,MACR;AAAA,MAEF,cACE,eAAe,MAAM,6CAAC,sCAAgB,WAAW,MAAM,YAAY,UAAU,IAAK;AAAA;AAAA,EAEtF;AAEJ;","names":["BlogPost","import_zod","remarkParse","remarkGfm","remarkMdx","GithubSlugger","import_remark_gfm","import_remark_mdx","import_remark_parse","import_unified","remarkParse","remarkGfm","remarkMdx","import_remark_gfm","import_jsx_runtime","remarkGfm","rehypeSlug","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","BlogPost"]}
|
|
1
|
+
{"version":3,"sources":["../src/blog-server.ts","../../tracking-core/src/capabilities.ts","../../tracking-core/src/phone.ts","../../tracking-core/src/resources/tracking-config-runtime.ts","../../tracking-core/src/resources/blog/schema.ts","../../tracking-core/src/resources/blog/theme.ts","../../tracking-core/src/resources/blog/client.ts","../../tracking-core/src/resources/blog/presets.ts","../../tracking-core/src/resources/blog/view/class-names.ts","../../tracking-core/src/resources/blog/seo.ts","../../tracking-core/src/resources/blog/toc.ts","../../tracking-core/src/resources/blog/validate-mdx.ts","../../tracking-core/src/resources/blog/view/blog-post-view.tsx","../../tracking-core/src/resources/blog/view/components.tsx","../../tracking-core/src/resources/blog/view/styles.ts","../../tracking-core/src/resources/blog/view/chrome/breadcrumbs.tsx","../../tracking-core/src/resources/blog/view/chrome/meta.tsx","../../tracking-core/src/resources/blog/view/chrome/toc.tsx","../../tracking-core/src/resources/blog/view/blog-post.tsx","../src/blog-post.tsx"],"sourcesContent":["import \"server-only\";\n\n// Server-only blog entry — `@aranova/tracking-next/blog-server`. Everything on\n// `./blog` PLUS the async RSC renderers (`<BlogPost/>`, `<BlogPostView/>`),\n// which compile MDX via `next-mdx-remote/rsc` and must never reach a client\n// bundle (enforced by the `server-only` import above).\n// The surface is authored once in tracking-core (see blog-server-public.ts);\n// this file exists as the per-package tsup build entry PLUS the one override\n// below: the consumer-facing `BlogPost` is the enhanced wrapper that pre-wires\n// the animated client islands (TocPanel scroll-spy TOC, ReadingProgress bar)\n// into core's injection slots. Explicit exports win over `export *`, so this\n// entry still exposes exactly ONE BlogPost. Core's un-enhanced renderer stays\n// reachable for compositions that want the zero-JS shell (`BlogPostView` +\n// `BlogTocShell` directly).\nexport * from \"../../tracking-core/src/blog-server-public\";\nexport { BlogPost, type BlogPostProps } from \"./blog-post\";\n","// Capability self-report registry.\n//\n// Every SDK construct registers itself here at construction time, and the\n// registered set rides out on `TrackingClientContext.capabilities`, which the\n// backend persists on the session row. The dashboard derives what a client site\n// ACTUALLY has installed from those sessions, rather than trusting an operator\n// checkbox — see contracts/tracking-capabilities.json, which pins this id list\n// against the backend enum and the dashboard type union.\n//\n// Browser-only by design: a server-side client (calendar bookings) emits no\n// session, and its artifact is better evidence than a self-report anyway.\n//\n// This module deliberately imports nothing: it is the one thing every other\n// resource can import without adding an edge that could close a cycle.\n\n/**\n * Every capability an install can report. Pinned by\n * `contracts/tracking-capabilities.json` — adding one here without a catalog\n * entry fails the drift test.\n */\nexport const TRACKING_CAPABILITIES = [\n \"ad_tags_google\",\n \"ad_tags_meta\",\n \"base_tracking\",\n \"blog_rendering\",\n \"calendar_read\",\n \"calendar_write\",\n \"consent_controls\",\n \"conversion_goals_auto\",\n \"conversion_goals_manual\",\n \"cta_click_capture\",\n \"form_capture\",\n \"phone_click_capture\",\n \"phone_fields\",\n] as const;\n\nexport type TrackingCapability = (typeof TRACKING_CAPABILITIES)[number];\n\n/**\n * DOM attribute a server-rendered surface stamps to report itself.\n *\n * Blog rendering never calls the Aranova API — it reads the CDN directly — so\n * neither the ingest payload nor the request-header path can see it. The\n * renderer stamps this attribute instead and the browser context collector\n * picks it up on the next batch.\n */\nexport const CAPABILITY_DOM_ATTRIBUTE = \"data-aranova-capability\";\n\nconst registered = new Set<TrackingCapability>();\n\n/**\n * Record that a capability is present in this runtime.\n *\n * Idempotent and side-effect free — safe to call on every construction,\n * including React strict-mode double mounts.\n */\nexport function registerCapability(capability: TrackingCapability): void {\n registered.add(capability);\n}\n\n/**\n * Capabilities registered so far in this runtime, sorted for a stable wire\n * value (so the backend can hash the array to skip redundant writes).\n *\n * Also collects `data-aranova-capability` markers from the DOM, which is how\n * server-rendered surfaces that never call our API report themselves.\n */\nexport function getRegisteredCapabilities(): TrackingCapability[] {\n const all = new Set<TrackingCapability>(registered);\n for (const marker of readDomMarkers()) all.add(marker);\n return Array.from(all).sort();\n}\n\nfunction readDomMarkers(): TrackingCapability[] {\n if (typeof document === \"undefined\") return [];\n const known = new Set<string>(TRACKING_CAPABILITIES);\n const found: TrackingCapability[] = [];\n // Unknown values are dropped rather than forwarded: the backend rejects ids\n // outside its enum, and a stray attribute on a client's page must never be\n // able to fail an otherwise-valid ingest batch.\n for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {\n const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);\n if (value && known.has(value)) found.push(value as TrackingCapability);\n }\n return found;\n}\n\n/** Test-only: drop everything registered so far. */\nexport function resetRegisteredCapabilitiesForTests(): void {\n registered.clear();\n}\n","// Framework-agnostic phone-number utilities — isomorphic (browser + Node/RSC),\n// zero React. Bundled `libphonenumber-js` (standard metadata) so clients add no\n// dependency. The transmitted value is ALWAYS E.164; display is the only knob.\n\nimport { AsYouType, parsePhoneNumberFromString, type CountryCode } from \"libphonenumber-js\";\n\nexport type { CountryCode };\n\n/**\n * How a phone number is shown in the UI. The transmitted value is always E.164\n * and is deliberately NOT part of this — only the display format is configurable.\n * A function form covers the long tail (`(parsed) => string`).\n */\nexport type PhoneDisplayFormat =\n | \"national\"\n | \"international\"\n | \"e164\"\n | ((parsed: ParsedPhone) => string);\n\nexport interface ParsedPhone {\n /** E.164 (`\"+14165550199\"`) or `null` when the input isn't a valid number. This is what gets transmitted. */\n e164: string | null;\n /** National display form (`\"(416) 555-0199\"`); empty string when unparseable. */\n national: string;\n /** International display form (`\"+1 416 555 0199\"`); empty string when unparseable. */\n international: string;\n /** ISO-3166 country resolved by libphonenumber, or `null`. */\n country: CountryCode | null;\n isValid: boolean;\n}\n\n/** Region assumed for numbers typed without a country code. */\nexport const DEFAULT_PHONE_COUNTRY: CountryCode = \"CA\";\n\n/** Parse a raw/display string into every representation at once (one parse → display + wire never drift). */\nexport function parsePhone(raw: string, country?: CountryCode): ParsedPhone {\n const region = country ?? DEFAULT_PHONE_COUNTRY;\n const parsed = parsePhoneNumberFromString(raw ?? \"\", region);\n if (!parsed) {\n return { e164: null, national: \"\", international: \"\", country: region, isValid: false };\n }\n const isValid = parsed.isValid();\n return {\n // E.164 is only surfaced for a *valid* number — a possible-but-invalid input\n // (e.g. too few digits) still parses but must not be transmitted.\n e164: isValid ? parsed.number : null,\n national: parsed.formatNational(),\n international: parsed.formatInternational(),\n country: parsed.country ?? region,\n isValid,\n };\n}\n\n/** Normalize any raw/display value to E.164, or `null` if it isn't a valid number. */\nexport function toE164(raw: string, country?: CountryCode): string | null {\n return parsePhone(raw, country).e164;\n}\n\n/** Format a value for display. Defaults to `'national'`. Never affects the wire value. */\nexport function formatPhone(\n value: string,\n format: PhoneDisplayFormat = \"national\",\n country?: CountryCode,\n): string {\n const parsed = parsePhone(value, country);\n if (typeof format === \"function\") return format(parsed);\n switch (format) {\n case \"international\":\n return parsed.international || value;\n case \"e164\":\n return parsed.e164 ?? value;\n case \"national\":\n default:\n return parsed.national || value;\n }\n}\n\n/** Live, incremental formatting for an `<input>` as the user types (`AsYouType`). */\nexport function formatPhoneAsTyped(raw: string, country?: CountryCode): string {\n return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? \"\");\n}\n","import { applyDefaultConsentState, isValidGtagId, loadGtagScript } from \"../gtag\";\nimport {\n parseConversionConfig,\n type ConversionConfig,\n type ConversionGoal,\n} from \"./conversion-config\";\nimport { fireConversionWithConsent } from \"./conversion-firing\";\nimport { getAutomaticTransactionId } from \"./automatic-transaction\";\nimport { automaticThresholdMet } from \"./automatic-trigger\";\nimport { fromMinor } from \"./sales/money\";\nimport type { SupportedCurrency } from \"./sales/schema\";\n\n/**\n * Where a business's published tracking config lives.\n *\n * `businessId` + `environment` are the stable identity; the ORIGIN it is fetched\n * from is deployment-specific (production CDN vs. a local object store), which is\n * why `cdnBaseUrl` exists. Codegen bakes only the identity, so one committed\n * generated file works in every environment and only an env var changes.\n *\n * `cdnUrl` remains supported and WINS when present — every client repo generated\n * before `cdnBaseUrl` passes one, and those must keep working untouched.\n */\nexport interface TrackingConfigReference {\n /**\n * Fully-resolved object URL. Optional: omit it and pass `cdnBaseUrl` (or rely\n * on the production default) to have it composed from the identity below.\n */\n cdnUrl?: string;\n /**\n * Origin (optionally with a path prefix) the config object is served from, e.g.\n * `https://demos.aranova.io` in production or\n * `http://localhost:9100/aranova-demos` against a local MinIO. Ignored when\n * `cdnUrl` is set; blank or omitted falls back to {@link DEFAULT_CDN_BASE_URL}.\n * `tracking-cli gen` wires this to an env var so retargeting is config, not code.\n */\n cdnBaseUrl?: string;\n businessId: string;\n environment: \"production\" | \"test\";\n}\n\n/** Production object-storage origin — the default when nothing overrides it. */\nexport const DEFAULT_CDN_BASE_URL = \"https://demos.aranova.io\";\n\n/**\n * Object key for a business's config, mirroring the backend's\n * `core/storage.py::tracking_config_key`. The backend stays the authority: it\n * publishes at this key and returns the absolute URL, which is what the CLI bakes\n * as the legacy `cdnUrl`. This is the composition path for references that carry\n * identity instead of a URL — the `v1` prefix is the schema MAJOR and must move in\n * lockstep with the backend if it is ever cut to `v2`.\n */\nfunction trackingConfigKey(businessId: string, environment: \"production\" | \"test\"): string {\n return `tracking-config/v1/${businessId}-${environment}.json`;\n}\n\n/**\n * The URL a reference resolves to. An explicit `cdnUrl` wins (back-compat);\n * otherwise compose from `cdnBaseUrl` (or the production default).\n *\n * A blank/whitespace `cdnBaseUrl` is treated as UNSET rather than composed: the\n * generated module reads it from an env var, and a var that is present-but-empty\n * (a stray `NEXT_PUBLIC_ARANOVA_CDN_BASE_URL=` line) would otherwise produce\n * `/tracking-config/...` — a same-origin request to the client's own site.\n */\nexport function resolveTrackingConfigUrl(ref: TrackingConfigReference): string {\n if (ref.cdnUrl) return ref.cdnUrl;\n const configured = ref.cdnBaseUrl?.trim();\n const base = (configured || DEFAULT_CDN_BASE_URL).replace(/\\/+$/, \"\");\n return `${base}/${trackingConfigKey(ref.businessId, ref.environment)}`;\n}\n\ntype RuntimeState = \"unconfirmed\" | \"active\" | \"tombstone\";\n\ninterface CachedEntry {\n etag: string | null;\n config: ConversionConfig;\n}\n\ninterface QueuedConversion {\n key: string;\n value?: number | null;\n currency?: string | null;\n transactionId?: string | null;\n}\n\nexport type TrackingConfigConversionOptions = Omit<QueuedConversion, \"key\">;\n\ninterface QueuedPageView {\n href: string;\n title: string | null;\n referrer: string | null;\n}\n\ninterface QueuedAutomaticEvent {\n eventType: string;\n metadata: Record<string, unknown>;\n transactionPath: string;\n transactionScope: string;\n}\n\nconst CACHE_PREFIX = \"_aranova_cfg_runtime_\";\nconst AUTHORITY_TTL_MS = 60_000;\n// Queues are bounded rather than cleared while authority is unconfirmed: the\n// config can appear at any time (a first publish, or R2 recovering), and the\n// most recent items are still worth firing when it does. Unbounded, a site whose\n// config 404s accumulates an entry per page view for the life of the tab.\nconst MAX_QUEUE_LENGTH = 50;\n// While unconfirmed, revalidation is spaced by a growing delay instead of riding\n// every queued item. Without it a missing config produces one no-cache request\n// per page view, forever. `focus` / `visibilitychange` still revalidate, so\n// recovery stays prompt once the object exists.\nconst RETRY_BASE_MS = 5_000;\nconst RETRY_MAX_MS = 5 * 60_000;\n\nconst runtimes = new Map<string, TrackingConfigRuntime>();\nconst configuredIds = new Set<string>();\nlet scriptLoad: Promise<void> | null = null;\nlet jsInitialized = false;\n\nfunction cacheKey(url: string): string {\n return `${CACHE_PREFIX}${url}`;\n}\n\n/** Append, dropping the oldest entry once the cap is reached. */\nfunction pushBounded<T>(queue: T[], item: T): void {\n queue.push(item);\n if (queue.length > MAX_QUEUE_LENGTH) queue.splice(0, queue.length - MAX_QUEUE_LENGTH);\n}\n\nfunction readCache(url: string): CachedEntry | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.sessionStorage.getItem(cacheKey(url));\n if (!raw) return null;\n const parsed = JSON.parse(raw) as { etag?: unknown; config?: unknown };\n const config = parseConversionConfig(parsed.config);\n if (!config) return null;\n return {\n etag: typeof parsed.etag === \"string\" ? parsed.etag : null,\n config,\n };\n } catch {\n return null;\n }\n}\n\nfunction writeCache(url: string, entry: CachedEntry): void {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(cacheKey(url), JSON.stringify(entry));\n } catch {\n // sessionStorage unavailable — runtime still works, just without a conditional candidate\n }\n}\n\nfunction isTombstone(config: ConversionConfig): boolean {\n return config.google_tracking_state === \"disabled\" || Object.keys(config.gtag_ids).length === 0;\n}\n\nfunction validateConfig(config: ConversionConfig, ref: TrackingConfigReference): boolean {\n return (\n config.business_id === ref.businessId &&\n config.environment === ref.environment &&\n (config.google_tracking_state === \"active\" || config.google_tracking_state === \"disabled\")\n );\n}\n\nfunction pageViewSnapshot(): QueuedPageView | null {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return null;\n return {\n href: window.location.href,\n title: document.title || null,\n referrer: document.referrer || null,\n };\n}\n\nfunction ensureScript(gtagId: string): Promise<void> {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return Promise.resolve();\n applyDefaultConsentState();\n loadGtagScript(gtagId);\n if (scriptLoad) return scriptLoad;\n scriptLoad = new Promise((resolve) => {\n const script = document.querySelector<HTMLScriptElement>(\n 'script[data-aranova-tracking=\"aranova-gtag-loader\"]',\n );\n if (!script) {\n resolve();\n return;\n }\n if ((script as HTMLScriptElement & { dataset: DOMStringMap }).dataset.loaded === \"true\") {\n resolve();\n return;\n }\n window.setTimeout(resolve, 0);\n script.addEventListener(\n \"load\",\n () => {\n script.dataset.loaded = \"true\";\n resolve();\n },\n { once: true },\n );\n script.addEventListener(\"error\", () => resolve(), { once: true });\n });\n return scriptLoad;\n}\n\nexport class TrackingConfigRuntime {\n private readonly fetchImpl: typeof fetch;\n /** Resolved config URL — see the constructor for why it is computed once. */\n private readonly url: string;\n private current: ConversionConfig | null = null;\n private etag: string | null = null;\n private stateValue: RuntimeState = \"unconfirmed\";\n private confirmedAt = 0;\n private authorityGeneration = 0;\n private inFlight: Promise<void> | null = null;\n private flushInFlight: Promise<void> | null = null;\n private flushRequested = false;\n private retryTimer: number | null = null;\n private started = false;\n /** Consecutive failed authority attempts — drives the revalidate backoff. */\n private authorityFailures = 0;\n /** Epoch ms before which `ensureAuthority` must not issue another request. */\n private nextAuthorityAttemptAt = 0;\n private readonly conversionQueue: QueuedConversion[] = [];\n private readonly automaticQueue: QueuedAutomaticEvent[] = [];\n private readonly pageQueue: QueuedPageView[] = [];\n private readonly listeners = new Set<() => void>();\n\n constructor(\n readonly ref: TrackingConfigReference,\n fetchImpl: typeof fetch = globalThis.fetch,\n ) {\n // Native browser fetch requires Window/WorkerGlobalScope as its receiver.\n // Calling an unbound native fetch stored on `this` throws \"Illegal invocation\".\n this.fetchImpl = fetchImpl.bind(globalThis);\n // Resolved once: the cache key, the fetch target, and the write-back key must\n // all be the same string, or a reference that composes its URL would read a\n // cache entry it never writes.\n this.url = resolveTrackingConfigUrl(ref);\n const cached = readCache(this.url);\n this.current = cached?.config ?? null;\n this.etag = cached?.etag ?? null;\n this.start();\n }\n\n /** The URL this runtime actually fetches (composed or explicit). */\n configUrl(): string {\n return this.url;\n }\n\n /**\n * Explicitly start authority resolution and Google-tag bootstrap.\n *\n * Idempotent so framework effects can call it after hydration without\n * depending on constructor timing.\n */\n start(): void {\n if (this.started) {\n void this.flush();\n return;\n }\n this.started = true;\n void this.revalidate();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"visible\") void this.revalidate();\n });\n window.addEventListener(\"focus\", () => void this.revalidate());\n }\n }\n\n state(): RuntimeState {\n return this.stateValue;\n }\n\n config(): ConversionConfig | null {\n return this.stateValue === \"active\" || this.stateValue === \"tombstone\" ? this.current : null;\n }\n\n __unsafeExpireAuthorityForTests(): void {\n this.confirmedAt = Number.NEGATIVE_INFINITY;\n this.nextAuthorityAttemptAt = 0;\n }\n\n /** Queue depths — asserted by tests to pin the bound. */\n __queueDepthsForTests(): { conversions: number; automatic: number; pages: number } {\n return {\n conversions: this.conversionQueue.length,\n automatic: this.automaticQueue.length,\n pages: this.pageQueue.length,\n };\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n async ensureAuthority(): Promise<boolean> {\n if (this.stateValue !== \"unconfirmed\" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {\n return true;\n }\n // Backoff applies only to this implicit path (driven by queued work). An\n // explicit `revalidate()` — the visibility/focus listeners, or a caller —\n // always goes to the network so recovery is never delayed by the timer.\n if (Date.now() < this.nextAuthorityAttemptAt) return false;\n await this.revalidateAuthority();\n return this.stateValue !== \"unconfirmed\" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;\n }\n\n async revalidate(): Promise<void> {\n this.nextAuthorityAttemptAt = 0;\n await this.revalidateAuthority();\n await this.flush();\n }\n\n private async revalidateAuthority(): Promise<void> {\n if (typeof window === \"undefined\" || !this.fetchImpl) return;\n if (this.inFlight) return this.inFlight;\n this.inFlight = this.revalidateNow().finally(() => {\n this.inFlight = null;\n });\n return this.inFlight;\n }\n\n queuePageView(snapshot: QueuedPageView | null = pageViewSnapshot()): void {\n if (!snapshot) return;\n pushBounded(this.pageQueue, snapshot);\n void this.flush();\n }\n\n fireConversion(key: string, options?: TrackingConfigConversionOptions): void {\n pushBounded(this.conversionQueue, { key, ...options });\n void this.flush();\n }\n\n queueAutomaticEvent(\n eventType: string,\n metadata: Record<string, unknown>,\n transactionPath: string,\n transactionScope: string,\n ): void {\n pushBounded(this.automaticQueue, {\n eventType,\n metadata,\n transactionPath,\n transactionScope,\n });\n void this.flush();\n }\n\n listGoals(): ConversionGoal[] {\n return this.current?.goals ?? [];\n }\n\n private async revalidateNow(): Promise<void> {\n try {\n const headers: Record<string, string> = {};\n if (this.etag) headers[\"If-None-Match\"] = this.etag;\n const response = await this.fetchImpl(this.url, {\n method: \"GET\",\n headers,\n cache: \"no-cache\",\n });\n if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {\n this.confirm(this.current, this.etag);\n return;\n }\n if (!response.ok) {\n this.expireAuthority();\n return;\n }\n const next = parseConversionConfig(await response.json());\n if (!next || !validateConfig(next, this.ref)) {\n this.expireAuthority();\n return;\n }\n if (this.current && next.config_version < this.current.config_version) {\n this.expireAuthority();\n return;\n }\n this.confirm(next, response.headers.get(\"ETag\"));\n } catch {\n this.expireAuthority();\n }\n }\n\n private expireAuthority(): void {\n this.authorityGeneration += 1;\n if (this.stateValue !== \"unconfirmed\") this.stateValue = \"unconfirmed\";\n // Exponential, capped. Queued work keeps accumulating (bounded), so the\n // config appearing later still fires the most recent items.\n this.authorityFailures += 1;\n const delay = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** (this.authorityFailures - 1));\n this.nextAuthorityAttemptAt = Date.now() + delay;\n }\n\n private confirm(config: ConversionConfig, etag: string | null): void {\n this.authorityGeneration += 1;\n this.current = config;\n this.etag = etag;\n this.confirmedAt = Date.now();\n this.authorityFailures = 0;\n this.nextAuthorityAttemptAt = 0;\n this.stateValue = isTombstone(config) ? \"tombstone\" : \"active\";\n writeCache(this.url, { etag, config });\n if (this.stateValue === \"tombstone\") {\n if (this.retryTimer !== null) {\n window.clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n this.conversionQueue.length = 0;\n this.automaticQueue.length = 0;\n this.pageQueue.length = 0;\n }\n for (const listener of this.listeners) listener();\n }\n\n private async flush(): Promise<void> {\n if (this.flushInFlight) {\n this.flushRequested = true;\n return this.flushInFlight;\n }\n this.flushRequested = false;\n this.flushInFlight = this.flushNow().finally(() => {\n this.flushInFlight = null;\n if (this.flushRequested && this.stateValue === \"active\") void this.flush();\n });\n return this.flushInFlight;\n }\n\n private scheduleRetry(): void {\n if (this.retryTimer !== null || typeof window === \"undefined\") return;\n this.retryTimer = window.setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, 1000);\n }\n\n private async flushNow(): Promise<void> {\n if (!(await this.ensureAuthority())) return;\n if (this.stateValue !== \"active\" || !this.current) return;\n const config = this.current;\n const generation = this.authorityGeneration;\n const ids = Object.values(config.gtag_ids).filter(\n (id): id is string => typeof id === \"string\" && isValidGtagId(id),\n );\n if (ids.length === 0) return;\n await ensureScript(ids[0]!);\n if (\n this.authorityGeneration !== generation ||\n this.stateValue !== \"active\" ||\n this.current !== config\n ) {\n return;\n }\n const gtag = window.gtag;\n if (typeof gtag !== \"function\") return;\n try {\n if (!jsInitialized) {\n gtag(\"js\", new Date());\n jsInitialized = true;\n }\n for (const id of ids) {\n if (configuredIds.has(id)) continue;\n gtag(\"config\", id, { send_page_view: false });\n configuredIds.add(id);\n }\n } catch {\n this.scheduleRetry();\n return;\n }\n while (this.pageQueue.length > 0) {\n const page = this.pageQueue[0]!;\n try {\n gtag(\"event\", \"page_view\", {\n page_location: page.href,\n page_title: page.title ?? undefined,\n page_referrer: page.referrer ?? undefined,\n });\n this.pageQueue.shift();\n } catch {\n this.scheduleRetry();\n return;\n }\n }\n while (this.automaticQueue.length > 0) {\n const event = this.automaticQueue[0]!;\n for (const goal of config.goals) {\n if (goal.kind !== \"event\" || !goal.firing) continue;\n if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;\n pushBounded(this.conversionQueue, {\n key: goal.key,\n transactionId: getAutomaticTransactionId(\n event.transactionScope,\n goal.key,\n event.transactionPath,\n ),\n });\n }\n this.automaticQueue.shift();\n }\n while (this.conversionQueue.length > 0) {\n const item = this.conversionQueue[0]!;\n const goal = config.goals.find((g) => g.key === item.key);\n const firing = goal?.firing;\n if (!firing) {\n this.conversionQueue.shift();\n continue;\n }\n const currency = item.currency ?? firing.currency ?? null;\n const value =\n item.value ??\n (firing.value_cents != null && currency\n ? fromMinor(firing.value_cents, currency as SupportedCurrency)\n : null);\n const outcome = fireConversionWithConsent({\n sendTo: firing.send_to,\n value,\n currency,\n transactionId: item.transactionId ?? null,\n });\n if (outcome === \"retryable\") {\n this.scheduleRetry();\n return;\n }\n this.conversionQueue.shift();\n }\n }\n}\n\nexport function getTrackingConfigRuntime(\n ref: TrackingConfigReference,\n fetchImpl?: typeof fetch,\n): TrackingConfigRuntime {\n // Keyed on the RESOLVED url: `{cdnUrl}` and an equivalent\n // `{cdnBaseUrl,businessId,environment}` name the same object and must share one\n // runtime, or each would keep its own authority state and gtag bootstrap.\n const key = `${resolveTrackingConfigUrl(ref)}|${ref.businessId}|${ref.environment}`;\n const existing = runtimes.get(key);\n if (existing) return existing;\n const runtime = new TrackingConfigRuntime(ref, fetchImpl ?? globalThis.fetch);\n runtimes.set(key, runtime);\n return runtime;\n}\n\nexport function resetTrackingConfigRuntimesForTests(): void {\n runtimes.clear();\n configuredIds.clear();\n scriptLoad = null;\n jsInitialized = false;\n}\n","import { z } from \"zod\";\n\nimport { blogThemeOverrideSchema, nullishField } from \"./theme\";\n\n/**\n * Blog wire schemas — the client-side source of truth for the CDN-published\n * blog objects (ENG-31 Area C).\n *\n * {cdnBaseUrl}/blogs/v1/{namespace}/manifest.json (mutable)\n * {cdnBaseUrl}/blogs/v1/{namespace}/posts/{slug}/{content_hash}.json (immutable)\n *\n * Mirrored by the backend publisher and checked by the schema-drift contract\n * (the `resources` section of `events.schema.json`). Field names are FROZEN.\n *\n * These are TOLERANT readers (same philosophy as `conversion-config.ts`): the\n * backend may add fields freely — unknown fields are stripped, malformed\n * optional fields fall back to a safe default via `.catch()`, and only a\n * structurally unusable object makes the parse helpers return `null`. They\n * never throw.\n */\n\n// The wire major, pinned to the version this reader understands. Within v1\n// the contract is additive-only (unknown fields are stripped), but a future\n// v2 object is by definition not readable under v1 assumptions — the literal\n// makes the whole-object parse fail, so `parseBlog*` returns `null` (the\n// tolerant \"unusable object\" path) instead of misreading it.\nconst schemaVersionSchema = z.literal(1);\n\nexport const blogHeroImageSchema = z.object({\n url: z.string(),\n alt: z.string().catch(\"\"),\n width: z.number().int(),\n height: z.number().int(),\n});\n\nexport const blogManifestEntrySchema = z.object({\n slug: z.string(),\n title: z.string(),\n description: z.string().catch(\"\"),\n content_hash: z.string(),\n content_key: z.string(),\n published_at: z.string(),\n updated_at: z.string(),\n // Estimated read time in whole minutes (computed at publish from the post\n // body). 0 when unknown; index cards render \"N min read\".\n reading_minutes: z.number().int().catch(0),\n // A malformed tags array / hero_image degrades gracefully instead of\n // dropping the whole entry.\n tags: z.array(z.string()).catch([]),\n hero_image: blogHeroImageSchema.nullable().catch(null),\n});\n\n// WIRE schema — what the backend publisher actually writes (emitted into\n// `events.schema.json` and drift-checked against the Pydantic `BlogManifest`).\n// The runtime reader below layers per-entry drop-tolerance on top of it: a\n// writer never emits `null` entries, so that tolerance is a READER refinement,\n// deliberately kept out of the wire contract.\nexport const blogManifestWireSchema = z.object({\n schema_version: schemaVersionSchema,\n namespace: z.string(),\n generated_at: z.string(),\n posts: z.array(blogManifestEntrySchema).catch([]),\n});\n\nexport const blogManifestSchema = blogManifestWireSchema.extend({\n // A single malformed entry is dropped; the rest of the manifest survives.\n posts: z\n .array(blogManifestEntrySchema.nullable().catch(null))\n .transform((entries) => entries.filter((e): e is BlogManifestEntry => e !== null))\n .catch([]),\n});\n\nexport const blogImageSchema = z.object({\n id: z.string(),\n url: z.string(),\n alt: z.string().catch(\"\"),\n caption: z.string().nullable().catch(null),\n role: z.enum([\"hero\", \"inline\"]).catch(\"inline\"),\n width: z.number().int(),\n height: z.number().int(),\n mime: z.string().catch(\"\"),\n});\n\nexport const blogSeoSchema = z.object({\n meta_title: z.string().catch(\"\"),\n meta_description: z.string().catch(\"\"),\n canonical_path: z.string().catch(\"\"),\n og_image: z.string().nullable().catch(null),\n});\n\n// WIRE schema — see `blogManifestWireSchema` for the wire/reader split.\nexport const blogPostContentWireSchema = z.object({\n schema_version: schemaVersionSchema,\n slug: z.string(),\n title: z.string(),\n description: z.string().catch(\"\"),\n mdx: z.string(),\n images: z.array(blogImageSchema).catch([]),\n // Already schema-validated at publish time, but re-sanitized on read (and\n // again before rendering) — the override schema itself drops invalid values.\n // Nullish: the backend serializes an unset override as an explicit `null`\n // (Pydantic `model_dump()`); it folds into `undefined` for TS consumers.\n theme_overrides: nullishField(blogThemeOverrideSchema).catch(undefined),\n seo: blogSeoSchema.catch({\n meta_title: \"\",\n meta_description: \"\",\n canonical_path: \"\",\n og_image: null,\n }),\n jsonld: z.record(z.unknown()).catch({}),\n published_at: z.string(),\n // The applied_at of the version this content was published from; `null`\n // when the backend has no separate modified date. Nullish on the wire\n // (Pydantic `model_dump()` emits explicit `null`s), folded to `null` for a\n // single TS sentinel; a malformed value degrades to `null` instead of\n // dropping the post.\n updated_at: z\n .string()\n .nullish()\n .transform((v) => v ?? null)\n .catch(null),\n version_number: z.number().int().catch(0),\n content_hash: z.string(),\n});\n\nexport const blogPostContentSchema = blogPostContentWireSchema.extend({\n // A single malformed image record is dropped; the post survives.\n images: z\n .array(blogImageSchema.nullable().catch(null))\n .transform((images) => images.filter((i): i is BlogImage => i !== null))\n .catch([]),\n});\n\nexport type BlogHeroImage = z.infer<typeof blogHeroImageSchema>;\nexport type BlogManifestEntry = z.infer<typeof blogManifestEntrySchema>;\nexport type BlogManifest = z.infer<typeof blogManifestSchema>;\nexport type BlogImage = z.infer<typeof blogImageSchema>;\nexport type BlogSeo = z.infer<typeof blogSeoSchema>;\nexport type BlogPostContent = z.infer<typeof blogPostContentSchema>;\n\n/** Tolerant parse of a raw manifest object — `null` only when unusable. Never throws. */\nexport function parseBlogManifest(raw: unknown): BlogManifest | null {\n const parsed = blogManifestSchema.safeParse(raw);\n return parsed.success ? parsed.data : null;\n}\n\n/** Tolerant parse of a raw post-content object — `null` only when unusable. Never throws. */\nexport function parseBlogPostContent(raw: unknown): BlogPostContent | null {\n const parsed = blogPostContentSchema.safeParse(raw);\n return parsed.success ? parsed.data : null;\n}\n","import { z } from \"zod\";\n\n/**\n * Blog theme — the typed styling contract for CDN-published blog posts.\n *\n * Three layers resolve into one fully-populated {@link BlogTheme}\n * (later layers win):\n *\n * 1. {@link BASE_BLOG_THEME} — neutral editorial defaults (always complete)\n * 2. the client-registered default theme (client code, per site)\n * 3. per-post `theme_overrides` from the content JSON (backend-authored)\n *\n * Because layers 2 and 3 cross a trust boundary (they end up in inline\n * `style` attributes and CSS custom properties), **sanitization lives in the\n * zod schemas themselves**: every value is validated against a strict grammar\n * (hex/functional colors, unit lengths, clamped weights, token-list font\n * families) and any invalid individual value is DROPPED via `.catch(undefined)`\n * — never passed through to CSS.\n */\n\n// ---------------------------------------------------------------------------\n// Responsive values (Chakra-style breakpoint maps)\n// ---------------------------------------------------------------------------\n\n/**\n * Fixed, SDK-owned breakpoints (mobile-first `min-width` px). Ordered smallest\n * to largest — CSS emission and any future style build must iterate in this\n * order so later (wider) breakpoints cascade over earlier ones.\n */\nexport const BLOG_BREAKPOINTS = [\"sm\", \"md\", \"lg\", \"xl\", \"2xl\", \"3xl\"] as const;\n\nexport type BlogBreakpoint = (typeof BLOG_BREAKPOINTS)[number];\n\nexport const BLOG_BREAKPOINT_MIN_WIDTHS_PX = {\n sm: 640,\n md: 768,\n lg: 1024,\n xl: 1280,\n \"2xl\": 1536,\n \"3xl\": 1920,\n} as const satisfies Record<BlogBreakpoint, number>;\n\n/** A breakpoint map: `base` is required, wider breakpoints are optional. */\nexport type BlogBreakpointMap<T = string> = { base: T } & {\n [K in BlogBreakpoint]?: T | undefined;\n};\n\n/**\n * A dimensional theme token: a plain scalar, or a breakpoint map that\n * resolves mobile-first (`base` below `sm`, each key from its min-width up).\n */\nexport type ResponsiveValue<T = string> = T | BlogBreakpointMap<T>;\n\n/**\n * Normalize a {@link ResponsiveValue} to its map form. The ONE place the\n * scalar-vs-map distinction is handled — downstream consumers (CSS var\n * emission, the style build) work off the map and never `typeof`-sniff.\n */\nexport function toBlogBreakpointMap<T extends string | number>(\n value: ResponsiveValue<T>,\n): BlogBreakpointMap<T> {\n return typeof value === \"object\" ? value : { base: value };\n}\n\n// ---------------------------------------------------------------------------\n// Value grammars (the sanitizer)\n// ---------------------------------------------------------------------------\n\nconst HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;\nconst FUNCTIONAL_COLOR = /^(rgb|rgba|hsl|hsla|oklch)\\([0-9a-zA-Z ,.%/-]*\\)$/;\nconst LENGTH = /^-?\\d+(\\.\\d+)?(px|rem|em|%|ch|vw)$/;\n// line-height additionally allows a unitless multiplier (\"1.7\").\nconst LINE_HEIGHT = /^-?\\d+(\\.\\d+)?(px|rem|em|%|ch|vw)?$/;\n// One font-family token: optionally quoted word characters/spaces/hyphens.\n// Rejects `url(`, `;`, `{`, `}`, `<`, `>` and every other CSS escape vector.\nconst FONT_FAMILY_TOKEN = /^['\"]?[a-zA-Z0-9 -]+['\"]?$/;\n// A single `var(--name)` reference — how next/font sites expose their real\n// (build-mangled) family names, e.g. `var(--font-sans)`. Exactly one balanced\n// paren pair and a custom-property name; no other characters can sneak in.\nconst FONT_FAMILY_VAR_TOKEN = /^var\\(--[a-zA-Z0-9-]+\\)$/;\n\nconst colorValueSchema = z.string().refine((v) => HEX_COLOR.test(v) || FUNCTIONAL_COLOR.test(v), {\n message: \"must be a #hex or rgb()/rgba()/hsl()/hsla()/oklch() color\",\n});\n\nconst lengthValueSchema = z.string().regex(LENGTH, {\n message: \"must be a CSS length (px|rem|em|%|ch|vw)\",\n});\n\nconst lineHeightValueSchema = z.string().regex(LINE_HEIGHT, {\n message: \"must be a CSS length or unitless line-height\",\n});\n\nconst fontWeightSchema = z.number().int().min(100).max(1000);\n\n// Text-alignment keyword (figcaption today). Scalar, non-responsive.\nconst alignValueSchema = z.enum([\"left\", \"center\", \"right\"]);\n\nconst fontFamilySchema = z.string().refine(\n (v) => {\n const tokens = v.split(\",\").map((t) => t.trim());\n return (\n tokens.length > 0 &&\n tokens.every((t) => FONT_FAMILY_TOKEN.test(t) || FONT_FAMILY_VAR_TOKEN.test(t))\n );\n },\n { message: \"must be a comma-separated list of font-family or var(--custom-property) tokens\" },\n);\n\n// The wire truth for every optional field is Pydantic's `model_dump()`, which\n// serializes `Type | None = None` fields as explicit `null`s (never omits\n// them). So every optional field here must be NULL-tolerant, not just\n// absent-tolerant: `.nullish()` + a transform that folds `null` back into\n// `undefined` so downstream TS consumers keep a single \"absent\" sentinel.\n// zod-to-json-schema emits the transform's INPUT schema (nullable), which is\n// exactly what the backend drift test compares against the Pydantic mirror.\ntype NullishField<S extends z.ZodTypeAny> = z.ZodEffects<\n z.ZodOptional<z.ZodNullable<S>>,\n z.output<S> | undefined,\n z.input<S> | null | undefined\n>;\n\nexport function nullishField<S extends z.ZodTypeAny>(schema: S): NullishField<S> {\n return schema.nullish().transform((v) => v ?? undefined) as NullishField<S>;\n}\n\n// A scalar CSS value schema (the tokens' grammars are all string-valued).\ntype ScalarValueSchema = z.ZodType<string, z.ZodTypeDef, string>;\n\ntype BreakpointMapSchema = z.ZodType<\n BlogBreakpointMap<string>,\n z.ZodTypeDef,\n { base: string } & { [K in BlogBreakpoint]?: string | null | undefined }\n>;\n\n// Breakpoint map for one token. `base` is required and validated with the\n// token's own grammar; each breakpoint key is optional, null-tolerant (wire\n// nulls) and individually DROPPED when invalid (`.catch(undefined)`) — the\n// same per-value sanitization philosophy as the rest of this file. Unknown\n// keys are stripped by zod's default object behavior.\nfunction breakpointMapSchema(valueSchema: ScalarValueSchema): BreakpointMapSchema {\n return z.object({\n base: valueSchema,\n sm: nullishField(valueSchema).catch(undefined),\n md: nullishField(valueSchema).catch(undefined),\n lg: nullishField(valueSchema).catch(undefined),\n xl: nullishField(valueSchema).catch(undefined),\n \"2xl\": nullishField(valueSchema).catch(undefined),\n \"3xl\": nullishField(valueSchema).catch(undefined),\n }) as unknown as BreakpointMapSchema;\n}\n\ntype ResponsiveSchema = z.ZodUnion<[ScalarValueSchema, BreakpointMapSchema]>;\n\n/**\n * Schema factory for a responsive token: the token's scalar grammar OR a\n * {@link BlogBreakpointMap} of it. A map missing/failing `base` fails the\n * whole union (override parsing then drops the entire field via the\n * field-level `.catch`); an invalid non-base breakpoint value is dropped\n * per-value while the rest of the map survives.\n */\nexport function responsive(valueSchema: ScalarValueSchema): ResponsiveSchema {\n return z.union([valueSchema, breakpointMapSchema(valueSchema)]);\n}\n\n// ---------------------------------------------------------------------------\n// Full theme schemas (strict — every field required and valid)\n// ---------------------------------------------------------------------------\n\n// Flat color shape (every key → the plain color grammar). The full schema,\n// the deep-partial dark palette, and the override schema all derive from this\n// ONE shape so the optional `heading` never gets double-wrapped in nullish\n// layers (which would emit nested anyOfs in the JSON Schema snapshot).\nconst blogThemeColorsShape = {\n background: colorValueSchema,\n surface: colorValueSchema,\n text: colorValueSchema,\n text_muted: colorValueSchema,\n primary: colorValueSchema,\n primary_foreground: colorValueSchema,\n link: colorValueSchema,\n border: colorValueSchema,\n code_background: colorValueSchema,\n heading: colorValueSchema,\n} as const;\n\nexport const blogThemeColorsSchema = z.object({\n ...blogThemeColorsShape,\n // The ONE optional color in a resolved theme: when unset, headings fall back\n // to `text` in the stylesheet (`--_heading-color`), so `blogThemeToCssVars`\n // only emits `--ara-blog-color-heading` when a theme actually sets it.\n heading: nullishField(colorValueSchema),\n});\n\nexport const blogThemeTypographySchema = z.object({\n heading_family: fontFamilySchema,\n body_family: fontFamilySchema,\n heading_weight: fontWeightSchema,\n body_weight: fontWeightSchema,\n h1_size: responsive(lengthValueSchema),\n h2_size: responsive(lengthValueSchema),\n h3_size: responsive(lengthValueSchema),\n h4_size: responsive(lengthValueSchema),\n body_size: responsive(lengthValueSchema),\n body_line_height: responsive(lineHeightValueSchema),\n small_size: responsive(lengthValueSchema),\n // Scalar (non-responsive) length; the grammar allows negative em values.\n // h1/h2 consume it directly, h3/h4 at half strength (calc in the base CSS).\n heading_letter_spacing: lengthValueSchema,\n // Figure caption text alignment (left/center/right). Site-owner override;\n // default center in the base CSS if unset.\n caption_align: alignValueSchema,\n});\n\nexport const blogThemeSpacingSchema = z.object({\n content_max_width: responsive(lengthValueSchema),\n block_gap: responsive(lengthValueSchema),\n section_gap: responsive(lengthValueSchema),\n});\n\n// Deep-partial companion to `blogThemeColorsSchema` for the optional dark\n// palette: each color is individually optional AND null-tolerant (see\n// `nullishField`), mirroring the Pydantic side's `str | None = None` fields.\nconst blogThemeColorsPartialSchema = z.object(\n Object.fromEntries(\n Object.keys(blogThemeColorsShape).map((key) => [key, nullishField(colorValueSchema)]),\n ) as { [K in keyof typeof blogThemeColorsShape]: NullishField<typeof colorValueSchema> },\n);\n\nexport const blogThemeSchema = z.object({\n colors: blogThemeColorsSchema,\n colors_dark: nullishField(blogThemeColorsPartialSchema),\n typography: blogThemeTypographySchema,\n spacing: blogThemeSpacingSchema,\n radius: responsive(lengthValueSchema),\n});\n\nexport type BlogThemeColors = z.infer<typeof blogThemeColorsSchema>;\nexport type BlogThemeTypography = z.infer<typeof blogThemeTypographySchema>;\nexport type BlogThemeSpacing = z.infer<typeof blogThemeSpacingSchema>;\nexport type BlogTheme = z.infer<typeof blogThemeSchema>;\n\n// ---------------------------------------------------------------------------\n// Override schema (deep-partial + per-field drop-on-invalid)\n// ---------------------------------------------------------------------------\n\n// Each field is nullish (`null` on the wire — the backend's `model_dump()`\n// emits explicit nulls for unset fields — folds into `undefined`) +\n// `.catch(undefined)`: an invalid individual value is dropped (falls back to\n// the layer below at resolve time) without failing the group or the whole\n// override. A non-object group likewise drops to undefined.\nfunction dropInvalidFields<T extends z.ZodRawShape>(\n shape: T,\n): { [K in keyof T]: z.ZodCatch<NullishField<T[K]>> } {\n const out: Record<string, z.ZodTypeAny> = {};\n for (const [key, schema] of Object.entries(shape)) {\n out[key] = nullTolerant(schema).catch(undefined);\n }\n return out as { [K in keyof T]: z.ZodCatch<NullishField<T[K]>> };\n}\n\n// Null-tolerant wrapper that keeps responsive unions FLAT on the wire.\n// Wrapping a union in `.nullable()` would emit nested `anyOf`s\n// (`anyOf: [anyOf: [scalar, map], null]`) in the JSON Schema snapshot, while\n// the Pydantic mirror's `str | Map | None` emits a flat three-branch `anyOf`\n// — so for unions we splice `z.null()` into the union itself instead.\nfunction nullTolerant(schema: z.ZodTypeAny): z.ZodTypeAny {\n if (schema instanceof z.ZodUnion) {\n const options = (schema as z.ZodUnion<[z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]>).options;\n return z\n .union([...options, z.null()])\n .optional()\n .transform((v) => v ?? undefined);\n }\n return nullishField(schema);\n}\n\n// Built from the flat shape (NOT blogThemeColorsSchema.shape) so the optional\n// `heading` gets exactly one nullish layer, like every other override field.\nconst blogThemeColorsOverrideSchema = z.object(dropInvalidFields(blogThemeColorsShape));\nconst blogThemeTypographyOverrideSchema = z.object(\n dropInvalidFields(blogThemeTypographySchema.shape),\n);\nconst blogThemeSpacingOverrideSchema = z.object(dropInvalidFields(blogThemeSpacingSchema.shape));\n\nexport const blogThemeOverrideSchema = z.object(\n dropInvalidFields({\n colors: blogThemeColorsOverrideSchema,\n colors_dark: blogThemeColorsOverrideSchema,\n typography: blogThemeTypographyOverrideSchema,\n spacing: blogThemeSpacingOverrideSchema,\n radius: responsive(lengthValueSchema),\n }),\n);\n\nexport type BlogThemeOverride = z.infer<typeof blogThemeOverrideSchema>;\n\n// ---------------------------------------------------------------------------\n// Base theme — neutral editorial defaults\n// ---------------------------------------------------------------------------\n\nexport const BASE_BLOG_THEME: BlogTheme = {\n colors: {\n background: \"#ffffff\",\n surface: \"#f5f5f5\",\n text: \"#171717\",\n text_muted: \"#6b6b6b\",\n primary: \"#171717\",\n primary_foreground: \"#ffffff\",\n link: \"#1d4ed8\",\n border: \"#e5e5e5\",\n code_background: \"#f5f5f5\",\n },\n typography: {\n heading_family: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n body_family: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n // Responsive defaults: comfortable mobile sizes, larger from `md` up.\n // This replaces the hardcoded mobile media query the base CSS used to need.\n h1_size: { base: \"1.9rem\", md: \"2.25rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.75rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1rem\",\n body_line_height: \"1.7\",\n small_size: \"0.875rem\",\n heading_letter_spacing: \"-0.02em\",\n caption_align: \"center\",\n },\n spacing: {\n content_max_width: \"42rem\",\n block_gap: \"1.25rem\",\n section_gap: \"3rem\",\n },\n radius: \"8px\",\n};\n\n// ---------------------------------------------------------------------------\n// Functions\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitize an untrusted theme(-override) object. Invalid individual values are\n * dropped; total garbage (non-object, arrays, …) yields `{}`. Never throws.\n */\nexport function sanitizeBlogTheme(raw: unknown): BlogThemeOverride {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const parsed = blogThemeOverrideSchema.safeParse(raw);\n return parsed.success ? parsed.data : {};\n}\n\nfunction mergeGroup<T extends Record<string, unknown>>(base: T, over: Partial<T> | undefined): T {\n if (!over) return base;\n const out = { ...base };\n for (const [key, value] of Object.entries(over)) {\n if (value !== undefined) (out as Record<string, unknown>)[key] = value;\n }\n return out;\n}\n\n/**\n * Deep-merge override layers onto {@link BASE_BLOG_THEME}. Later layers win;\n * `null`/`undefined` layers and `undefined` fields are skipped, so the result\n * is always a fully-populated {@link BlogTheme}.\n *\n * Responsive tokens merge at TOKEN granularity: a later layer's value —\n * scalar or breakpoint map — REPLACES the whole token. There is deliberately\n * no per-breakpoint merge (a layer overriding `h1_size: \"2rem\"` on top of a\n * base map must not inherit the base's `md` entry).\n */\nexport function resolveBlogTheme(\n ...layers: Array<BlogThemeOverride | null | undefined>\n): BlogTheme {\n let resolved: BlogTheme = {\n colors: { ...BASE_BLOG_THEME.colors },\n colors_dark: BASE_BLOG_THEME.colors_dark ? { ...BASE_BLOG_THEME.colors_dark } : undefined,\n typography: { ...BASE_BLOG_THEME.typography },\n spacing: { ...BASE_BLOG_THEME.spacing },\n radius: BASE_BLOG_THEME.radius,\n };\n for (const layer of layers) {\n if (!layer) continue;\n resolved = {\n colors: mergeGroup(resolved.colors, layer.colors),\n colors_dark:\n layer.colors_dark || resolved.colors_dark\n ? mergeGroup(resolved.colors_dark ?? {}, layer.colors_dark)\n : undefined,\n typography: mergeGroup(resolved.typography, layer.typography),\n spacing: mergeGroup(resolved.spacing, layer.spacing),\n radius: layer.radius ?? resolved.radius,\n };\n }\n return resolved;\n}\n\n// (split/join instead of String#replaceAll — the package lib target is ES2020.)\nfunction kebab(key: string): string {\n return key.split(\"_\").join(\"-\");\n}\n\n// Emit one (possibly responsive) token: the base var carries the scalar (or\n// the map's `base`), and each PRESENT breakpoint key adds a `-<bp>` suffixed\n// var (e.g. `--ara-blog-type-h1-size-md`). Absent breakpoints emit nothing —\n// the stylesheet's media queries fall back to the base var.\nfunction emitResponsiveVar(\n vars: Record<string, string>,\n name: string,\n value: ResponsiveValue<string | number>,\n): void {\n const map = toBlogBreakpointMap(value);\n vars[name] = String(map.base);\n for (const bp of BLOG_BREAKPOINTS) {\n const bpValue = map[bp];\n if (bpValue !== undefined) vars[`${name}-${bp}`] = String(bpValue);\n }\n}\n\n/**\n * Flatten a resolved theme into `--ara-blog-*` CSS custom properties for an\n * inline `style` attribute. When `colors_dark` is present, color values are\n * emitted as `light-dark(lightVal, darkVal)` and `color-scheme: light dark`\n * is included so the browser picks the active side. Responsive tokens emit\n * their base var plus `-sm`/`-md`/… suffixed vars for present breakpoints.\n */\nexport function blogThemeToCssVars(theme: BlogTheme): Record<string, string> {\n const vars: Record<string, string> = {};\n const dark = theme.colors_dark;\n const hasDark = !!dark && Object.values(dark).some((v) => v !== undefined);\n\n // Union of light + dark keys: an optional color (only `heading` today) set\n // ONLY on the dark side still needs its var emitted.\n const colorKeys = new Set([...Object.keys(theme.colors), ...(dark ? Object.keys(dark) : [])]);\n for (const key of colorKeys) {\n const light = theme.colors[key as keyof BlogThemeColors];\n const darkValue = hasDark ? dark?.[key as keyof BlogThemeColors] : undefined;\n // Optional colors (only `heading` today) emit nothing when unset — the\n // stylesheet's fallback (`--_heading-color` → text) takes over. A theme\n // that sets ONLY the dark side still gets a var: the light side falls\n // back to `text`, which is what the stylesheet would use anyway.\n const lightValue = light ?? (darkValue !== undefined ? theme.colors.text : undefined);\n if (lightValue === undefined) continue;\n vars[`--ara-blog-color-${kebab(key)}`] = darkValue\n ? `light-dark(${lightValue}, ${darkValue})`\n : lightValue;\n }\n // camelCase: these vars land in a React `style` prop, which rejects kebab\n // names for real (non-custom) CSS properties.\n if (hasDark) vars.colorScheme = \"light dark\";\n\n for (const [key, value] of Object.entries(theme.typography)) {\n emitResponsiveVar(vars, `--ara-blog-type-${kebab(key)}`, value);\n }\n for (const [key, value] of Object.entries(theme.spacing)) {\n emitResponsiveVar(vars, `--ara-blog-space-${kebab(key)}`, value);\n }\n emitResponsiveVar(vars, \"--ara-blog-radius\", theme.radius);\n return vars;\n}\n","import { registerCapability } from \"../../capabilities\";\nimport { DEFAULT_CDN_BASE_URL } from \"../tracking-config-runtime\";\nimport {\n parseBlogManifest,\n parseBlogPostContent,\n type BlogManifest,\n type BlogManifestEntry,\n type BlogPostContent,\n} from \"./schema\";\n\n/**\n * Blog CDN client — read-only fetchers over the published blog objects.\n *\n * Designed for Next.js server components but framework-free: the Next-specific\n * `next.revalidate` fetch extension is expressed as a local structural type\n * ({@link NextFetchInit}) so we never import `next` types and a plain\n * `fetch` (Node, tests) simply ignores the extra property.\n *\n * Error policy: reads are best-effort. Network failures, non-2xx responses,\n * and unparseable bodies all resolve to `null` / `[]` — a blog page must\n * degrade, never crash the host site.\n */\n\n/** `RequestInit` + the Next.js data-cache extension (structural — no `next` import). */\nexport type NextFetchInit = RequestInit & {\n next?: { revalidate?: number | false; tags?: string[] };\n};\n\nexport interface BlogClientConfig {\n /**\n * Reserved for future authenticated reads (draft previews fetched from the\n * API instead of the public CDN). Published content is world-readable, so\n * the key is currently NOT sent — but it is part of the config contract so\n * client sites don't need a breaking config change when draft reads ship.\n */\n apiKey: string;\n /**\n * CDN origin, e.g. `https://cdn.aranova.io` (trailing slash tolerated).\n * Blank or whitespace falls back to {@link DEFAULT_CDN_BASE_URL} — the same\n * contract the tracking-config resolver has had since PR #180. The blog\n * client originally shipped without the fallback, so an unset env var made\n * the manifest URL relative; server-side fetch rejected it and the fail-soft\n * error policy rendered a published blog as permanently empty.\n */\n cdnBaseUrl: string;\n /** Opaque per-business namespace under `blogs/v1/`. */\n namespace: string;\n /** Manifest ISR revalidate window (seconds). Default 300. */\n manifestRevalidateSeconds?: number;\n /** Injectable for tests / non-global-fetch runtimes. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface BlogClient {\n /** The mutable manifest (index of published posts), or `null` on any miss. */\n getManifest(): Promise<BlogManifest | null>;\n /** Manifest entries sorted `published_at` desc. `[]` on any miss. */\n listPosts(): Promise<BlogManifestEntry[]>;\n /** Full post content for a slug, or `null` when unknown/unfetchable. */\n getPost(slug: string): Promise<BlogPostContent | null>;\n /** `generateStaticParams()`-shaped slug list. */\n getStaticParams(): Promise<Array<{ slug: string }>>;\n}\n\nexport function createBlogClient(config: BlogClientConfig): BlogClient {\n registerCapability(\"blog_rendering\");\n // Blank-as-unset, mirroring `resolveTrackingConfigUrl`: a present-but-empty\n // env var must not produce a relative (same-origin) manifest URL.\n const base = (config.cdnBaseUrl.trim() || DEFAULT_CDN_BASE_URL).replace(/\\/+$/, \"\");\n const doFetch: typeof fetch = (...args) => (config.fetchImpl ?? fetch)(...args);\n\n async function fetchJson(url: string, init: NextFetchInit): Promise<unknown | null> {\n try {\n const response = await doFetch(url, init);\n if (!response.ok) return null;\n return (await response.json()) as unknown;\n } catch {\n return null;\n }\n }\n\n async function getManifest(): Promise<BlogManifest | null> {\n // MUTABLE object (Cache-Control max-age=60 at the CDN) — cached through the\n // Next.js data cache with a revalidate window instead of force-cache.\n const init = {\n next: { revalidate: config.manifestRevalidateSeconds ?? 300 },\n } satisfies NextFetchInit;\n const raw = await fetchJson(`${base}/blogs/v1/${config.namespace}/manifest.json`, init);\n return raw === null ? null : parseBlogManifest(raw);\n }\n\n async function listPosts(): Promise<BlogManifestEntry[]> {\n const manifest = await getManifest();\n if (!manifest) return [];\n return [...manifest.posts].sort((a, b) => b.published_at.localeCompare(a.published_at));\n }\n\n async function getPost(slug: string): Promise<BlogPostContent | null> {\n const manifest = await getManifest();\n const entry = manifest?.posts.find((post) => post.slug === slug);\n if (!entry) return null;\n // IMMUTABLE object (hash-addressed, max-age=1y) — cache forever; a content\n // change publishes a new hash, which the manifest points at.\n const init = { cache: \"force-cache\" } satisfies NextFetchInit;\n const raw = await fetchJson(`${base}/${entry.content_key.replace(/^\\/+/, \"\")}`, init);\n return raw === null ? null : parseBlogPostContent(raw);\n }\n\n async function getStaticParams(): Promise<Array<{ slug: string }>> {\n const posts = await listPosts();\n return posts.map((post) => ({ slug: post.slug }));\n }\n\n return { getManifest, listPosts, getPost, getStaticParams };\n}\n","// Curated font/type-scale presets for the blog theme — React-free, part of\n// the `@aranova/tracking-next/blog` surface (re-exported via blog-public.ts).\n//\n// Each preset is a plain `BlogThemeOverride`: a font pairing with REAL\n// web-safe fallback stacks, a responsive type scale, weights, letter-spacing,\n// spacing, and radius. Colors are deliberately (almost entirely) UNSET so the\n// client site's own palette wins — the only exception is a subtle\n// `code_background` where the code surface is part of the preset's identity.\n//\n// Contract (tested in __tests__/presets.test.ts): every preset passes\n// `sanitizeBlogTheme(preset)` UNCHANGED — a preset must never rely on values\n// the sanitizer would drop.\n\nimport type { BlogThemeOverride } from \"./theme\";\n\nexport type BlogThemePresetName = \"editorial\" | \"modern\" | \"warm\" | \"technical\";\n\n/**\n * Named theme presets. Layer one under your site palette:\n *\n * ```ts\n * const theme = resolveBlogTheme(\n * createBlogFontTheme(BLOG_THEME_PRESETS.editorial, {\n * heading: \"var(--font-serif)\", // next/font variable\n * body: \"var(--font-sans)\",\n * }),\n * { colors: { primary: \"#0f766e\", link: \"#0f766e\" } },\n * );\n * ```\n */\nexport const BLOG_THEME_PRESETS: Record<BlogThemePresetName, BlogThemeOverride> = {\n /** Serif display headings over a humanist sans body — long-form magazine feel. */\n editorial: {\n typography: {\n heading_family: \"'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif\",\n body_family: \"'Source Sans 3', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n heading_weight: 600,\n body_weight: 400,\n h1_size: { base: \"2rem\", md: \"2.5rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.875rem\" },\n h3_size: \"1.3rem\",\n h4_size: \"1.125rem\",\n body_size: \"1.0625rem\",\n body_line_height: \"1.75\",\n heading_letter_spacing: \"-0.015em\",\n },\n spacing: {\n block_gap: \"1.35rem\",\n section_gap: \"3.25rem\",\n },\n radius: \"6px\",\n },\n /** Geometric sans pairing (Inter-first) — tight, contemporary product voice. */\n modern: {\n typography: {\n heading_family: \"'Inter', 'SF Pro Display', 'Segoe UI', Roboto, sans-serif\",\n body_family: \"'Inter', 'SF Pro Text', 'Segoe UI', Roboto, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n h1_size: { base: \"2.1rem\", md: \"2.75rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.75rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1rem\",\n body_line_height: \"1.7\",\n heading_letter_spacing: \"-0.03em\",\n },\n spacing: {\n block_gap: \"1.25rem\",\n section_gap: \"3rem\",\n },\n radius: \"12px\",\n },\n /** Rounded-friendly pairing with a softer scale — approachable, human tone. */\n warm: {\n typography: {\n heading_family: \"'Nunito', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif\",\n body_family: \"'Nunito Sans', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n h1_size: { base: \"1.9rem\", md: \"2.35rem\" },\n h2_size: { base: \"1.45rem\", md: \"1.7rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1.0625rem\",\n body_line_height: \"1.75\",\n heading_letter_spacing: \"-0.01em\",\n },\n spacing: {\n block_gap: \"1.35rem\",\n section_gap: \"3.25rem\",\n },\n radius: \"16px\",\n },\n /** Workmanlike sans with a prominent code surface — docs/engineering feel. */\n technical: {\n colors: {\n // The one preset that ships a color: a cool code surface is part of the\n // \"technical\" identity. Everything else stays on the site palette.\n code_background: \"#eef1f5\",\n },\n colors_dark: {\n code_background: \"#1b1f27\",\n },\n typography: {\n heading_family: \"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n body_family: \"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n heading_weight: 600,\n body_weight: 400,\n h1_size: { base: \"1.85rem\", md: \"2.25rem\" },\n h2_size: { base: \"1.45rem\", md: \"1.65rem\" },\n h3_size: \"1.2rem\",\n h4_size: \"1.0625rem\",\n body_size: \"1rem\",\n body_line_height: \"1.65\",\n heading_letter_spacing: \"-0.01em\",\n },\n spacing: {\n block_gap: \"1.15rem\",\n section_gap: \"2.75rem\",\n },\n radius: \"6px\",\n },\n};\n\n/** Real font sources for a preset: next/font CSS variables (or literal family names). */\nexport interface BlogFontSources {\n /** e.g. `\"var(--font-serif)\"` from `next/font`'s `variable` option. */\n heading?: string;\n /** e.g. `\"var(--font-sans)\"`. */\n body?: string;\n}\n\n// Prepend a caller-supplied family (usually a next/font `var(--…)`) onto the\n// preset's fallback stack. The caller value may itself be a comma-separated\n// stack; either way the preset's web-safe fallbacks ride behind it.\nfunction prependFamily(front: string | undefined, stack: string | undefined): string | undefined {\n if (!front) return stack;\n return stack ? `${front}, ${stack}` : front;\n}\n\n/**\n * Wire a site's real fonts into a preset: returns a NEW override (no\n * mutation) whose heading/body families are `\"<font>, <preset fallback\n * stack>\"` — pass your `next/font` variable names and the preset's web-safe\n * fallbacks keep covering the pre-hydration/fallback paint.\n *\n * ```ts\n * const serif = Source_Serif_4({ subsets: [\"latin\"], variable: \"--font-serif\" });\n * const sans = Inter({ subsets: [\"latin\"], variable: \"--font-sans\" });\n * const override = createBlogFontTheme(BLOG_THEME_PRESETS.editorial, {\n * heading: \"var(--font-serif)\",\n * body: \"var(--font-sans)\",\n * });\n * ```\n */\nexport function createBlogFontTheme(\n preset: BlogThemeOverride,\n fonts: BlogFontSources,\n): BlogThemeOverride {\n const heading_family = prependFamily(fonts.heading, preset.typography?.heading_family);\n const body_family = prependFamily(fonts.body, preset.typography?.body_family);\n return {\n ...preset,\n typography: {\n ...preset.typography,\n ...(heading_family !== undefined ? { heading_family } : {}),\n ...(body_family !== undefined ? { body_family } : {}),\n },\n };\n}\n","// Client-side className overrides for the blog view — the escape hatch for\n// sites that want to hook a Tailwind/utility class onto a specific surface\n// without replacing the whole component (see `components` for that) or\n// writing global CSS against the fixed `.aranova-blog-*` selectors (still the\n// simplest option and requires no API — these classes are stable and public).\n//\n// `joinClassNames` is a plain append, not a Tailwind-merge: every base class\n// here is an SDK-internal selector (`aranova-blog-callout`, …), never a\n// utility class, so there is nothing for an override to conflict with —\n// dropping a `tailwind-merge` dependency into a framework-free package for a\n// non-conflict is unnecessary weight.\nexport function joinClassNames(base: string, extra?: string): string {\n return extra ? `${base} ${extra}` : base;\n}\n\n/**\n * One optional override per styleable surface. Every key merges onto its\n * fixed base class (never replaces it), so the `.aranova-blog-*` selector\n * still applies — this is additive, not a restyle mechanism.\n */\nexport interface BlogClassNameOverrides {\n /** The `<article>` root. */\n article?: string;\n /** The content/aside grid wrapper. */\n layout?: string;\n /** The content column. */\n content?: string;\n /** The aside rail (table of contents). */\n aside?: string;\n /** `<Callout>` MDX component. */\n callout?: string;\n /** `<Cta>` MDX component. */\n cta?: string;\n /** `<BlogImage>` MDX component's `<figure>` wrapper. */\n figure?: string;\n /** `<FaqItem>` MDX component's `<details>` wrapper. */\n faqItem?: string;\n /** `<FaqItem>`'s `<summary>`. */\n faqQuestion?: string;\n /** `<FaqItem>`'s answer `<div>`. */\n faqAnswer?: string;\n /** `<KeyTakeaways>` MDX component. */\n keyTakeaways?: string;\n /** The `<table>` overflow wrapper `<div>`. */\n tableWrap?: string;\n /** `BlogPostMeta` byline row. */\n meta?: string;\n /** `BlogBreadcrumbs` nav. */\n breadcrumbs?: string;\n /** `BlogTocShell` / the animated `TocPanel` island. */\n toc?: string;\n /** The `ReadingProgress` island. */\n progress?: string;\n}\n","import type { BlogManifestEntry, BlogPostContent } from \"./schema\";\n\n/**\n * SEO builders — React-free helpers that turn post content into Next.js\n * `Metadata`-shaped objects, BlogPosting JSON-LD, and sitemap entries.\n *\n * Deliberately does NOT import `next` types: {@link BlogPostMetadata} is a\n * local structural interface whose shape is spread-compatible with Next's\n * `Metadata`, so the helpers stay usable from any framework (and from the\n * internal dashboard's preview renderer).\n */\n\nexport interface BlogSeoOptions {\n /** Site origin, e.g. `https://client.example.com`. */\n siteUrl: string;\n /** Route the blog is mounted at. Default `/blog`. */\n basePath?: string;\n}\n\nexport interface BlogJsonLdOptions extends BlogSeoOptions {\n /** Emits `publisher: { \"@type\": \"Organization\", name }` when given. */\n publisherName?: string;\n}\n\nexport interface BlogPostMetadataImage {\n url: string;\n width?: number;\n height?: number;\n alt?: string;\n}\n\n/** Structural subset of Next.js `Metadata` — spread it into `generateMetadata`. */\nexport interface BlogPostMetadata {\n title: string;\n description: string;\n alternates: { canonical: string };\n openGraph: {\n title: string;\n description: string;\n type: \"article\";\n url: string;\n images?: BlogPostMetadataImage[];\n publishedTime?: string;\n };\n}\n\nexport interface BlogSitemapEntry {\n url: string;\n lastModified: string;\n}\n\n/** Join URL segments without doubling or dropping slashes. */\nfunction joinUrl(base: string, ...paths: Array<string | undefined>): string {\n let out = base.replace(/\\/+$/, \"\");\n for (const path of paths) {\n const segment = path?.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (segment) out += `/${segment}`;\n }\n return out;\n}\n\n// The canonical URL is ALWAYS derived from the client site's own routing\n// (`siteUrl` + `basePath` + slug) — the client mounts the route, so client\n// routing wins. The backend's `seo.canonical_path` is informational only: it\n// assumes a `/blog` mount and would emit a canonical pointing at a\n// non-existent path on sites mounted elsewhere (e.g. `/news`). This also\n// keeps metadata, JSON-LD, and sitemap URLs derived from one rule.\nfunction canonicalUrl(post: BlogPostContent, opts: BlogSeoOptions): string {\n return joinUrl(opts.siteUrl, opts.basePath ?? \"/blog\", post.slug);\n}\n\nfunction ogImages(post: BlogPostContent): BlogPostMetadataImage[] | undefined {\n if (post.seo.og_image) return [{ url: post.seo.og_image }];\n const hero = post.images.find((image) => image.role === \"hero\");\n if (!hero) return undefined;\n return [{ url: hero.url, width: hero.width, height: hero.height, alt: hero.alt }];\n}\n\n/** Build a Next `Metadata`-shaped object for a post's `generateMetadata`. */\nexport function buildBlogPostMetadata(\n post: BlogPostContent,\n opts: BlogSeoOptions,\n): BlogPostMetadata {\n const url = canonicalUrl(post, opts);\n const title = post.seo.meta_title || post.title;\n const description = post.seo.meta_description || post.description;\n const images = ogImages(post);\n return {\n title,\n description,\n alternates: { canonical: url },\n openGraph: {\n title,\n description,\n type: \"article\",\n url,\n ...(images ? { images } : {}),\n ...(post.published_at ? { publishedTime: post.published_at } : {}),\n },\n };\n}\n\n/**\n * Build a `BlogPosting` JSON-LD object. When the backend shipped a non-empty\n * `post.jsonld`, its values are spread over the locally derived defaults so\n * backend-authored enrichment (author, keywords, …) wins — EXCEPT the pinned\n * fields spread after it: `@context`/`@type` (type identity of the emitted\n * document) and `url`/`mainEntityOfPage` (client-domain truths derived from\n * the site's own routing) can never be overridden by backend data.\n */\nexport function buildBlogPostingJsonLd(\n post: BlogPostContent,\n opts: BlogJsonLdOptions,\n): Record<string, unknown> {\n const url = canonicalUrl(post, opts);\n const images = post.images.map((image) => image.url);\n const doc: Record<string, unknown> = {\n headline: post.title,\n description: post.seo.meta_description || post.description,\n datePublished: post.published_at,\n dateModified: post.updated_at ?? post.published_at,\n ...(images.length > 0 ? { image: images } : {}),\n ...(opts.publisherName\n ? { publisher: { \"@type\": \"Organization\", name: opts.publisherName } }\n : {}),\n };\n const backend = post.jsonld;\n const merged =\n backend && typeof backend === \"object\" && Object.keys(backend).length > 0\n ? { ...doc, ...backend }\n : doc;\n return {\n ...merged,\n \"@context\": \"https://schema.org\",\n \"@type\": \"BlogPosting\",\n url,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": url },\n };\n}\n\nexport interface BlogFaqPair {\n question: string;\n answer: string;\n}\n\n// `<FaqItem question=\"…\">…</FaqItem>` (or '…') with the answer captured lazily.\nconst FAQ_ITEM_RE = /<FaqItem\\s+question=(?:\"([^\"]*)\"|'([^']*)')\\s*>([\\s\\S]*?)<\\/FaqItem>/g;\n// Fenced code blocks — FaqItem text inside a fence is code, not an FAQ.\nconst CODE_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[^\\n]*\\n[\\s\\S]*?(?:^ {0,3}\\1`* *$|(?![\\s\\S]))/gm;\n// Component tags + inline code markers stripped from answer prose.\nconst ANSWER_TAG_RE = /<[^>]*>/g;\n\n/**\n * Extract `<FaqItem question>` question/answer pairs from a post's MDX.\n *\n * Deliberately a light regex, not a parser: it runs on documents that have\n * ALREADY passed `validateBlogMdx` at both the persist and render boundaries,\n * where `<FaqItem>` is guaranteed to carry exactly one quoted-string\n * `question` attribute and flow-markdown children. On anything else it simply\n * extracts fewer/no pairs — it never renders content, so there is no\n * injection surface. Fenced code blocks are dropped first so documentation\n * ABOUT the component is not read as an FAQ. Answers are flattened to plain\n * text (tags stripped, markdown markers left as-is, whitespace collapsed) —\n * Google accepts plain-text `acceptedAnswer.text`.\n */\nexport function extractFaqPairs(mdx: string): BlogFaqPair[] {\n const withoutCode = mdx.replaceAll(CODE_FENCE_RE, \"\");\n const pairs: BlogFaqPair[] = [];\n for (const match of withoutCode.matchAll(FAQ_ITEM_RE)) {\n const question = (match[1] ?? match[2] ?? \"\").trim();\n const answer = match[3].replaceAll(ANSWER_TAG_RE, \" \").replaceAll(/\\s+/g, \" \").trim();\n if (question && answer) pairs.push({ question, answer });\n }\n return pairs;\n}\n\n/**\n * Build a schema.org `FAQPage` JSON-LD object from extracted FAQ pairs\n * (see {@link extractFaqPairs}). Returns `null` when there are no pairs —\n * an empty `FAQPage` is invalid structured data, so callers can gate the\n * `<script type=\"application/ld+json\">` emission on the return value.\n */\nexport function buildFaqJsonLd(pairs: BlogFaqPair[]): Record<string, unknown> | null {\n if (pairs.length === 0) return null;\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: pairs.map((pair) => ({\n \"@type\": \"Question\",\n name: pair.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: pair.answer },\n })),\n };\n}\n\nconst JSONLD_ESCAPES: Record<string, string> = {\n \"<\": \"\\\\u003c\",\n \">\": \"\\\\u003e\",\n \"&\": \"\\\\u0026\",\n \"\\u2028\": \"\\\\u2028\",\n \"\\u2029\": \"\\\\u2029\",\n};\n\n/**\n * Serialize a JSON-LD document for embedding in a\n * `<script type=\"application/ld+json\">` tag. Escapes `<`, `>`, `&` and the\n * U+2028/U+2029 line separators as `\\uXXXX` sequences (valid JSON, identical\n * parse result) so backend-influenced content can never break out of the\n * script context (`</script>` injection) or produce invalid JS source.\n */\nexport function serializeJsonLd(data: Record<string, unknown>): string {\n return JSON.stringify(data).replaceAll(/[<>&\\u2028\\u2029]/g, (ch) => JSONLD_ESCAPES[ch] ?? ch);\n}\n\n/** Sitemap rows for every published post (shape matches Next's `MetadataRoute.Sitemap`). */\nexport function buildBlogSitemapEntries(\n posts: BlogManifestEntry[],\n opts: BlogSeoOptions,\n): BlogSitemapEntry[] {\n return posts.map((post) => ({\n url: joinUrl(opts.siteUrl, opts.basePath ?? \"/blog\", post.slug),\n lastModified: post.updated_at,\n }));\n}\n","// React-free table-of-contents + reading-time helpers for blog MDX. Pure mdast\n// walk over the SAME front-end the validator uses (remark-parse + remark-mdx),\n// so heading extraction can never disagree with what the renderer compiles.\n//\n// Slug parity contract: ids are produced with a fresh `GithubSlugger` per\n// document, walked in document order over h2/h3 — EXACTLY what `rehype-slug`\n// does at render time in `BlogPostView`... almost. rehype-slug slugs EVERY\n// heading rank (h1–h6), and duplicate-slug `-N` suffixes are allocated from\n// that full sequence. To keep extracted ids byte-identical to rendered ids,\n// the walk below feeds every heading through the slugger but only REPORTS\n// h2/h3 (the ranks a table of contents links to). Verified by the corpus\n// parity test in `__tests__/toc.test.ts`.\n\nimport GithubSlugger from \"github-slugger\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\n\n/** One table-of-contents entry: an h2/h3 with its rendered anchor id. */\nexport interface BlogHeading {\n /** Heading rank — a TOC only links section (h2) and subsection (h3) heads. */\n depth: 2 | 3;\n /** Plain-text heading content (formatting stripped, code/emphasis flattened). */\n text: string;\n /** The `id` rehype-slug stamps on the rendered heading element. */\n id: string;\n}\n\n// Minimal structural view of the mdast nodes we walk (same approach as\n// validate-mdx.ts — keeps the public d.ts free of @types/mdast).\ninterface MdastNode {\n type: string;\n depth?: number;\n value?: string;\n children?: MdastNode[];\n}\n\n/** Textual leaf types whose `value` contributes to a heading's visible text. */\nconst TEXT_LEAF_TYPES = new Set([\"text\", \"inlineCode\"]);\n\nfunction headingText(node: MdastNode): string {\n let text = \"\";\n const stack: MdastNode[] = [];\n const roots = node.children ?? [];\n for (let i = roots.length - 1; i >= 0; i--) {\n stack.push(roots[i]);\n }\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current) break;\n if (TEXT_LEAF_TYPES.has(current.type) && typeof current.value === \"string\") {\n text += current.value;\n }\n if (Array.isArray(current.children)) {\n for (let i = current.children.length - 1; i >= 0; i--) {\n stack.push(current.children[i]);\n }\n }\n }\n return text;\n}\n\n/**\n * Extract the h2/h3 outline of a blog MDX document with the exact anchor ids\n * `BlogPostView` renders (rehype-slug parity — see the module header). Returns\n * `[]` for unparseable input; feed it the same `mdx` the view receives.\n */\nexport function extractBlogHeadings(mdx: string): BlogHeading[] {\n let tree: MdastNode;\n try {\n tree = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMdx)\n .parse(mdx) as unknown as MdastNode;\n } catch {\n return [];\n }\n\n const slugger = new GithubSlugger();\n const headings: BlogHeading[] = [];\n\n // Document-order walk. Headings are flow content — they appear at the root\n // or inside containers (blockquote, list, Callout) — and rehype-slug slugs\n // them wherever they are, so the walk must descend everywhere.\n const stack: MdastNode[] = [tree];\n while (stack.length > 0) {\n const node = stack.pop();\n if (!node) break;\n if (node.type === \"heading\" && typeof node.depth === \"number\") {\n const text = headingText(node);\n // Every rank consumes a slug (dedupe-counter parity with rehype-slug),\n // but only h2/h3 land in the TOC.\n const id = slugger.slug(text);\n if (node.depth === 2 || node.depth === 3) {\n headings.push({ depth: node.depth, text, id });\n }\n }\n if (Array.isArray(node.children)) {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n return headings;\n}\n\n/** Average adult reading speed used for the estimate, in words per minute. */\nconst WORDS_PER_MINUTE = 220;\n\n// Crude MDX-syntax strip for word counting: JSX tags, code fences/backticks,\n// heading/list/quote markers, link targets. Word-count precision beyond this\n// is noise at a \"N min read\" granularity.\nconst MDX_SYNTAX_RE = /<[^>]*>|```[^`]*```|`|^[#>\\-*+\\s]+|\\]\\([^)]*\\)|[[\\]!]/gm;\n\n/**\n * Estimated reading time of a blog MDX document, in whole minutes (>= 1).\n * Deliberately crude — strips MDX/markdown syntax and counts words at\n * {@link WORDS_PER_MINUTE}.\n */\nexport function estimateReadingTime(mdx: string): number {\n const words = mdx\n .replaceAll(MDX_SYNTAX_RE, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 0).length;\n return Math.max(1, Math.round(words / WORDS_PER_MINUTE));\n}\n","// Mechanical MDX grammar enforcement for generated blog posts — Layer 1 (the\n// authoritative render boundary). Pure and framework-free: no React, no\n// next-mdx-remote. `BlogPostView` runs this unconditionally BEFORE compileMDX;\n// the API mirrors the same grammar at persist time\n// (apps/api/src/services/blog_mdx_validation.py).\n//\n// Why parse here at all: next-mdx-remote v6 strips expression *evaluation*,\n// but intrinsic JSX/HTML elements (<script src>, <iframe>, event-handler\n// attributes, javascript: URLs) flow straight into served HTML — stored XSS\n// on customer domains. The component allow-list passed to the renderer is not\n// a boundary (intrinsics bypass it), and prompt rules are only instructions.\n//\n// No parser differential by construction: we parse with the SAME mdast\n// front-end @mdx-js/mdx uses internally (remark-parse + remark-mdx, versions\n// aligned with what next-mdx-remote resolves), so whatever the compiler would\n// see, this validator saw first. The grammar is default-deny: any node type\n// not explicitly whitelisted rejects the document.\n\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\n\nexport interface BlogMdxViolation {\n /** Stable machine-readable rule id (safe to surface — never raw content). */\n rule: string;\n message: string;\n line?: number;\n}\n\nexport type BlogMdxValidationResult = { ok: true } | { ok: false; violations: BlogMdxViolation[] };\n\n/** DoS guard: maximum accepted MDX source size, in UTF-16 code units. */\nexport const BLOG_MDX_MAX_SIZE = 500_000;\n/** DoS guard: maximum AST nesting depth (realistic posts stay under ~10). */\nexport const BLOG_MDX_MAX_DEPTH = 64;\n/** DoS guard: maximum total AST node count. */\nexport const BLOG_MDX_MAX_NODES = 100_000;\n/** Cap on reported violations so a hostile document can't flood the caller. */\nconst MAX_VIOLATIONS = 50;\n\n/**\n * One allowed component of the blog MDX grammar, in serializable form.\n * `attrs` is the EXACT attribute set — every attribute is required, nothing\n * else allowed; `selfClosing` components must have no children.\n *\n * `cssClass`/`promptUsage`/`promptDescription` exist SOLELY so this one\n * record can be the entire cross-language/cross-surface registry — see the\n * grammar doc comment below for what derives from each field.\n */\nexport interface BlogMdxComponentGrammarEntry {\n name: string;\n attrs: readonly string[];\n urlAttrs: readonly string[];\n selfClosing: boolean;\n /** Base CSS class the default renderer applies, when the component is styled. */\n cssClass?: string;\n /** Example JSX usage shown to the drafting/editing agents. */\n promptUsage: string;\n /** One-line description of when/how to use it, shown to the agents. */\n promptDescription: string;\n}\n\n/**\n * The component grammar — the SINGLE source of truth for the closed\n * component set (name/attrs/urlAttrs/selfClosing/cssClass/prompt copy). This\n * is the only place to touch when adding, removing, or changing a component;\n * everything else derives from it instead of hand-mirroring it:\n *\n * - the MDX validator specs below (grammar enforcement),\n * - the `BlogComponents` renderer contract (a mapped-type assertion in\n * `view/components.tsx` makes an entry without a renderer a compile error),\n * - the `resources.blog_mdx_grammar` snapshot in `events.schema.json`\n * (emitted verbatim by `src/events/build-schema.ts`), which\n * `apps/api/src/services/blog_mdx_components.py` is GENERATED from\n * (`apps/api/scripts/generate_blog_mdx_grammar.py`) — the backend has no\n * hand-typed mirror to drift out of sync,\n * - the agent prompt templates (`blog/_mdx_grammar.j2`, via the generated\n * Python module's `prompt_usage`/`prompt_description` fields),\n * - `styles.css` (a completeness test asserts every `cssClass` here has a\n * rule), and the MDX conformance corpus (a completeness test asserts every\n * `name` here has at least one accept-case fixture).\n */\nexport const BLOG_MDX_COMPONENT_GRAMMAR = [\n {\n name: \"Callout\",\n attrs: [],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-callout\",\n promptUsage: \"<Callout>...</Callout>\",\n promptDescription: \"a highlighted tip/note block; no attributes.\",\n },\n {\n name: \"Cta\",\n attrs: [\"href\", \"label\"],\n urlAttrs: [\"href\"],\n selfClosing: true,\n cssClass: \"aranova-blog-cta\",\n promptUsage: '<Cta href=\"...\" label=\"...\" />',\n promptDescription: \"a call-to-action button (self-closing).\",\n },\n {\n name: \"BlogImage\",\n attrs: [\"id\"],\n urlAttrs: [],\n selfClosing: true,\n promptUsage: '<BlogImage id=\"...\" />',\n promptDescription:\n \"a generated image placeholder (self-closing). The `id` references an \" +\n \"image brief id from the plan; the renderer resolves it.\",\n },\n {\n name: \"FaqItem\",\n attrs: [\"question\"],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-faq-item\",\n promptUsage: '<FaqItem question=\"...\">...</FaqItem>',\n promptDescription:\n \"one FAQ entry: the required `question` attribute carries the question, \" +\n \"the children are the prose answer. Group consecutive `<FaqItem>`s \" +\n \"under an FAQ heading near the end of the post when the topic suits a Q&A section.\",\n },\n {\n name: \"KeyTakeaways\",\n attrs: [],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-key-takeaways\",\n promptUsage: \"<KeyTakeaways>...</KeyTakeaways>\",\n promptDescription:\n \"a TL;DR box containing a standard markdown bullet list of 3-5 takeaways; \" +\n \"place it right after the H1 (and hero image).\",\n },\n] as const satisfies readonly BlogMdxComponentGrammarEntry[];\n\n/** Union of the allowed component names, derived from the grammar. */\nexport type BlogMdxComponentName = (typeof BLOG_MDX_COMPONENT_GRAMMAR)[number][\"name\"];\n\n/** The closed component set of the blog grammar (mirrors the agent prompts). */\nexport const BLOG_MDX_ALLOWED_COMPONENTS: readonly BlogMdxComponentName[] =\n BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => entry.name);\n\n/**\n * Standard-markdown mdast node types the grammar accepts. Everything else —\n * including `html`, the MDX expression/ESM node types, and any type added by\n * a future plugin — is rejected by default.\n */\nconst ALLOWED_NODE_TYPES = new Set([\n \"root\",\n \"heading\",\n \"paragraph\",\n \"text\",\n \"emphasis\",\n \"strong\",\n \"delete\",\n \"link\",\n \"image\",\n \"linkReference\",\n \"imageReference\",\n \"definition\",\n \"list\",\n \"listItem\",\n \"blockquote\",\n \"code\",\n \"inlineCode\",\n \"table\",\n \"tableRow\",\n \"tableCell\",\n \"thematicBreak\",\n \"break\",\n]);\n\n/** Named rejections for the node types that are the actual attack surface. */\nconst FORBIDDEN_NODE_TYPES: Record<string, { rule: string; message: string }> = {\n html: { rule: \"raw-html\", message: \"Raw HTML is not allowed\" },\n mdxjsEsm: { rule: \"esm\", message: \"import/export statements are not allowed\" },\n mdxFlowExpression: { rule: \"expression\", message: \"JSX expressions ({…}) are not allowed\" },\n mdxTextExpression: { rule: \"expression\", message: \"JSX expressions ({…}) are not allowed\" },\n};\n\ninterface ComponentSpec {\n /** Exact attribute set — every attribute is required, nothing else allowed. */\n attrs: readonly string[];\n /** Attributes whose (string) value must pass the URL policy. */\n urlAttrs: readonly string[];\n /** Self-closing components must have no children. */\n allowChildren: boolean;\n}\n\nconst COMPONENT_SPECS: Record<string, ComponentSpec> = Object.fromEntries(\n BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => [\n entry.name,\n { attrs: entry.attrs, urlAttrs: entry.urlAttrs, allowChildren: !entry.selfClosing },\n ]),\n);\n\nconst URL_SCHEME_RE = /^([A-Za-z][A-Za-z0-9+.-]*):/;\nconst ALLOWED_URL_SCHEMES = new Set([\"http\", \"https\", \"mailto\"]);\n\n/**\n * Scheme whitelist: http(s)/mailto/relative (schemeless) only. ASCII control\n * characters and whitespace are stripped BEFORE scheme detection — browsers\n * ignore them when parsing URLs, so `java\\tscript:` must not slip through.\n * Returns the offending scheme, or null when the URL is acceptable.\n */\nfunction forbiddenUrlScheme(rawUrl: string): string | null {\n // Intentional control-character class — the whole point is stripping the\n // characters browsers ignore in URLs before the scheme check.\n // eslint-disable-next-line no-control-regex\n const cleaned = rawUrl.replaceAll(/[\\u0000-\\u0020\\u007F]/g, \"\");\n const match = URL_SCHEME_RE.exec(cleaned);\n if (!match) return null; // relative / fragment / protocol-relative\n const scheme = match[1].toLowerCase();\n return ALLOWED_URL_SCHEMES.has(scheme) ? null : scheme;\n}\n\n// Loose structural view of the mdast/MDX node shapes we inspect. Kept local\n// (not the full @types/mdast surface) so the public d.ts stays dependency-free.\ninterface MdxAstAttribute {\n type: string; // \"mdxJsxAttribute\" | \"mdxJsxExpressionAttribute\"\n name?: string;\n value?: unknown; // string | null | { type: \"mdxJsxAttributeValueExpression\" }\n}\n\ninterface MdxAstNode {\n type: string;\n children?: MdxAstNode[];\n url?: string;\n name?: string | null;\n attributes?: MdxAstAttribute[];\n position?: { start?: { line?: number } };\n depth?: number;\n}\n\nfunction nodeLine(node: MdxAstNode): number | undefined {\n return node.position?.start?.line;\n}\n\nfunction checkComponent(node: MdxAstNode, violations: BlogMdxViolation[]): void {\n const line = nodeLine(node);\n const name = node.name;\n\n if (!name) {\n violations.push({\n rule: \"unknown-component\",\n message: \"JSX fragments (<>…</>) are not allowed\",\n line,\n });\n return;\n }\n\n const spec = COMPONENT_SPECS[name];\n if (!spec) {\n violations.push({\n rule: \"unknown-component\",\n message: `<${name}> is not an allowed element (allowed: ${BLOG_MDX_ALLOWED_COMPONENTS.join(\", \")})`,\n line,\n });\n return;\n }\n\n const seen = new Set<string>();\n for (const attr of node.attributes ?? []) {\n if (attr.type !== \"mdxJsxAttribute\" || typeof attr.name !== \"string\") {\n // Spread attributes ({...props}) or anything unrecognized.\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> has a non-literal attribute`,\n line,\n });\n continue;\n }\n if (!spec.attrs.includes(attr.name)) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> does not accept attribute \"${attr.name}\"`,\n line,\n });\n continue;\n }\n if (typeof attr.value !== \"string\") {\n // null = bare attribute; object = {expression} value.\n violations.push({\n rule: \"component-attribute\",\n message: `<${name} ${attr.name}> must be a quoted string literal`,\n line,\n });\n continue;\n }\n seen.add(attr.name);\n if (spec.urlAttrs.includes(attr.name)) {\n const scheme = forbiddenUrlScheme(attr.value);\n if (scheme !== null) {\n violations.push({\n rule: \"url-scheme\",\n message: `<${name} ${attr.name}> uses forbidden URL scheme \"${scheme}:\"`,\n line,\n });\n }\n }\n }\n\n for (const required of spec.attrs) {\n if (!seen.has(required)) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> requires attribute \"${required}\"`,\n line,\n });\n }\n }\n\n if (!spec.allowChildren && (node.children?.length ?? 0) > 0) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> must be self-closing (no children)`,\n line,\n });\n }\n}\n\nexport interface BlogMdxValidateOptions {\n /**\n * Reject any content before the H1 (frontmatter, a stray metadata block,\n * prose) — only meaningful for a whole post. Off by default because the\n * cross-language conformance corpus exercises single components in\n * isolation, not full documents; the render boundary (BlogPostView, the\n * only production caller) always validates a whole post and passes `true`.\n */\n requireLeadingH1?: boolean;\n}\n\n/**\n * Validate a generated blog MDX document against the frozen content grammar:\n * standard markdown + exactly the components in\n * {@link BLOG_MDX_COMPONENT_GRAMMAR} (`<Callout>`, `<Cta href label/>`,\n * `<BlogImage id/>`, `<FaqItem question>`, `<KeyTakeaways>`). Rejects ESM,\n * JSX expressions, raw HTML, every other JSX/HTML element, non-literal\n * attributes, and non-http(s)/mailto/relative URLs. Pure and synchronous —\n * safe anywhere (RSC, node, edge).\n */\nexport function validateBlogMdx(\n mdx: string,\n options?: BlogMdxValidateOptions,\n): BlogMdxValidationResult {\n const violations: BlogMdxViolation[] = [];\n\n if (typeof mdx !== \"string\" || mdx.length > BLOG_MDX_MAX_SIZE) {\n return {\n ok: false,\n violations: [\n {\n rule: \"size-limit\",\n message: `Document exceeds the ${BLOG_MDX_MAX_SIZE}-character limit`,\n },\n ],\n };\n }\n\n let tree: MdxAstNode;\n try {\n tree = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMdx)\n .parse(mdx) as unknown as MdxAstNode;\n } catch (error) {\n const line = (error as { line?: number }).line;\n return {\n ok: false,\n violations: [\n {\n rule: \"parse-error\",\n message: \"Document is not valid MDX\",\n ...(typeof line === \"number\" ? { line } : {}),\n },\n ],\n };\n }\n\n if (options?.requireLeadingH1) {\n const firstChild = tree.children?.[0];\n if (!firstChild || firstChild.type !== \"heading\" || firstChild.depth !== 1) {\n violations.push({\n rule: \"leading-h1\",\n message:\n \"Document must start with the H1 title (`# ...`) — no frontmatter or metadata block before it\",\n line: nodeLine(firstChild ?? tree),\n });\n }\n }\n\n // Iterative default-deny walk with explicit depth tracking (DoS guard —\n // never recurse on attacker-shaped input).\n let nodeCount = 0;\n const stack: Array<{ node: MdxAstNode; depth: number }> = [{ node: tree, depth: 0 }];\n\n while (stack.length > 0 && violations.length < MAX_VIOLATIONS) {\n const item = stack.pop();\n if (!item) break;\n const { node, depth } = item;\n\n nodeCount += 1;\n if (nodeCount > BLOG_MDX_MAX_NODES) {\n violations.push({ rule: \"size-limit\", message: \"Document has too many nodes\" });\n break;\n }\n if (depth > BLOG_MDX_MAX_DEPTH) {\n violations.push({ rule: \"depth-limit\", message: \"Document nests too deeply\" });\n break;\n }\n\n const forbidden = FORBIDDEN_NODE_TYPES[node.type];\n if (forbidden) {\n violations.push({ ...forbidden, line: nodeLine(node) });\n continue; // never descend into forbidden subtrees\n }\n\n if (node.type === \"mdxJsxFlowElement\" || node.type === \"mdxJsxTextElement\") {\n checkComponent(node, violations);\n // Descend regardless: Callout children are flow content that must obey\n // the same grammar, and even rejected subtrees should report every rule\n // they break (useful pipeline feedback; the doc is already invalid).\n } else if (!ALLOWED_NODE_TYPES.has(node.type)) {\n violations.push({\n rule: \"disallowed-node\",\n message: `Markdown construct \"${node.type}\" is not allowed`,\n line: nodeLine(node),\n });\n continue;\n }\n\n if (\n (node.type === \"link\" || node.type === \"image\" || node.type === \"definition\") &&\n typeof node.url === \"string\"\n ) {\n const scheme = forbiddenUrlScheme(node.url);\n if (scheme !== null) {\n violations.push({\n rule: \"url-scheme\",\n message: `${node.type} uses forbidden URL scheme \"${scheme}:\"`,\n line: nodeLine(node),\n });\n }\n }\n\n if (Array.isArray(node.children)) {\n for (const child of node.children) {\n stack.push({ node: child, depth: depth + 1 });\n }\n }\n }\n\n return violations.length === 0 ? { ok: true } : { ok: false, violations };\n}\n","// Async React Server Component — no 'use client'. Rendered on the server (or\n// at build time) by Next.js App Router pages and by the internal dashboard's\n// draft preview. Never ships MDX compilation to the browser.\n\nimport { compileMDX } from \"next-mdx-remote/rsc\";\nimport rehypeSlug from \"rehype-slug\";\nimport remarkGfm from \"remark-gfm\";\nimport type { CSSProperties, ReactNode } from \"react\";\n\nimport { CAPABILITY_DOM_ATTRIBUTE } from \"../../../capabilities\";\nimport { BASE_BLOG_THEME, blogThemeToCssVars, type BlogTheme } from \"../theme\";\nimport { validateBlogMdx } from \"../validate-mdx\";\nimport { type BlogClassNameOverrides, joinClassNames } from \"./class-names\";\nimport { createDefaultComponents, type BlogComponents } from \"./components\";\nimport { BLOG_BASE_CSS } from \"./styles\";\n\n// Blog rendering happens entirely server-side against the CDN, so it never\n// calls the Aranova API and neither the ingest payload nor the request-header\n// path can observe it. Stamping the rendered article is how the capability\n// crosses to the browser, where the tracking client picks it up on its next\n// batch. Applied to the error paths too: a failed post still proves the\n// renderer is wired up.\nconst CAPABILITY_MARKER = { [CAPABILITY_DOM_ATTRIBUTE]: \"blog_rendering\" } as const;\n\nexport interface BlogPostViewProps {\n /** Raw MDX source (the `mdx` field of the post content JSON). */\n mdx: string;\n /** A fully-RESOLVED theme (see `resolveBlogTheme`). Default: `BASE_BLOG_THEME`. */\n theme?: BlogTheme;\n /** Overrides merged over the default component set. */\n components?: Partial<BlogComponents>;\n /** Article chrome rendered above the content/aside layout (breadcrumbs, meta, …). */\n header?: ReactNode;\n /** Sidebar chrome (e.g. `BlogTocShell`); omitted entirely when not provided. */\n aside?: ReactNode;\n /** Entrance-motion opt-in, stamped as `data-motion` for the base CSS. Default `\"none\"`. */\n motion?: \"none\" | \"subtle\";\n /** Site-supplied classes merged onto the fixed base classes (additive, per surface). */\n classNames?: BlogClassNameOverrides;\n /**\n * Reject content before the H1 (frontmatter, a stray metadata block, prose)\n * — only meaningful for a whole post. Default `true`; direct-fragment\n * callers (component-grammar tests) pass `false`.\n */\n requireLeadingH1?: boolean;\n /** Which side the aside rail sits on at desktop width. Default `\"right\"`. */\n tocSide?: \"left\" | \"right\";\n}\n\n/**\n * Renders MDX inside a themed `<article class=\"aranova-blog-post\">`. The theme\n * flows in as sanitized `--ara-blog-*` CSS variables on the article element;\n * `BLOG_BASE_CSS` (a static constant — nothing interpolated) consumes them.\n *\n * Structure: `{header}` chrome first, then a `.aranova-blog-layout` holding the\n * `.aranova-blog-content` column and (when provided) an `.aranova-blog-aside`\n * rail — the base CSS turns the layout into a sticky-aside grid on wide\n * viewports and keeps it a plain block on mobile.\n *\n * MDX is compiled via `compileMDX` — the awaitable form of `<MDXRemote/>` from\n * `next-mdx-remote/rsc` — because an element-level `<MDXRemote/>` compiles\n * during React's render, where a malformed document would throw past any\n * try/catch here. `rehype-slug` stamps GitHub-style anchor ids on every\n * heading (slug parity with `extractBlogHeadings` in ../toc.ts). On compile\n * failure a typed fallback article renders instead; this component never\n * throws into the host page.\n */\nexport async function BlogPostView({\n mdx,\n theme = BASE_BLOG_THEME,\n components,\n header,\n aside,\n motion = \"none\",\n classNames,\n requireLeadingH1 = true,\n tocSide = \"right\",\n}: BlogPostViewProps): Promise<ReactNode> {\n // Custom-property keys (\"--ara-blog-*\") are valid at runtime but not part of\n // the typed CSSProperties surface — a shape cast (not `any`) bridges that.\n const style = blogThemeToCssVars(theme) as CSSProperties;\n // Shadowed entirely by `components` when called via BlogPost (already a\n // complete, image-aware set) — only matters for a direct BlogPostView call.\n const merged = { ...createDefaultComponents([], classNames), ...components };\n\n // Unconditional grammar enforcement BEFORE any MDX compilation — the\n // component allow-list is not a security boundary (intrinsic <script>/\n // <iframe>/… render through it), so a document that violates the frozen\n // blog grammar never reaches compileMDX. Only rule names are surfaced;\n // the raw content is never echoed into the fallback.\n const validation = validateBlogMdx(mdx, { requireLeadingH1 });\n if (!validation.ok) {\n const rules = Array.from(new Set(validation.violations.map((v) => v.rule))).join(\", \");\n return (\n <article\n className={joinClassNames(\"aranova-blog-post\", classNames?.article)}\n data-error=\"mdx-validation-failed\"\n style={style}\n {...CAPABILITY_MARKER}\n >\n <style>{BLOG_BASE_CSS}</style>\n <p>This post could not be displayed. (blocked: {rules})</p>\n </article>\n );\n }\n\n let content: ReactNode;\n try {\n const compiled = await compileMDX({\n source: mdx,\n components: merged,\n options: { mdxOptions: { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSlug] } },\n });\n content = compiled.content;\n } catch {\n return (\n <article\n className={joinClassNames(\"aranova-blog-post\", classNames?.article)}\n data-error=\"mdx-render-failed\"\n style={style}\n {...CAPABILITY_MARKER}\n >\n <style>{BLOG_BASE_CSS}</style>\n <p>This post could not be displayed.</p>\n </article>\n );\n }\n\n return (\n <article\n className={joinClassNames(\"aranova-blog-post\", classNames?.article)}\n data-motion={motion}\n style={style}\n {...CAPABILITY_MARKER}\n >\n <style>{BLOG_BASE_CSS}</style>\n {header}\n <div\n className={joinClassNames(\"aranova-blog-layout\", classNames?.layout)}\n data-toc-side={tocSide}\n >\n <div className={joinClassNames(\"aranova-blog-content\", classNames?.content)}>{content}</div>\n {aside ? (\n <aside className={joinClassNames(\"aranova-blog-aside\", classNames?.aside)}>{aside}</aside>\n ) : null}\n </div>\n </article>\n );\n}\n","// Server-only MDX component set for the blog post view. No 'use client' — these\n// are plain (server-renderable) components; interactivity is deliberately out of\n// scope for published blog content.\n\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\n\nimport type { BlogImage } from \"../schema\";\nimport type { BlogMdxComponentName } from \"../validate-mdx\";\nimport { type BlogClassNameOverrides, joinClassNames } from \"./class-names\";\n\n/**\n * The CLOSED component interface the MDX renderer accepts. Generated MDX may\n * only reference these elements/components; anything else fails MDX rendering\n * (caught by the view's fallback). Callers may override any subset via the\n * `components` prop on `BlogPost` / `BlogPostView`.\n */\nexport interface BlogComponents {\n h1: (props: ComponentPropsWithoutRef<\"h1\">) => ReactNode;\n h2: (props: ComponentPropsWithoutRef<\"h2\">) => ReactNode;\n h3: (props: ComponentPropsWithoutRef<\"h3\">) => ReactNode;\n h4: (props: ComponentPropsWithoutRef<\"h4\">) => ReactNode;\n h5: (props: ComponentPropsWithoutRef<\"h5\">) => ReactNode;\n h6: (props: ComponentPropsWithoutRef<\"h6\">) => ReactNode;\n p: (props: ComponentPropsWithoutRef<\"p\">) => ReactNode;\n a: (props: ComponentPropsWithoutRef<\"a\">) => ReactNode;\n img: (props: ComponentPropsWithoutRef<\"img\">) => ReactNode;\n pre: (props: ComponentPropsWithoutRef<\"pre\">) => ReactNode;\n code: (props: ComponentPropsWithoutRef<\"code\">) => ReactNode;\n blockquote: (props: ComponentPropsWithoutRef<\"blockquote\">) => ReactNode;\n ul: (props: ComponentPropsWithoutRef<\"ul\">) => ReactNode;\n ol: (props: ComponentPropsWithoutRef<\"ol\">) => ReactNode;\n li: (props: ComponentPropsWithoutRef<\"li\">) => ReactNode;\n table: (props: ComponentPropsWithoutRef<\"table\">) => ReactNode;\n hr: (props: ComponentPropsWithoutRef<\"hr\">) => ReactNode;\n /** Styled aside for tips/notes: `<Callout>…</Callout>` in MDX. */\n Callout: (props: { children?: ReactNode }) => ReactNode;\n /** Primary-colored button-styled anchor: `<Cta href label/>` in MDX. */\n Cta: (props: { href: string; label: string }) => ReactNode;\n /** Renders an image from the post's image records by id: `<BlogImage id/>`. */\n BlogImage: (props: { id: string }) => ReactNode;\n /** One FAQ entry (question heading + prose answer children): `<FaqItem question>…</FaqItem>`. */\n FaqItem: (props: { question: string; children?: ReactNode }) => ReactNode;\n /** Styled TL;DR box holding a markdown bullet list: `<KeyTakeaways>…</KeyTakeaways>`. */\n KeyTakeaways: (props: { children?: ReactNode }) => ReactNode;\n}\n\n/**\n * Compile-time tie between the MDX component grammar and the renderer set:\n * indexing {@link BlogComponents} with every grammar component name fails to\n * typecheck if a component is added to `BLOG_MDX_COMPONENT_GRAMMAR` without a\n * corresponding renderer key here (spec-without-renderer = compile error).\n */\nexport type BlogGrammarComponentRenderers = {\n [Name in BlogMdxComponentName]: BlogComponents[Name];\n};\n\n/**\n * Default renderers. `images` is closed over so `<BlogImage id/>` in MDX can\n * resolve the post's image records without threading props through MDX.\n *\n * Images render as plain `<img>` (with intrinsic width/height + lazy loading),\n * NOT `next/image` — tracking-core is framework-free, and the CDN host would\n * otherwise need to be whitelisted in every consumer's `next.config`. A Next\n * site wanting `next/image` can override `img`/`BlogImage` via `components`.\n *\n * `classNames` merges a site-supplied class onto the fixed base class of each\n * styleable component (see {@link BlogClassNameOverrides}) — additive, never\n * a replacement of the `.aranova-blog-*` selector.\n */\nexport function createDefaultComponents(\n images: BlogImage[],\n classNames?: BlogClassNameOverrides,\n): BlogComponents {\n const imagesById = new Map(images.map((image) => [image.id, image]));\n\n return {\n // `children` is destructured explicitly so static a11y analysis\n // (heading-has-content, anchor-has-content) can see the content flow.\n h1: ({ children, ...props }) => <h1 {...props}>{children}</h1>,\n h2: ({ children, ...props }) => <h2 {...props}>{children}</h2>,\n h3: ({ children, ...props }) => <h3 {...props}>{children}</h3>,\n h4: ({ children, ...props }) => <h4 {...props}>{children}</h4>,\n h5: ({ children, ...props }) => <h5 {...props}>{children}</h5>,\n h6: ({ children, ...props }) => <h6 {...props}>{children}</h6>,\n p: ({ children, ...props }) => <p {...props}>{children}</p>,\n a: ({ children, ...props }) => <a {...props}>{children}</a>,\n img: ({ alt = \"\", ...props }) => <img loading=\"lazy\" alt={alt} {...props} />,\n pre: ({ children, ...props }) => <pre {...props}>{children}</pre>,\n code: ({ children, ...props }) => <code {...props}>{children}</code>,\n blockquote: ({ children, ...props }) => <blockquote {...props}>{children}</blockquote>,\n ul: ({ children, ...props }) => <ul {...props}>{children}</ul>,\n ol: ({ children, ...props }) => <ol {...props}>{children}</ol>,\n li: ({ children, ...props }) => <li {...props}>{children}</li>,\n // Wrapped so wide tables scroll horizontally inside the article instead of\n // overflowing the host page (the wrapper also carries border + radius).\n table: ({ children, ...props }) => (\n <div className={joinClassNames(\"aranova-blog-table-wrap\", classNames?.tableWrap)}>\n <table {...props}>{children}</table>\n </div>\n ),\n hr: (props) => <hr {...props} />,\n Callout: ({ children }) => (\n <aside className={joinClassNames(\"aranova-blog-callout\", classNames?.callout)}>\n {children}\n </aside>\n ),\n Cta: ({ href, label }) => (\n // `data-aranova-cta` opts this into the tracking SDK's cta_click\n // auto-capture (default selector `[data-aranova-cta]`) with the label as\n // the cta_name — zero site config. Inert if auto-capture is off.\n <a\n className={joinClassNames(\"aranova-blog-cta\", classNames?.cta)}\n href={href}\n data-aranova-cta={label}\n data-aranova-section=\"blog\"\n >\n {label}\n </a>\n ),\n BlogImage: ({ id }) => {\n const image = imagesById.get(id);\n if (!image) return null; // unknown id: degrade silently, never crash the page\n return (\n <figure className={classNames?.figure}>\n <img\n src={image.url}\n alt={image.alt}\n width={image.width}\n height={image.height}\n loading=\"lazy\"\n />\n {image.caption ? <figcaption>{image.caption}</figcaption> : null}\n </figure>\n );\n },\n // Native disclosure accordion — no JS, server-renderable. The h3 inside\n // <summary> is valid HTML and keeps the document outline / a11y heading.\n FaqItem: ({ question, children }) => (\n <details className={joinClassNames(\"aranova-blog-faq-item\", classNames?.faqItem)}>\n <summary className={joinClassNames(\"aranova-blog-faq-question\", classNames?.faqQuestion)}>\n <h3>{question}</h3>\n </summary>\n <div className={joinClassNames(\"aranova-blog-faq-answer\", classNames?.faqAnswer)}>\n {children}\n </div>\n </details>\n ),\n KeyTakeaways: ({ children }) => (\n <aside className={joinClassNames(\"aranova-blog-key-takeaways\", classNames?.keyTakeaways)}>\n {children}\n </aside>\n ),\n };\n}\n","// GENERATED FILE — do not edit. Authored source: styles.css\n// (+ the responsive-token block build-styles.ts generates from theme.ts).\n// Regenerate with `npm run build:css` (runs automatically in `npm run build`).\n\n/**\n * Static, author-controlled stylesheet for the blog post view. Scoped under\n * `.aranova-blog-post` and driven entirely by the `--ara-blog-*` CSS custom\n * properties emitted by `blogThemeToCssVars`.\n *\n * SECURITY: this string is a compile-time constant — NOTHING from post content\n * or theme data is ever interpolated into it. All per-post variation flows\n * through the sanitized CSS variables on the `<article>` element.\n */\nexport const BLOG_BASE_CSS = `/* Aranova blog post base stylesheet — authored source of truth.\n *\n * Scoped under \\`.aranova-blog-post\\` and driven entirely by the sanitized\n * \\`--ara-blog-*\\` CSS custom properties emitted by \\`blogThemeToCssVars\\` on the\n * article element. Host sites usually run Tailwind preflight, so:\n *\n * - element rules use \\`.aranova-blog-post :where(el)\\` — one class of\n * specificity (0,1,0), enough to beat preflight's bare element resets\n * while every rule in this sheet stays flat and order-resolvable;\n * - component classes (\\`.aranova-blog-callout\\`, …) keep their natural\n * class-on-class specificity.\n *\n * Responsive-capable tokens (h1/h2/h3/h4/body/small sizes, body line-height,\n * content max-width, block gap, section gap, radius) are NEVER consumed via their raw\n * \\`--ara-blog-*\\` vars here — the generated block below re-points internal\n * \\`--_*\\` vars per breakpoint, and all rules consume those.\n *\n * Rhythm rules near the end target BOTH \\`.aranova-blog-post\\` (content is a\n * direct child of the article today) and \\`.aranova-blog-content\\` (a later\n * phase wraps content in that div) so that phase needs no CSS changes.\n *\n * No raw colors anywhere: every color flows through the theme vars, which\n * already carry \\`light-dark(...)\\` values when a dark palette is present.\n */\n\n/* Responsive token plumbing — GENERATED by build-styles.ts from theme.ts\n (BLOG_BREAKPOINT_MIN_WIDTHS_PX + the responsive() token set). Mobile-first:\n each breakpoint falls back through every narrower key down to the base var.\n Authored rules consume ONLY the --_* internal vars for these tokens. */\n.aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size);\n --_h2-size: var(--ara-blog-type-h2-size);\n --_h3-size: var(--ara-blog-type-h3-size);\n --_h4-size: var(--ara-blog-type-h4-size);\n --_body-size: var(--ara-blog-type-body-size);\n --_body-line-height: var(--ara-blog-type-body-line-height);\n --_small-size: var(--ara-blog-type-small-size);\n --_content-max-width: var(--ara-blog-space-content-max-width);\n --_block-gap: var(--ara-blog-space-block-gap);\n --_section-gap: var(--ara-blog-space-section-gap);\n --_radius: var(--ara-blog-radius);\n}\n@media (min-width: 640px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size));\n --_h2-size: var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size));\n --_h3-size: var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size));\n --_h4-size: var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size));\n --_body-size: var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size));\n --_body-line-height: var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height));\n --_small-size: var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size));\n --_content-max-width: var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width));\n --_block-gap: var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap));\n --_section-gap: var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap));\n --_radius: var(--ara-blog-radius-sm, var(--ara-blog-radius));\n }\n}\n@media (min-width: 768px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size)));\n --_h2-size: var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size)));\n --_h3-size: var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size)));\n --_h4-size: var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size)));\n --_body-size: var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size)));\n --_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)));\n --_small-size: var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size)));\n --_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)));\n --_block-gap: var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap)));\n --_section-gap: var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap)));\n --_radius: var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius)));\n }\n}\n@media (min-width: 1024px) {\n .aranova-blog-post {\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_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))));\n --_radius: var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius))));\n }\n}\n@media (min-width: 1280px) {\n .aranova-blog-post {\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n --_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)))));\n }\n}\n@media (min-width: 1536px) {\n .aranova-blog-post {\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n --_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))))));\n }\n}\n@media (min-width: 1920px) {\n .aranova-blog-post {\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n --_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)))))));\n }\n}\n\n.aranova-blog-post {\n /* Internal spacing scale, derived from the responsive block gap. */\n --_flow: var(--_block-gap);\n --_flow-sm: calc(var(--_block-gap) * 0.5);\n --_flow-lg: calc(var(--_block-gap) * 1.75);\n --_flow-section: var(--_section-gap);\n /* Optional heading color token — themes that don't set it fall back to text. */\n --_heading-color: var(--ara-blog-color-heading, var(--ara-blog-color-text));\n\n max-width: var(--_content-max-width);\n margin-inline: auto;\n background: var(--ara-blog-color-background);\n color: var(--ara-blog-color-text);\n font-family: var(--ara-blog-type-body-family);\n font-weight: var(--ara-blog-type-body-weight);\n font-size: var(--_body-size);\n line-height: var(--_body-line-height);\n overflow-wrap: break-word;\n}\n@media (max-width: 639.99px) {\n .aranova-blog-post {\n padding-inline: 1.25rem;\n }\n}\n\n/* --- Headings ------------------------------------------------------------ */\n.aranova-blog-post :where(h1, h2, h3, h4, h5, h6) {\n font-family: var(--ara-blog-type-heading-family);\n font-weight: var(--ara-blog-type-heading-weight);\n line-height: 1.25;\n margin-block: 0;\n color: var(--_heading-color);\n scroll-margin-top: 5rem;\n}\n.aranova-blog-post :where(h1, h2, h3) {\n /* \\`pretty\\` fills each line greedily (only the LAST line is massaged to avoid\n * an orphan word) — so a heading uses the full measure. \\`balance\\` was leaving\n * short first lines. */\n text-wrap: pretty;\n}\n.aranova-blog-post :where(h1) {\n font-size: var(--_h1-size);\n letter-spacing: var(--ara-blog-type-heading-letter-spacing);\n}\n.aranova-blog-post :where(h2) {\n font-size: var(--_h2-size);\n letter-spacing: var(--ara-blog-type-heading-letter-spacing);\n}\n.aranova-blog-post :where(h3) {\n font-size: var(--_h3-size);\n letter-spacing: calc(var(--ara-blog-type-heading-letter-spacing) * 0.5);\n}\n.aranova-blog-post :where(h4) {\n font-size: var(--_h4-size);\n font-weight: 600;\n letter-spacing: calc(var(--ara-blog-type-heading-letter-spacing) * 0.5);\n}\n.aranova-blog-post :where(h5) {\n font-size: var(--_body-size);\n font-weight: 600;\n}\n.aranova-blog-post :where(h6) {\n font-size: var(--_small-size);\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--ara-blog-color-text-muted);\n}\n\n/* --- Text ---------------------------------------------------------------- */\n.aranova-blog-post :where(p) {\n margin-block: 0;\n}\n/* Lead paragraph: the one directly after the post title. */\n.aranova-blog-post :where(h1) + :where(p) {\n font-size: 1.15em;\n color: color-mix(in srgb, var(--ara-blog-color-text) 72%, var(--ara-blog-color-text-muted));\n}\n.aranova-blog-post :where(a) {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n.aranova-blog-post :where(a):focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n/* --- Lists ---------------------------------------------------------------- */\n.aranova-blog-post :where(ul, ol) {\n margin-block: 0;\n padding-inline-start: 1.5em;\n}\n.aranova-blog-post :where(ul) {\n list-style-type: disc;\n}\n.aranova-blog-post :where(ul ul) {\n list-style-type: circle;\n}\n.aranova-blog-post :where(ul ul ul) {\n list-style-type: square;\n}\n.aranova-blog-post :where(ol) {\n list-style-type: decimal;\n}\n.aranova-blog-post :where(ol ol) {\n list-style-type: lower-alpha;\n}\n.aranova-blog-post :where(ul) > li::marker {\n color: var(--ara-blog-color-text-muted);\n}\n.aranova-blog-post :where(ol) > li::marker {\n font-weight: 600;\n color: var(--_heading-color);\n}\n.aranova-blog-post :where(li + li) {\n margin-block-start: 0.35em;\n}\n.aranova-blog-post :where(li > ul, li > ol) {\n margin-block-start: 0.35em;\n}\n\n/* --- Blockquote ------------------------------------------------------------ */\n.aranova-blog-post :where(blockquote) {\n margin: 0;\n padding-block: 0.25em;\n padding-inline: 1.25em 0.5em;\n border-inline-start: 3px solid var(--ara-blog-color-primary);\n color: var(--ara-blog-color-text-muted);\n font-style: italic;\n}\n\n/* --- Code ------------------------------------------------------------------ */\n.aranova-blog-post :where(code) {\n background: var(--ara-blog-color-code-background);\n border-radius: calc(var(--_radius) / 2);\n padding: 0.15em 0.35em;\n font-size: 0.9em;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n.aranova-blog-post :where(pre) {\n margin: 0;\n background: var(--ara-blog-color-code-background);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 1em;\n overflow-x: auto;\n line-height: 1.5;\n}\n.aranova-blog-post :where(pre code) {\n background: none;\n border-radius: 0;\n padding: 0;\n font-size: var(--_small-size);\n}\n\n/* --- Media ------------------------------------------------------------------ */\n.aranova-blog-post :where(img) {\n max-width: 100%;\n height: auto;\n display: block;\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n}\n.aranova-blog-post :where(figure) {\n margin: 0;\n}\n.aranova-blog-post :where(figcaption) {\n margin-block-start: 0.5em;\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n text-align: var(--ara-blog-type-caption-align, center);\n}\n\n/* --- Tables ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-table-wrap {\n overflow-x: auto;\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n}\n.aranova-blog-post :where(table) {\n border-collapse: collapse;\n width: 100%;\n font-size: var(--_small-size);\n}\n.aranova-blog-post :where(th, td) {\n border: 0;\n border-block-end: 1px solid var(--ara-blog-color-border);\n padding: 0.6em 0.85em;\n text-align: left;\n}\n.aranova-blog-post :where(th) {\n background: var(--ara-blog-color-surface);\n border-block-end: 2px solid var(--ara-blog-color-border);\n font-weight: 600;\n}\n.aranova-blog-post :where(tbody tr:nth-child(even) td) {\n background: color-mix(\n in srgb,\n var(--ara-blog-color-surface) 55%,\n var(--ara-blog-color-background)\n );\n}\n.aranova-blog-post :where(tbody tr:last-child) :where(th, td) {\n border-block-end: 0;\n}\n\n/* --- Section break ------------------------------------------------------------ */\n.aranova-blog-post :where(hr) {\n border: none;\n border-top: 2px solid var(--ara-blog-color-border);\n width: 4rem;\n margin-inline: auto;\n}\n\n/* --- Callout ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-callout {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-inline-start: 4px solid var(--ara-blog-color-primary);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-callout > * + * {\n margin-block-start: var(--_flow-sm);\n}\n\n/* --- CTA ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-cta {\n display: inline-block;\n background: var(--ara-blog-color-primary);\n color: var(--ara-blog-color-primary-foreground);\n border-radius: var(--_radius);\n padding: 0.6em 1.2em;\n font-weight: 600;\n text-decoration: none;\n transition:\n filter 150ms ease,\n transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-cta:hover {\n filter: brightness(1.08);\n transform: translateY(-1px);\n}\n.aranova-blog-post .aranova-blog-cta:active {\n filter: none;\n transform: none;\n}\n.aranova-blog-post .aranova-blog-cta:focus-visible {\n outline: 2px solid var(--ara-blog-color-primary);\n outline-offset: 2px;\n}\n\n/* --- FAQ (details/summary accordion) ------------------------------------------ */\n.aranova-blog-post .aranova-blog-faq-item {\n border-block-start: 1px solid var(--ara-blog-color-border);\n padding-block: calc(var(--_flow) * 0.75);\n}\n.aranova-blog-post .aranova-blog-faq-question {\n cursor: pointer;\n list-style: none;\n position: relative;\n padding-inline-end: 1.75em;\n}\n.aranova-blog-post .aranova-blog-faq-question::-webkit-details-marker {\n display: none;\n}\n/* Rotating chevron (border trick — colors via theme vars only). */\n.aranova-blog-post .aranova-blog-faq-question::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-faq-item[open] > .aranova-blog-faq-question::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-faq-question:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n.aranova-blog-post .aranova-blog-faq-answer {\n padding-block-start: var(--_flow-sm);\n}\n.aranova-blog-post .aranova-blog-faq-answer > * + * {\n margin-block-start: var(--_flow-sm);\n}\n/* Progressive open/close animation where the platform supports it. */\n@supports (interpolate-size: allow-keywords) {\n .aranova-blog-post .aranova-blog-faq-item {\n interpolate-size: allow-keywords;\n }\n .aranova-blog-post .aranova-blog-faq-item::details-content {\n block-size: 0;\n overflow-y: clip;\n transition:\n block-size 200ms ease,\n content-visibility 200ms allow-discrete;\n }\n .aranova-blog-post .aranova-blog-faq-item[open]::details-content {\n block-size: auto;\n }\n}\n\n/* --- Key takeaways ------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-key-takeaways {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 1em 1.25em;\n}\n.aranova-blog-post .aranova-blog-key-takeaways :where(ul) {\n margin-block: 0;\n}\n.aranova-blog-post .aranova-blog-key-takeaways :where(ul) > li::marker {\n color: var(--ara-blog-color-primary);\n}\n\n/* --- Article layout (content column + optional aside rail) -----------------------\n * \\`.aranova-blog-layout\\` is a plain block on narrow viewports (the aside, when\n * present, stacks after the content). On wide viewports a grid opens ONLY when\n * an aside exists, and the article widens by exactly the rail + gap so the\n * content column keeps its \\`content_max_width\\`. */\n.aranova-blog-post .aranova-blog-layout {\n /* Flex column (not block) so the aside can be VISUALLY hoisted above the\n * content on narrow viewports while the content stays first in DOM\n * (reading order / SEO). */\n display: flex;\n flex-direction: column;\n}\n.aranova-blog-post .aranova-blog-layout > .aranova-blog-aside {\n order: -1;\n margin-block-end: var(--_flow-lg);\n}\n@media (min-width: 1100px) {\n .aranova-blog-post:has(.aranova-blog-aside) {\n max-width: calc(var(--_content-max-width) + 240px + 3rem);\n }\n .aranova-blog-post .aranova-blog-layout:has(.aranova-blog-aside) {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 240px;\n column-gap: 3rem;\n align-items: start;\n }\n .aranova-blog-post .aranova-blog-layout:has(.aranova-blog-aside) > .aranova-blog-aside {\n /* Reset the mobile hoist: grid auto-placement honors \\`order\\`, so -1 would\n * drop the aside into the FIRST column. */\n order: 0;\n margin-block-end: 0;\n position: sticky;\n top: 5rem;\n align-self: start;\n max-height: calc(100vh - 6rem);\n overflow: auto;\n }\n /* Rail on the LEFT: flip the column order (content DOM stays first for\n * reading order / SEO; only the visual columns swap). */\n .aranova-blog-post .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside) {\n grid-template-columns: 240px minmax(0, 1fr);\n }\n /* Horizontal collapse, CLOSED: the rail column shrinks to the compact toggle\n * (auto) and the article reflows into the reclaimed space. */\n .aranova-blog-post\n .aranova-blog-layout:has(\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n ) {\n grid-template-columns: minmax(0, 1fr) auto;\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n ) {\n grid-template-columns: auto minmax(0, 1fr);\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside)\n > .aranova-blog-content {\n order: 2;\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside)\n > .aranova-blog-aside {\n order: 1;\n }\n}\n\n/* --- Byline / meta row (chrome) --------------------------------------------------- */\n.aranova-blog-post .aranova-blog-meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.35em 0.5em;\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n padding-block-end: var(--_flow);\n border-block-end: 1px solid var(--ara-blog-color-border);\n}\n.aranova-blog-post .aranova-blog-meta-item {\n display: inline-flex;\n align-items: baseline;\n gap: 0.5em;\n white-space: nowrap;\n}\n.aranova-blog-post .aranova-blog-meta-tags {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 0.35em;\n margin-inline-start: auto;\n}\n.aranova-blog-post .aranova-blog-meta-tag {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: 999px;\n padding: 0.1em 0.7em;\n white-space: nowrap;\n}\n\n/* --- Breadcrumbs (chrome) ---------------------------------------------------------- */\n.aranova-blog-post .aranova-blog-breadcrumbs {\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n}\n.aranova-blog-post .aranova-blog-breadcrumbs ol {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0.2em 0.5em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs li {\n display: inline-flex;\n align-items: baseline;\n gap: 0.5em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs a {\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs a:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs [aria-current=\"page\"] {\n color: var(--ara-blog-color-text);\n}\n\n/* --- Table of contents (chrome) -----------------------------------------------------\n * Zero-JS shell: a native details/summary disclosure. Chevron follows the FAQ\n * accordion's border-trick pattern (theme vars only, no raw colors). */\n.aranova-blog-post .aranova-blog-toc {\n font-size: var(--_small-size);\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-toc-title {\n cursor: pointer;\n list-style: none;\n position: relative;\n padding-inline-end: 1.75em;\n font-weight: 600;\n color: var(--_heading-color);\n}\n.aranova-blog-post .aranova-blog-toc-title::-webkit-details-marker {\n display: none;\n}\n.aranova-blog-post .aranova-blog-toc-title::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-toc[open] > .aranova-blog-toc-title::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-toc-title:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n.aranova-blog-post .aranova-blog-toc ol {\n list-style: none;\n margin: 0.75em 0 0;\n padding: 0;\n}\n.aranova-blog-post .aranova-blog-toc li {\n margin: 0;\n}\n.aranova-blog-post .aranova-blog-toc li + li {\n margin-block-start: 0.3em;\n}\n.aranova-blog-post .aranova-blog-toc li[data-depth=\"3\"] {\n padding-inline-start: 0.9em;\n font-size: 0.94em;\n}\n.aranova-blog-post .aranova-blog-toc a {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n line-height: 1.35;\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n}\n.aranova-blog-post .aranova-blog-toc a:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n\n/* --- Table of contents (client island) ------------------------------------------\n * Animated companion to the zero-JS shell above: same box, but a real <button>\n * header (aria-expanded) and a grid-template-rows 0fr↔1fr collapse. The global\n * reduced-motion block at the end of this sheet disables the transition. */\n.aranova-blog-post .aranova-blog-toc-panel {\n font-size: var(--_small-size);\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-toc-panel-title {\n /* Button reset — host sites may or may not run Tailwind preflight. */\n appearance: none;\n background: none;\n border: 0;\n margin: 0;\n padding: 0;\n padding-inline-end: 1.75em;\n width: 100%;\n text-align: start;\n font: inherit;\n cursor: pointer;\n position: relative;\n font-weight: 600;\n color: var(--_heading-color);\n}\n/* Rotating chevron (border trick — same pattern as the FAQ / zero-JS TOC). */\n.aranova-blog-post .aranova-blog-toc-panel-title::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 250ms ease;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-toc-panel-title:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n/* Collapsible body: the grid row animates 0fr↔1fr; the clip wrapper hides overflow. */\n.aranova-blog-post .aranova-blog-toc-panel-body {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 250ms ease;\n}\n.aranova-blog-post .aranova-blog-toc-panel[data-open=\"true\"] > .aranova-blog-toc-panel-body {\n grid-template-rows: 1fr;\n}\n/* Horizontal collapse: a clean binary toggle along the inline axis. Open = the\n * normal rail; closed = the whole panel is a compact chevron chip and the rail\n * grid column reclaims the space (via \\`:has()\\` on the layout, below). No width\n * animation: grid \\`auto\\` tracks can't interpolate, and a sliding clip either\n * overflows the sticky aside (horizontal scrollbar) or reflows the entries —\n * instant + correct beats janky. */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]\n > .aranova-blog-toc-panel-body {\n display: block;\n grid-template-rows: none;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-body {\n display: none;\n}\n/* Closed panel compacts to a small rounded chevron chip: the label is clipped\n * out of a square toggle box and the panel hugs it (the rail's \\`auto\\` grid\n * column then shrinks to the chip). */\n.aranova-blog-post .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"]) {\n width: fit-content;\n padding: 0.6em;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title {\n display: block;\n width: 1.4em;\n height: 1.4em;\n overflow: hidden;\n text-indent: 150%;\n white-space: nowrap;\n padding-inline-end: 0;\n}\n/* Horizontal chevron points along the fold axis (‹/›), flipping on open/close\n * per side, instead of the vertical ⌄/⌃. */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]\n > .aranova-blog-toc-panel-title::after {\n inset-inline: auto 0.5em;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n inset-inline: 0;\n margin-inline: auto;\n}\n/* Left rail: open folds left (‹ = rotate 135deg), closed expands right (› = -45deg). */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"left\"][data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(135deg);\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"left\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-70%) rotate(-45deg);\n}\n/* Right rail: mirror — open folds right (›), closed expands left (‹). */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"right\"][data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-70%) rotate(-45deg);\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"right\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(135deg);\n}\n.aranova-blog-post .aranova-blog-toc-panel-clip {\n overflow: hidden;\n min-height: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel ol {\n list-style: none;\n margin: 0.75em 0 0;\n padding: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel li {\n margin: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel li + li {\n margin-block-start: 0.3em;\n}\n.aranova-blog-post .aranova-blog-toc-panel li[data-depth=\"3\"] {\n padding-inline-start: 0.9em;\n font-size: 0.94em;\n}\n.aranova-blog-post .aranova-blog-toc-panel-link {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n line-height: 1.35;\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n border-inline-start: 2px solid transparent;\n padding-inline-start: 0.6em;\n margin-inline-start: -0.75em;\n transition: color 150ms ease;\n}\n.aranova-blog-post .aranova-blog-toc-panel-link:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n/* Scroll-spy active entry: primary color + inline-start accent. */\n.aranova-blog-post .aranova-blog-toc-panel-link[data-active=\"true\"] {\n color: var(--ara-blog-color-primary);\n border-inline-start-color: var(--ara-blog-color-primary);\n font-weight: 600;\n}\n\n/* --- Reading progress (client island) --------------------------------------------\n * Fixed strip along the top of the viewport; fill = scaleX(scrolled fraction),\n * driven directly from rAF (no transition — it tracks position, so the\n * reduced-motion block has nothing to disable and the bar stays useful). */\n.aranova-blog-post .aranova-blog-progress {\n position: fixed;\n inset-block-start: 0;\n inset-inline: 0;\n z-index: 60;\n background: var(--ara-blog-color-primary);\n transform: scaleX(0);\n transform-origin: 0 50%;\n pointer-events: none;\n}\n\n/* --- Vertical rhythm ------------------------------------------------------------\n * Kept AFTER the element rules: the owl selector and the :where()-wrapped\n * element rules all sit at (0,1,0), so source order lets rhythm win over the\n * elements' margin resets, and the per-heading overrides below win over the\n * owl. \\`:is(...)\\` targets both today's direct-child layout and the later\n * \\`.aranova-blog-content\\` wrapper at class specificity. */\n:is(.aranova-blog-post, .aranova-blog-content) > * + * {\n margin-block-start: var(--_flow);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(h2) {\n margin-block-start: var(--_flow-section);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(h3, h4) {\n margin-block-start: var(--_flow-lg);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(figure) {\n margin-block: var(--_flow-lg);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(hr) {\n margin-block: var(--_flow-section);\n}\n\n/* --- Motion ------------------------------------------------------------------\n * Opt-in: a later phase stamps data-motion=\"subtle\" on the article; without\n * the attribute nothing animates. */\n@media (prefers-reduced-motion: no-preference) {\n .aranova-blog-post[data-motion=\"subtle\"] {\n animation: aranova-blog-fade-up 300ms ease-out both;\n }\n}\n@keyframes aranova-blog-fade-up {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: none;\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .aranova-blog-post,\n .aranova-blog-post *,\n .aranova-blog-post *::before,\n .aranova-blog-post *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n`;\n","// Server-only article chrome — no 'use client'. A breadcrumb trail rendered\n// into BlogPostView's `header` slot, above the post title.\n\nimport type { ReactNode } from \"react\";\n\nimport { joinClassNames } from \"../class-names\";\n\nexport interface BlogBreadcrumbItem {\n label: string;\n href: string;\n}\n\nexport interface BlogBreadcrumbsProps {\n /** Ancestor links, outermost first (e.g. Home → Blog). */\n items: readonly BlogBreadcrumbItem[];\n /** The current page (usually the post title) — rendered as text, not a link. */\n currentLabel: string;\n /** Site-supplied class merged onto the fixed `.aranova-blog-breadcrumbs` class. */\n className?: string;\n}\n\n/**\n * Accessible breadcrumb trail: ancestor links separated by a decorative `/`,\n * ending in the current page as `<span aria-current=\"page\">`. Styled by the\n * `.aranova-blog-breadcrumbs` section of the base CSS.\n */\nexport function BlogBreadcrumbs({\n items,\n currentLabel,\n className,\n}: BlogBreadcrumbsProps): ReactNode {\n return (\n <nav aria-label=\"Breadcrumbs\" className={joinClassNames(\"aranova-blog-breadcrumbs\", className)}>\n <ol>\n {items.map((item) => (\n <li key={`${item.href} ${item.label}`}>\n <a href={item.href}>{item.label}</a>\n <span aria-hidden=\"true\" className=\"aranova-blog-breadcrumbs-separator\">\n /\n </span>\n </li>\n ))}\n <li>\n <span aria-current=\"page\">{currentLabel}</span>\n </li>\n </ol>\n </nav>\n );\n}\n","// Server-only article chrome — no 'use client'. A muted byline row (dates,\n// reading time, tag pills) rendered into BlogPostView's `header` slot.\n\nimport type { ReactNode } from \"react\";\n\nimport { joinClassNames } from \"../class-names\";\n\n/** Fallback locale for date formatting (the agency's home market). */\nexport const DEFAULT_BLOG_LOCALE = \"en-CA\";\n\nexport interface BlogPostMetaProps {\n /** ISO date(-time) string; invalid/absent values render nothing. */\n publishedAt?: string;\n /** ISO date(-time) string; only shown when it differs from `publishedAt`'s day. */\n updatedAt?: string | null;\n /** Whole minutes (see `estimateReadingTime`); non-positive values render nothing. */\n readingTimeMinutes?: number;\n tags?: readonly string[];\n /** BCP-47 locale for date formatting. Default {@link DEFAULT_BLOG_LOCALE}. */\n locale?: string;\n /** Site-supplied class merged onto the fixed `.aranova-blog-meta` class. */\n className?: string;\n}\n\ninterface FormattedDate {\n /** Machine-readable value for the `datetime` attribute (the date part). */\n dateTime: string;\n /** Human-readable long-form date. */\n label: string;\n}\n\n// Locale/date inputs come from operator-controlled config and the published\n// content object, but stay defensive anyway: a bad locale falls back, a bad\n// date renders nothing (never throws into the host page).\nfunction formatBlogDate(iso: string | undefined | null, locale: string): FormattedDate | null {\n if (!iso) return null;\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return null;\n let formatter: Intl.DateTimeFormat;\n try {\n formatter = new Intl.DateTimeFormat(locale, { dateStyle: \"long\", timeZone: \"UTC\" });\n } catch {\n formatter = new Intl.DateTimeFormat(DEFAULT_BLOG_LOCALE, {\n dateStyle: \"long\",\n timeZone: \"UTC\",\n });\n }\n return { dateTime: date.toISOString().slice(0, 10), label: formatter.format(date) };\n}\n\n/**\n * Post byline row: published date (+ \"Updated …\" when meaningfully different),\n * \"· N min read\", and tag pills. Renders `null` when there is nothing to show.\n * Styled by the `.aranova-blog-meta` section of the base CSS (muted small row\n * with a bottom divider).\n */\nexport function BlogPostMeta({\n publishedAt,\n updatedAt,\n readingTimeMinutes,\n tags,\n locale = DEFAULT_BLOG_LOCALE,\n className,\n}: BlogPostMetaProps): ReactNode {\n const published = formatBlogDate(publishedAt, locale);\n const updated = formatBlogDate(updatedAt, locale);\n // Same-day updates are noise — only surface a real revision.\n const showUpdated = updated !== null && updated.dateTime !== published?.dateTime;\n const showReadingTime = typeof readingTimeMinutes === \"number\" && readingTimeMinutes > 0;\n const shownTags = (tags ?? []).filter((tag) => tag.length > 0);\n\n const items: Array<{ key: string; node: ReactNode }> = [];\n if (published) {\n items.push({\n key: \"published\",\n node: <time dateTime={published.dateTime}>{published.label}</time>,\n });\n }\n if (showUpdated && updated) {\n items.push({\n key: \"updated\",\n node: (\n <>\n Updated <time dateTime={updated.dateTime}>{updated.label}</time>\n </>\n ),\n });\n }\n if (showReadingTime) {\n items.push({ key: \"reading-time\", node: <>{readingTimeMinutes} min read</> });\n }\n\n if (items.length === 0 && shownTags.length === 0) return null;\n\n return (\n <div className={joinClassNames(\"aranova-blog-meta\", className)}>\n {items.map((item, index) => (\n <span key={item.key} className=\"aranova-blog-meta-item\">\n {index > 0 ? (\n <span aria-hidden=\"true\" className=\"aranova-blog-meta-separator\">\n ·\n </span>\n ) : null}\n {item.node}\n </span>\n ))}\n {shownTags.length > 0 ? (\n <span className=\"aranova-blog-meta-tags\">\n {shownTags.map((tag) => (\n <span key={tag} className=\"aranova-blog-meta-tag\">\n {tag}\n </span>\n ))}\n </span>\n ) : null}\n </div>\n );\n}\n","// Server-only article chrome — no 'use client'. The zero-JS table-of-contents\n// shell rendered into BlogPostView's `aside` slot. A later phase ships an\n// animated client-island TOC; because the aside slot takes any ReactNode, that\n// island simply replaces this shell in the composition — same anchor ids\n// (extractBlogHeadings ↔ rehype-slug parity), no renderer changes.\n\nimport type { ReactNode } from \"react\";\n\nimport type { BlogHeading } from \"../../toc\";\nimport { joinClassNames } from \"../class-names\";\n\nexport interface BlogTocShellProps {\n /** The post outline — see `extractBlogHeadings`. Empty array renders nothing. */\n headings: readonly BlogHeading[];\n /** Disclosure summary label. Default `\"On this page\"`. */\n title?: string;\n /** Site-supplied class merged onto the fixed `.aranova-blog-toc` class. */\n className?: string;\n}\n\n/**\n * No-JS table of contents: a native `<details open>` disclosure holding an\n * anchor list (h3 entries indented under their h2). Styled by the\n * `.aranova-blog-toc` section of the base CSS (chevron consistent with the\n * FAQ disclosure pattern).\n */\nexport function BlogTocShell({\n headings,\n title = \"On this page\",\n className,\n}: BlogTocShellProps): ReactNode {\n if (headings.length === 0) return null;\n return (\n <details open className={joinClassNames(\"aranova-blog-toc\", className)}>\n <summary className=\"aranova-blog-toc-title\">{title}</summary>\n <nav aria-label=\"Table of contents\">\n <ol>\n {headings.map((heading) => (\n <li key={heading.id} data-depth={heading.depth}>\n <a href={`#${heading.id}`} title={heading.text}>\n {heading.text}\n </a>\n </li>\n ))}\n </ol>\n </nav>\n </details>\n );\n}\n","// Async React Server Component — no 'use client'. The one-line integration for\n// client sites AND the internal dashboard's draft preview (same component, a\n// different BlogClient/content source).\n\nimport type { ReactNode } from \"react\";\n\nimport type { BlogClient } from \"../client\";\nimport { resolveBlogTheme, sanitizeBlogTheme, type BlogThemeOverride } from \"../theme\";\nimport { estimateReadingTime, extractBlogHeadings, type BlogHeading } from \"../toc\";\nimport { BlogPostView } from \"./blog-post-view\";\nimport type { BlogClassNameOverrides } from \"./class-names\";\nimport { BlogBreadcrumbs, type BlogBreadcrumbItem } from \"./chrome/breadcrumbs\";\nimport { BlogPostMeta } from \"./chrome/meta\";\nimport { BlogTocShell } from \"./chrome/toc\";\nimport { createDefaultComponents, type BlogComponents } from \"./components\";\n\nexport interface BlogPostProps {\n slug: string;\n client: BlogClient;\n /** Client-registered default theme layer (sits between the base theme and per-post overrides). */\n defaultTheme?: BlogThemeOverride;\n /** Overrides merged over the default component set. */\n components?: Partial<BlogComponents>;\n /** Rendered when the slug is unknown/unfetchable. Default: a minimal not-found article. */\n notFound?: ReactNode;\n /** Render the byline row (published/updated dates + reading time). Default `false`. */\n showMeta?: boolean;\n /** Render the sticky table-of-contents rail (omitted when the post has no h2/h3). Default `false`. */\n showToc?: boolean;\n /** Which side the ToC rail sits on at desktop width. Default `\"right\"`. */\n tocSide?: \"left\" | \"right\";\n /** Render the breadcrumb trail (requires `breadcrumbs`). Default `false`. */\n showBreadcrumbs?: boolean;\n /** Ancestor links for the breadcrumb trail, outermost first (e.g. Home → Blog). */\n breadcrumbs?: readonly BlogBreadcrumbItem[];\n /** BCP-47 locale for the byline dates. Default `\"en-CA\"`. */\n locale?: string;\n /**\n * Component-injection slot for the table of contents: when provided (and\n * `showToc` finds an outline), it renders INSTEAD of the zero-JS\n * `BlogTocShell` default. This is how `@aranova/tracking-next`'s enhanced\n * `BlogPost` wires in the animated `TocPanel` client island — tracking-core\n * cannot import it back from tracking-next (core is the dependency), and the\n * island must reach server bundles only via the\n * `@aranova/tracking-next/blog-client` self-specifier so its `\"use client\"`\n * boundary survives `splitting: false` bundling.\n */\n tocSlot?: (headings: readonly BlogHeading[]) => ReactNode;\n /**\n * Component-injection slot rendered at the very top of the header chrome\n * (before breadcrumbs/byline). Same rationale as `tocSlot` — the enhanced\n * wrapper injects the `ReadingProgress` island here under `showProgress`.\n */\n progressSlot?: () => ReactNode;\n /**\n * Site-supplied classes merged onto the fixed base class of each styleable\n * surface (article/layout/MDX components/chrome) — additive, never a\n * replacement of the `.aranova-blog-*` selector. For a full component\n * rewrite instead, use `components`.\n */\n classNames?: BlogClassNameOverrides;\n}\n\n/**\n * Fetch-and-render a published post: `getPost(slug)` → theme resolution\n * (BASE < `defaultTheme` < per-post `theme_overrides`, every untrusted layer\n * re-sanitized) → {@link BlogPostView} with the post's image records wired\n * into the default components, plus opt-in article chrome (breadcrumbs +\n * byline in the header slot, table of contents in the aside slot).\n */\nexport async function BlogPost({\n slug,\n client,\n defaultTheme,\n components,\n notFound,\n showMeta = false,\n showToc = false,\n tocSide = \"right\",\n showBreadcrumbs = false,\n breadcrumbs,\n locale,\n tocSlot,\n progressSlot,\n classNames,\n}: BlogPostProps): Promise<ReactNode> {\n const post = await client.getPost(slug);\n if (!post) {\n return (\n notFound ?? (\n <article className=\"aranova-blog-post\" data-error=\"not-found\">\n <p>This post could not be found.</p>\n </article>\n )\n );\n }\n\n // theme_overrides is already parsed into the override shape, but sanitize\n // defensively anyway — resolveBlogTheme's output lands in inline styles.\n const theme = resolveBlogTheme(\n sanitizeBlogTheme(defaultTheme),\n sanitizeBlogTheme(post.theme_overrides),\n );\n const merged = { ...createDefaultComponents(post.images, classNames), ...components };\n\n // Header chrome: breadcrumbs above the byline. (Tags live on the manifest\n // entry, not the content object, so the byline carries dates + reading time.)\n const crumbs =\n showBreadcrumbs && breadcrumbs && breadcrumbs.length > 0 ? (\n <BlogBreadcrumbs\n items={breadcrumbs}\n currentLabel={post.title}\n className={classNames?.breadcrumbs}\n />\n ) : null;\n const meta = showMeta ? (\n <BlogPostMeta\n publishedAt={post.published_at}\n updatedAt={post.updated_at}\n readingTimeMinutes={estimateReadingTime(post.mdx)}\n locale={locale}\n className={classNames?.meta}\n />\n ) : null;\n const progress = progressSlot ? progressSlot() : null;\n const header =\n progress || crumbs || meta ? (\n <>\n {progress}\n {crumbs}\n {meta}\n </>\n ) : undefined;\n\n // Aside chrome: the injected TOC (client island) when a slot is provided,\n // the zero-JS shell otherwise. Omitted entirely (no empty rail, no grid\n // column) when the post has no h2/h3 outline.\n const headings = showToc ? extractBlogHeadings(post.mdx) : [];\n const aside =\n headings.length > 0\n ? (tocSlot?.(headings) ?? <BlogTocShell headings={headings} className={classNames?.toc} />)\n : undefined;\n\n return (\n <BlogPostView\n mdx={post.mdx}\n theme={theme}\n components={merged}\n header={header}\n aside={aside}\n classNames={classNames}\n tocSide={tocSide}\n />\n );\n}\n","// Enhanced BlogPost — the consumer-facing composition on\n// `@aranova/tracking-next/blog-server`. Server component (no 'use client').\n//\n// Composition design (ONE public BlogPost): tracking-core's BlogPost keeps its\n// zero-JS `BlogTocShell` default and exposes optional component-injection\n// slots (`tocSlot`, `progressSlot`); this wrapper pre-wires the animated\n// client islands into those slots. Core cannot import the islands from\n// tracking-next (core is the dependency), and this module must NOT import\n// their source relatively — under tsup's `splitting: false` that would inline\n// them into dist/blog-server.* and drop their `\"use client\"` directive. The\n// `@aranova/tracking-next/blog-client` self-specifier is marked external in\n// tsup (like `next-mdx-remote/rsc`), so the emitted blog-server bundle keeps a\n// real import across the server/client boundary and Next hydrates the islands.\n//\n// The islands are SSR'd like any client component, so `showToc` renders the\n// full anchor list server-side (no zero-JS shell needed here — the shell\n// remains core's default for compositions that don't inject a slot).\n\nimport type { ReactNode } from \"react\";\n\nimport { ReadingProgress, TocPanel } from \"@aranova/tracking-next/blog-client\";\nimport {\n BlogPost as BlogPostBase,\n type BlogPostProps as BlogPostBaseProps,\n} from \"../../tracking-core/src/blog-server-public\";\n\nexport interface BlogPostProps extends Omit<BlogPostBaseProps, \"tocSlot\" | \"progressSlot\"> {\n /** Render the fixed top-of-viewport reading progress bar. Default `false`. */\n showProgress?: boolean;\n /**\n * ToC collapse axis: `\"vertical\"` (default) folds up under the title,\n * `\"horizontal\"` folds sideways toward `tocSide`. Only affects the animated\n * `TocPanel` island rendered here.\n */\n tocCollapse?: \"vertical\" | \"horizontal\";\n}\n\n/**\n * `BlogPost` with the animated islands wired in: `showToc` renders the\n * collapsible scroll-spy `TocPanel` (instead of core's zero-JS shell) and\n * `showProgress` adds the `ReadingProgress` bar to the header chrome. All\n * other props pass straight through to tracking-core's `BlogPost`.\n */\nexport function BlogPost({\n showProgress = false,\n tocCollapse = \"vertical\",\n ...props\n}: BlogPostProps): ReactNode {\n // `tocSide` drives the layout column (in `...props` for core) AND the island's\n // horizontal-collapse direction, so read it here without consuming it.\n const tocSide = props.tocSide ?? \"right\";\n return (\n <BlogPostBase\n {...props}\n tocSlot={(headings) => (\n <TocPanel\n headings={headings}\n className={props.classNames?.toc}\n collapse={tocCollapse}\n side={tocSide}\n />\n )}\n progressSlot={\n showProgress ? () => <ReadingProgress className={props.classNames?.progress} /> : undefined\n }\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAO;;;AC8CA,IAAM,2BAA2B;AAExC,IAAM,aAAa,oBAAI,IAAwB;AAQxC,SAAS,mBAAmB,YAAsC;AACvE,aAAW,IAAI,UAAU;AAC3B;;;ACtDA,+BAAwE;;;ACsCjE,IAAM,uBAAuB;AAuEpC,IAAM,eAAe,IAAI;;;ACjHzB,IAAAC,cAAkB;;;ACAlB,iBAAkB;AA6BX,IAAM,mBAAmB,CAAC,MAAM,MAAM,MAAM,MAAM,OAAO,KAAK;AAI9D,IAAM,gCAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,OAAO;AACT;AAkBO,SAAS,oBACd,OACsB;AACtB,SAAO,OAAO,UAAU,WAAW,QAAQ,EAAE,MAAM,MAAM;AAC3D;AAMA,IAAM,YAAY;AAClB,IAAM,mBAAmB;AACzB,IAAM,SAAS;AAEf,IAAM,cAAc;AAGpB,IAAM,oBAAoB;AAI1B,IAAM,wBAAwB;AAE9B,IAAM,mBAAmB,aAAE,OAAO,EAAE,OAAO,CAAC,MAAM,UAAU,KAAK,CAAC,KAAK,iBAAiB,KAAK,CAAC,GAAG;AAAA,EAC/F,SAAS;AACX,CAAC;AAED,IAAM,oBAAoB,aAAE,OAAO,EAAE,MAAM,QAAQ;AAAA,EACjD,SAAS;AACX,CAAC;AAED,IAAM,wBAAwB,aAAE,OAAO,EAAE,MAAM,aAAa;AAAA,EAC1D,SAAS;AACX,CAAC;AAED,IAAM,mBAAmB,aAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,IAAI,GAAI;AAG3D,IAAM,mBAAmB,aAAE,KAAK,CAAC,QAAQ,UAAU,OAAO,CAAC;AAE3D,IAAM,mBAAmB,aAAE,OAAO,EAAE;AAAA,EAClC,CAAC,MAAM;AACL,UAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAC/C,WACE,OAAO,SAAS,KAChB,OAAO,MAAM,CAAC,MAAM,kBAAkB,KAAK,CAAC,KAAK,sBAAsB,KAAK,CAAC,CAAC;AAAA,EAElF;AAAA,EACA,EAAE,SAAS,iFAAiF;AAC9F;AAeO,SAAS,aAAqC,QAA4B;AAC/E,SAAO,OAAO,QAAQ,EAAE,UAAU,CAAC,MAAM,KAAK,MAAS;AACzD;AAgBA,SAAS,oBAAoB,aAAqD;AAChF,SAAO,aAAE,OAAO;AAAA,IACd,MAAM;AAAA,IACN,IAAI,aAAa,WAAW,EAAE,MAAM,MAAS;AAAA,IAC7C,IAAI,aAAa,WAAW,EAAE,MAAM,MAAS;AAAA,IAC7C,IAAI,aAAa,WAAW,EAAE,MAAM,MAAS;AAAA,IAC7C,IAAI,aAAa,WAAW,EAAE,MAAM,MAAS;AAAA,IAC7C,OAAO,aAAa,WAAW,EAAE,MAAM,MAAS;AAAA,IAChD,OAAO,aAAa,WAAW,EAAE,MAAM,MAAS;AAAA,EAClD,CAAC;AACH;AAWO,SAAS,WAAW,aAAkD;AAC3E,SAAO,aAAE,MAAM,CAAC,aAAa,oBAAoB,WAAW,CAAC,CAAC;AAChE;AAUA,IAAM,uBAAuB;AAAA,EAC3B,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,SAAS;AACX;AAEO,IAAM,wBAAwB,aAAE,OAAO;AAAA,EAC5C,GAAG;AAAA;AAAA;AAAA;AAAA,EAIH,SAAS,aAAa,gBAAgB;AACxC,CAAC;AAEM,IAAM,4BAA4B,aAAE,OAAO;AAAA,EAChD,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,SAAS,WAAW,iBAAiB;AAAA,EACrC,SAAS,WAAW,iBAAiB;AAAA,EACrC,SAAS,WAAW,iBAAiB;AAAA,EACrC,SAAS,WAAW,iBAAiB;AAAA,EACrC,WAAW,WAAW,iBAAiB;AAAA,EACvC,kBAAkB,WAAW,qBAAqB;AAAA,EAClD,YAAY,WAAW,iBAAiB;AAAA;AAAA;AAAA,EAGxC,wBAAwB;AAAA;AAAA;AAAA,EAGxB,eAAe;AACjB,CAAC;AAEM,IAAM,yBAAyB,aAAE,OAAO;AAAA,EAC7C,mBAAmB,WAAW,iBAAiB;AAAA,EAC/C,WAAW,WAAW,iBAAiB;AAAA,EACvC,aAAa,WAAW,iBAAiB;AAC3C,CAAC;AAKD,IAAM,+BAA+B,aAAE;AAAA,EACrC,OAAO;AAAA,IACL,OAAO,KAAK,oBAAoB,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,aAAa,gBAAgB,CAAC,CAAC;AAAA,EACtF;AACF;AAEO,IAAM,kBAAkB,aAAE,OAAO;AAAA,EACtC,QAAQ;AAAA,EACR,aAAa,aAAa,4BAA4B;AAAA,EACtD,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,QAAQ,WAAW,iBAAiB;AACtC,CAAC;AAgBD,SAAS,kBACP,OACoD;AACpD,QAAM,MAAoC,CAAC;AAC3C,aAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,KAAK,GAAG;AACjD,QAAI,GAAG,IAAI,aAAa,MAAM,EAAE,MAAM,MAAS;AAAA,EACjD;AACA,SAAO;AACT;AAOA,SAAS,aAAa,QAAoC;AACxD,MAAI,kBAAkB,aAAE,UAAU;AAChC,UAAM,UAAW,OAAuE;AACxF,WAAO,aACJ,MAAM,CAAC,GAAG,SAAS,aAAE,KAAK,CAAC,CAAC,EAC5B,SAAS,EACT,UAAU,CAAC,MAAM,KAAK,MAAS;AAAA,EACpC;AACA,SAAO,aAAa,MAAM;AAC5B;AAIA,IAAM,gCAAgC,aAAE,OAAO,kBAAkB,oBAAoB,CAAC;AACtF,IAAM,oCAAoC,aAAE;AAAA,EAC1C,kBAAkB,0BAA0B,KAAK;AACnD;AACA,IAAM,iCAAiC,aAAE,OAAO,kBAAkB,uBAAuB,KAAK,CAAC;AAExF,IAAM,0BAA0B,aAAE;AAAA,EACvC,kBAAkB;AAAA,IAChB,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,QAAQ,WAAW,iBAAiB;AAAA,EACtC,CAAC;AACH;AAQO,IAAM,kBAA6B;AAAA,EACxC,QAAQ;AAAA,IACN,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,oBAAoB;AAAA,IACpB,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,iBAAiB;AAAA,EACnB;AAAA,EACA,YAAY;AAAA,IACV,gBAAgB;AAAA,IAChB,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,aAAa;AAAA;AAAA;AAAA,IAGb,SAAS,EAAE,MAAM,UAAU,IAAI,UAAU;AAAA,IACzC,SAAS,EAAE,MAAM,UAAU,IAAI,UAAU;AAAA,IACzC,SAAS;AAAA,IACT,SAAS;AAAA,IACT,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,YAAY;AAAA,IACZ,wBAAwB;AAAA,IACxB,eAAe;AAAA,EACjB;AAAA,EACA,SAAS;AAAA,IACP,mBAAmB;AAAA,IACnB,WAAW;AAAA,IACX,aAAa;AAAA,EACf;AAAA,EACA,QAAQ;AACV;AAUO,SAAS,kBAAkB,KAAiC;AACjE,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,EAAG,QAAO,CAAC;AACnE,QAAM,SAAS,wBAAwB,UAAU,GAAG;AACpD,SAAO,OAAO,UAAU,OAAO,OAAO,CAAC;AACzC;AAEA,SAAS,WAA8C,MAAS,MAAiC;AAC/F,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,MAAM,EAAE,GAAG,KAAK;AACtB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,QAAI,UAAU,OAAW,CAAC,IAAgC,GAAG,IAAI;AAAA,EACnE;AACA,SAAO;AACT;AAYO,SAAS,oBACX,QACQ;AACX,MAAI,WAAsB;AAAA,IACxB,QAAQ,EAAE,GAAG,gBAAgB,OAAO;AAAA,IACpC,aAAa,gBAAgB,cAAc,EAAE,GAAG,gBAAgB,YAAY,IAAI;AAAA,IAChF,YAAY,EAAE,GAAG,gBAAgB,WAAW;AAAA,IAC5C,SAAS,EAAE,GAAG,gBAAgB,QAAQ;AAAA,IACtC,QAAQ,gBAAgB;AAAA,EAC1B;AACA,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,MAAO;AACZ,eAAW;AAAA,MACT,QAAQ,WAAW,SAAS,QAAQ,MAAM,MAAM;AAAA,MAChD,aACE,MAAM,eAAe,SAAS,cAC1B,WAAW,SAAS,eAAe,CAAC,GAAG,MAAM,WAAW,IACxD;AAAA,MACN,YAAY,WAAW,SAAS,YAAY,MAAM,UAAU;AAAA,MAC5D,SAAS,WAAW,SAAS,SAAS,MAAM,OAAO;AAAA,MACnD,QAAQ,MAAM,UAAU,SAAS;AAAA,IACnC;AAAA,EACF;AACA,SAAO;AACT;AAGA,SAAS,MAAM,KAAqB;AAClC,SAAO,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AAChC;AAMA,SAAS,kBACP,MACA,MACA,OACM;AACN,QAAM,MAAM,oBAAoB,KAAK;AACrC,OAAK,IAAI,IAAI,OAAO,IAAI,IAAI;AAC5B,aAAW,MAAM,kBAAkB;AACjC,UAAM,UAAU,IAAI,EAAE;AACtB,QAAI,YAAY,OAAW,MAAK,GAAG,IAAI,IAAI,EAAE,EAAE,IAAI,OAAO,OAAO;AAAA,EACnE;AACF;AASO,SAAS,mBAAmB,OAA0C;AAC3E,QAAM,OAA+B,CAAC;AACtC,QAAM,OAAO,MAAM;AACnB,QAAM,UAAU,CAAC,CAAC,QAAQ,OAAO,OAAO,IAAI,EAAE,KAAK,CAAC,MAAM,MAAM,MAAS;AAIzE,QAAM,YAAY,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,MAAM,MAAM,GAAG,GAAI,OAAO,OAAO,KAAK,IAAI,IAAI,CAAC,CAAE,CAAC;AAC5F,aAAW,OAAO,WAAW;AAC3B,UAAM,QAAQ,MAAM,OAAO,GAA4B;AACvD,UAAM,YAAY,UAAU,OAAO,GAA4B,IAAI;AAKnE,UAAM,aAAa,UAAU,cAAc,SAAY,MAAM,OAAO,OAAO;AAC3E,QAAI,eAAe,OAAW;AAC9B,SAAK,oBAAoB,MAAM,GAAG,CAAC,EAAE,IAAI,YACrC,cAAc,UAAU,KAAK,SAAS,MACtC;AAAA,EACN;AAGA,MAAI,QAAS,MAAK,cAAc;AAEhC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,UAAU,GAAG;AAC3D,sBAAkB,MAAM,mBAAmB,MAAM,GAAG,CAAC,IAAI,KAAK;AAAA,EAChE;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,OAAO,GAAG;AACxD,sBAAkB,MAAM,oBAAoB,MAAM,GAAG,CAAC,IAAI,KAAK;AAAA,EACjE;AACA,oBAAkB,MAAM,qBAAqB,MAAM,MAAM;AACzD,SAAO;AACT;;;ADjbA,IAAM,sBAAsB,cAAE,QAAQ,CAAC;AAEhC,IAAM,sBAAsB,cAAE,OAAO;AAAA,EAC1C,KAAK,cAAE,OAAO;AAAA,EACd,KAAK,cAAE,OAAO,EAAE,MAAM,EAAE;AAAA,EACxB,OAAO,cAAE,OAAO,EAAE,IAAI;AAAA,EACtB,QAAQ,cAAE,OAAO,EAAE,IAAI;AACzB,CAAC;AAEM,IAAM,0BAA0B,cAAE,OAAO;AAAA,EAC9C,MAAM,cAAE,OAAO;AAAA,EACf,OAAO,cAAE,OAAO;AAAA,EAChB,aAAa,cAAE,OAAO,EAAE,MAAM,EAAE;AAAA,EAChC,cAAc,cAAE,OAAO;AAAA,EACvB,aAAa,cAAE,OAAO;AAAA,EACtB,cAAc,cAAE,OAAO;AAAA,EACvB,YAAY,cAAE,OAAO;AAAA;AAAA;AAAA,EAGrB,iBAAiB,cAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC;AAAA;AAAA;AAAA,EAGzC,MAAM,cAAE,MAAM,cAAE,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAAA,EAClC,YAAY,oBAAoB,SAAS,EAAE,MAAM,IAAI;AACvD,CAAC;AAOM,IAAM,yBAAyB,cAAE,OAAO;AAAA,EAC7C,gBAAgB;AAAA,EAChB,WAAW,cAAE,OAAO;AAAA,EACpB,cAAc,cAAE,OAAO;AAAA,EACvB,OAAO,cAAE,MAAM,uBAAuB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAEM,IAAM,qBAAqB,uBAAuB,OAAO;AAAA;AAAA,EAE9D,OAAO,cACJ,MAAM,wBAAwB,SAAS,EAAE,MAAM,IAAI,CAAC,EACpD,UAAU,CAAC,YAAY,QAAQ,OAAO,CAAC,MAA8B,MAAM,IAAI,CAAC,EAChF,MAAM,CAAC,CAAC;AACb,CAAC;AAEM,IAAM,kBAAkB,cAAE,OAAO;AAAA,EACtC,IAAI,cAAE,OAAO;AAAA,EACb,KAAK,cAAE,OAAO;AAAA,EACd,KAAK,cAAE,OAAO,EAAE,MAAM,EAAE;AAAA,EACxB,SAAS,cAAE,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI;AAAA,EACzC,MAAM,cAAE,KAAK,CAAC,QAAQ,QAAQ,CAAC,EAAE,MAAM,QAAQ;AAAA,EAC/C,OAAO,cAAE,OAAO,EAAE,IAAI;AAAA,EACtB,QAAQ,cAAE,OAAO,EAAE,IAAI;AAAA,EACvB,MAAM,cAAE,OAAO,EAAE,MAAM,EAAE;AAC3B,CAAC;AAEM,IAAM,gBAAgB,cAAE,OAAO;AAAA,EACpC,YAAY,cAAE,OAAO,EAAE,MAAM,EAAE;AAAA,EAC/B,kBAAkB,cAAE,OAAO,EAAE,MAAM,EAAE;AAAA,EACrC,gBAAgB,cAAE,OAAO,EAAE,MAAM,EAAE;AAAA,EACnC,UAAU,cAAE,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI;AAC5C,CAAC;AAGM,IAAM,4BAA4B,cAAE,OAAO;AAAA,EAChD,gBAAgB;AAAA,EAChB,MAAM,cAAE,OAAO;AAAA,EACf,OAAO,cAAE,OAAO;AAAA,EAChB,aAAa,cAAE,OAAO,EAAE,MAAM,EAAE;AAAA,EAChC,KAAK,cAAE,OAAO;AAAA,EACd,QAAQ,cAAE,MAAM,eAAe,EAAE,MAAM,CAAC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAKzC,iBAAiB,aAAa,uBAAuB,EAAE,MAAM,MAAS;AAAA,EACtE,KAAK,cAAc,MAAM;AAAA,IACvB,YAAY;AAAA,IACZ,kBAAkB;AAAA,IAClB,gBAAgB;AAAA,IAChB,UAAU;AAAA,EACZ,CAAC;AAAA,EACD,QAAQ,cAAE,OAAO,cAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;AAAA,EACtC,cAAc,cAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMvB,YAAY,cACT,OAAO,EACP,QAAQ,EACR,UAAU,CAAC,MAAM,KAAK,IAAI,EAC1B,MAAM,IAAI;AAAA,EACb,gBAAgB,cAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC;AAAA,EACxC,cAAc,cAAE,OAAO;AACzB,CAAC;AAEM,IAAM,wBAAwB,0BAA0B,OAAO;AAAA;AAAA,EAEpE,QAAQ,cACL,MAAM,gBAAgB,SAAS,EAAE,MAAM,IAAI,CAAC,EAC5C,UAAU,CAAC,WAAW,OAAO,OAAO,CAAC,MAAsB,MAAM,IAAI,CAAC,EACtE,MAAM,CAAC,CAAC;AACb,CAAC;AAUM,SAAS,kBAAkB,KAAmC;AACnE,QAAM,SAAS,mBAAmB,UAAU,GAAG;AAC/C,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;AAGO,SAAS,qBAAqB,KAAsC;AACzE,QAAM,SAAS,sBAAsB,UAAU,GAAG;AAClD,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;;;AEtFO,SAAS,iBAAiB,QAAsC;AACrE,qBAAmB,gBAAgB;AAGnC,QAAM,QAAQ,OAAO,WAAW,KAAK,KAAK,sBAAsB,QAAQ,QAAQ,EAAE;AAClF,QAAM,UAAwB,IAAI,UAAU,OAAO,aAAa,OAAO,GAAG,IAAI;AAE9E,iBAAe,UAAU,KAAa,MAA8C;AAClF,QAAI;AACF,YAAM,WAAW,MAAM,QAAQ,KAAK,IAAI;AACxC,UAAI,CAAC,SAAS,GAAI,QAAO;AACzB,aAAQ,MAAM,SAAS,KAAK;AAAA,IAC9B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,iBAAe,cAA4C;AAGzD,UAAM,OAAO;AAAA,MACX,MAAM,EAAE,YAAY,OAAO,6BAA6B,IAAI;AAAA,IAC9D;AACA,UAAM,MAAM,MAAM,UAAU,GAAG,IAAI,aAAa,OAAO,SAAS,kBAAkB,IAAI;AACtF,WAAO,QAAQ,OAAO,OAAO,kBAAkB,GAAG;AAAA,EACpD;AAEA,iBAAe,YAA0C;AACvD,UAAM,WAAW,MAAM,YAAY;AACnC,QAAI,CAAC,SAAU,QAAO,CAAC;AACvB,WAAO,CAAC,GAAG,SAAS,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,cAAc,EAAE,YAAY,CAAC;AAAA,EACxF;AAEA,iBAAe,QAAQ,MAA+C;AACpE,UAAM,WAAW,MAAM,YAAY;AACnC,UAAM,QAAQ,UAAU,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI;AAC/D,QAAI,CAAC,MAAO,QAAO;AAGnB,UAAM,OAAO,EAAE,OAAO,cAAc;AACpC,UAAM,MAAM,MAAM,UAAU,GAAG,IAAI,IAAI,MAAM,YAAY,QAAQ,QAAQ,EAAE,CAAC,IAAI,IAAI;AACpF,WAAO,QAAQ,OAAO,OAAO,qBAAqB,GAAG;AAAA,EACvD;AAEA,iBAAe,kBAAoD;AACjE,UAAM,QAAQ,MAAM,UAAU;AAC9B,WAAO,MAAM,IAAI,CAAC,UAAU,EAAE,MAAM,KAAK,KAAK,EAAE;AAAA,EAClD;AAEA,SAAO,EAAE,aAAa,WAAW,SAAS,gBAAgB;AAC5D;;;ACpFO,IAAM,qBAAqE;AAAA;AAAA,EAEhF,WAAW;AAAA,IACT,YAAY;AAAA,MACV,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,SAAS,EAAE,MAAM,QAAQ,IAAI,SAAS;AAAA,MACtC,SAAS,EAAE,MAAM,UAAU,IAAI,WAAW;AAAA,MAC1C,SAAS;AAAA,MACT,SAAS;AAAA,MACT,WAAW;AAAA,MACX,kBAAkB;AAAA,MAClB,wBAAwB;AAAA,IAC1B;AAAA,IACA,SAAS;AAAA,MACP,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,EACV;AAAA;AAAA,EAEA,QAAQ;AAAA,IACN,YAAY;AAAA,MACV,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,SAAS,EAAE,MAAM,UAAU,IAAI,UAAU;AAAA,MACzC,SAAS,EAAE,MAAM,UAAU,IAAI,UAAU;AAAA,MACzC,SAAS;AAAA,MACT,SAAS;AAAA,MACT,WAAW;AAAA,MACX,kBAAkB;AAAA,MAClB,wBAAwB;AAAA,IAC1B;AAAA,IACA,SAAS;AAAA,MACP,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,EACV;AAAA;AAAA,EAEA,MAAM;AAAA,IACJ,YAAY;AAAA,MACV,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,SAAS,EAAE,MAAM,UAAU,IAAI,UAAU;AAAA,MACzC,SAAS,EAAE,MAAM,WAAW,IAAI,SAAS;AAAA,MACzC,SAAS;AAAA,MACT,SAAS;AAAA,MACT,WAAW;AAAA,MACX,kBAAkB;AAAA,MAClB,wBAAwB;AAAA,IAC1B;AAAA,IACA,SAAS;AAAA,MACP,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,EACV;AAAA;AAAA,EAEA,WAAW;AAAA,IACT,QAAQ;AAAA;AAAA;AAAA,MAGN,iBAAiB;AAAA,IACnB;AAAA,IACA,aAAa;AAAA,MACX,iBAAiB;AAAA,IACnB;AAAA,IACA,YAAY;AAAA,MACV,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,SAAS,EAAE,MAAM,WAAW,IAAI,UAAU;AAAA,MAC1C,SAAS,EAAE,MAAM,WAAW,IAAI,UAAU;AAAA,MAC1C,SAAS;AAAA,MACT,SAAS;AAAA,MACT,WAAW;AAAA,MACX,kBAAkB;AAAA,MAClB,wBAAwB;AAAA,IAC1B;AAAA,IACA,SAAS;AAAA,MACP,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AAAA,IACA,QAAQ;AAAA,EACV;AACF;AAaA,SAAS,cAAc,OAA2B,OAA+C;AAC/F,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,QAAQ,GAAG,KAAK,KAAK,KAAK,KAAK;AACxC;AAiBO,SAAS,oBACd,QACA,OACmB;AACnB,QAAM,iBAAiB,cAAc,MAAM,SAAS,OAAO,YAAY,cAAc;AACrF,QAAM,cAAc,cAAc,MAAM,MAAM,OAAO,YAAY,WAAW;AAC5E,SAAO;AAAA,IACL,GAAG;AAAA,IACH,YAAY;AAAA,MACV,GAAG,OAAO;AAAA,MACV,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,MACzD,GAAI,gBAAgB,SAAY,EAAE,YAAY,IAAI,CAAC;AAAA,IACrD;AAAA,EACF;AACF;;;AC/JO,SAAS,eAAe,MAAc,OAAwB;AACnE,SAAO,QAAQ,GAAG,IAAI,IAAI,KAAK,KAAK;AACtC;;;ACuCA,SAAS,QAAQ,SAAiB,OAA0C;AAC1E,MAAI,MAAM,KAAK,QAAQ,QAAQ,EAAE;AACjC,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,MAAM,QAAQ,QAAQ,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAC5D,QAAI,QAAS,QAAO,IAAI,OAAO;AAAA,EACjC;AACA,SAAO;AACT;AAQA,SAAS,aAAa,MAAuB,MAA8B;AACzE,SAAO,QAAQ,KAAK,SAAS,KAAK,YAAY,SAAS,KAAK,IAAI;AAClE;AAEA,SAAS,SAAS,MAA4D;AAC5E,MAAI,KAAK,IAAI,SAAU,QAAO,CAAC,EAAE,KAAK,KAAK,IAAI,SAAS,CAAC;AACzD,QAAM,OAAO,KAAK,OAAO,KAAK,CAAC,UAAU,MAAM,SAAS,MAAM;AAC9D,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,CAAC,EAAE,KAAK,KAAK,KAAK,OAAO,KAAK,OAAO,QAAQ,KAAK,QAAQ,KAAK,KAAK,IAAI,CAAC;AAClF;AAGO,SAAS,sBACd,MACA,MACkB;AAClB,QAAM,MAAM,aAAa,MAAM,IAAI;AACnC,QAAM,QAAQ,KAAK,IAAI,cAAc,KAAK;AAC1C,QAAM,cAAc,KAAK,IAAI,oBAAoB,KAAK;AACtD,QAAM,SAAS,SAAS,IAAI;AAC5B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,YAAY,EAAE,WAAW,IAAI;AAAA,IAC7B,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,MAC3B,GAAI,KAAK,eAAe,EAAE,eAAe,KAAK,aAAa,IAAI,CAAC;AAAA,IAClE;AAAA,EACF;AACF;AAUO,SAAS,uBACd,MACA,MACyB;AACzB,QAAM,MAAM,aAAa,MAAM,IAAI;AACnC,QAAM,SAAS,KAAK,OAAO,IAAI,CAAC,UAAU,MAAM,GAAG;AACnD,QAAM,MAA+B;AAAA,IACnC,UAAU,KAAK;AAAA,IACf,aAAa,KAAK,IAAI,oBAAoB,KAAK;AAAA,IAC/C,eAAe,KAAK;AAAA,IACpB,cAAc,KAAK,cAAc,KAAK;AAAA,IACtC,GAAI,OAAO,SAAS,IAAI,EAAE,OAAO,OAAO,IAAI,CAAC;AAAA,IAC7C,GAAI,KAAK,gBACL,EAAE,WAAW,EAAE,SAAS,gBAAgB,MAAM,KAAK,cAAc,EAAE,IACnE,CAAC;AAAA,EACP;AACA,QAAM,UAAU,KAAK;AACrB,QAAM,SACJ,WAAW,OAAO,YAAY,YAAY,OAAO,KAAK,OAAO,EAAE,SAAS,IACpE,EAAE,GAAG,KAAK,GAAG,QAAQ,IACrB;AACN,SAAO;AAAA,IACL,GAAG;AAAA,IACH,YAAY;AAAA,IACZ,SAAS;AAAA,IACT;AAAA,IACA,kBAAkB,EAAE,SAAS,WAAW,OAAO,IAAI;AAAA,EACrD;AACF;AAQA,IAAM,cAAc;AAEpB,IAAM,gBAAgB;AAEtB,IAAM,gBAAgB;AAef,SAAS,gBAAgB,KAA4B;AAC1D,QAAM,cAAc,IAAI,WAAW,eAAe,EAAE;AACpD,QAAM,QAAuB,CAAC;AAC9B,aAAW,SAAS,YAAY,SAAS,WAAW,GAAG;AACrD,UAAM,YAAY,MAAM,CAAC,KAAK,MAAM,CAAC,KAAK,IAAI,KAAK;AACnD,UAAM,SAAS,MAAM,CAAC,EAAE,WAAW,eAAe,GAAG,EAAE,WAAW,QAAQ,GAAG,EAAE,KAAK;AACpF,QAAI,YAAY,OAAQ,OAAM,KAAK,EAAE,UAAU,OAAO,CAAC;AAAA,EACzD;AACA,SAAO;AACT;AAQO,SAAS,eAAe,OAAsD;AACnF,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,YAAY,MAAM,IAAI,CAAC,UAAU;AAAA,MAC/B,SAAS;AAAA,MACT,MAAM,KAAK;AAAA,MACX,gBAAgB,EAAE,SAAS,UAAU,MAAM,KAAK,OAAO;AAAA,IACzD,EAAE;AAAA,EACJ;AACF;AAEA,IAAM,iBAAyC;AAAA,EAC7C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AASO,SAAS,gBAAgB,MAAuC;AACrE,SAAO,KAAK,UAAU,IAAI,EAAE,WAAW,sBAAsB,CAAC,OAAO,eAAe,EAAE,KAAK,EAAE;AAC/F;AAGO,SAAS,wBACd,OACA,MACoB;AACpB,SAAO,MAAM,IAAI,CAAC,UAAU;AAAA,IAC1B,KAAK,QAAQ,KAAK,SAAS,KAAK,YAAY,SAAS,KAAK,IAAI;AAAA,IAC9D,cAAc,KAAK;AAAA,EACrB,EAAE;AACJ;;;AClNA,4BAA0B;AAC1B,wBAAsB;AACtB,wBAAsB;AACtB,0BAAwB;AACxB,qBAAwB;AAsBxB,IAAM,kBAAkB,oBAAI,IAAI,CAAC,QAAQ,YAAY,CAAC;AAEtD,SAAS,YAAY,MAAyB;AAC5C,MAAI,OAAO;AACX,QAAM,QAAqB,CAAC;AAC5B,QAAM,QAAQ,KAAK,YAAY,CAAC;AAChC,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;AAC1C,UAAM,KAAK,MAAM,CAAC,CAAC;AAAA,EACrB;AACA,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,UAAU,MAAM,IAAI;AAC1B,QAAI,CAAC,QAAS;AACd,QAAI,gBAAgB,IAAI,QAAQ,IAAI,KAAK,OAAO,QAAQ,UAAU,UAAU;AAC1E,cAAQ,QAAQ;AAAA,IAClB;AACA,QAAI,MAAM,QAAQ,QAAQ,QAAQ,GAAG;AACnC,eAAS,IAAI,QAAQ,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AACrD,cAAM,KAAK,QAAQ,SAAS,CAAC,CAAC;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,oBAAoB,KAA4B;AAC9D,MAAI;AACJ,MAAI;AACF,eAAO,wBAAQ,EACZ,IAAI,oBAAAC,OAAW,EACf,IAAI,kBAAAC,OAAS,EACb,IAAI,kBAAAC,OAAS,EACb,MAAM,GAAG;AAAA,EACd,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,UAAU,IAAI,sBAAAC,QAAc;AAClC,QAAM,WAA0B,CAAC;AAKjC,QAAM,QAAqB,CAAC,IAAI;AAChC,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,OAAO,MAAM,IAAI;AACvB,QAAI,CAAC,KAAM;AACX,QAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,UAAU;AAC7D,YAAM,OAAO,YAAY,IAAI;AAG7B,YAAM,KAAK,QAAQ,KAAK,IAAI;AAC5B,UAAI,KAAK,UAAU,KAAK,KAAK,UAAU,GAAG;AACxC,iBAAS,KAAK,EAAE,OAAO,KAAK,OAAO,MAAM,GAAG,CAAC;AAAA,MAC/C;AAAA,IACF;AACA,QAAI,MAAM,QAAQ,KAAK,QAAQ,GAAG;AAChC,eAAS,IAAI,KAAK,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AAClD,cAAM,KAAK,KAAK,SAAS,CAAC,CAAC;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAGA,IAAM,mBAAmB;AAKzB,IAAM,gBAAgB;AAOf,SAAS,oBAAoB,KAAqB;AACvD,QAAM,QAAQ,IACX,WAAW,eAAe,GAAG,EAC7B,MAAM,KAAK,EACX,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE;AAC/B,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,gBAAgB,CAAC;AACzD;;;AC7GA,IAAAC,qBAAsB;AACtB,IAAAC,qBAAsB;AACtB,IAAAC,uBAAwB;AACxB,IAAAC,kBAAwB;AAYjB,IAAM,oBAAoB;AAE1B,IAAM,qBAAqB;AAE3B,IAAM,qBAAqB;AAElC,IAAM,iBAAiB;AA4ChB,IAAM,6BAA6B;AAAA,EACxC;AAAA,IACE,MAAM;AAAA,IACN,OAAO,CAAC;AAAA,IACR,UAAU,CAAC;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO,CAAC,QAAQ,OAAO;AAAA,IACvB,UAAU,CAAC,MAAM;AAAA,IACjB,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO,CAAC,IAAI;AAAA,IACZ,UAAU,CAAC;AAAA,IACX,aAAa;AAAA,IACb,aAAa;AAAA,IACb,mBACE;AAAA,EAEJ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO,CAAC,UAAU;AAAA,IAClB,UAAU,CAAC;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,IACb,mBACE;AAAA,EAGJ;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO,CAAC;AAAA,IACR,UAAU,CAAC;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,IACV,aAAa;AAAA,IACb,mBACE;AAAA,EAEJ;AACF;AAMO,IAAM,8BACX,2BAA2B,IAAI,CAAC,UAAU,MAAM,IAAI;AAOtD,IAAM,qBAAqB,oBAAI,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM,uBAA0E;AAAA,EAC9E,MAAM,EAAE,MAAM,YAAY,SAAS,0BAA0B;AAAA,EAC7D,UAAU,EAAE,MAAM,OAAO,SAAS,2CAA2C;AAAA,EAC7E,mBAAmB,EAAE,MAAM,cAAc,SAAS,6CAAwC;AAAA,EAC1F,mBAAmB,EAAE,MAAM,cAAc,SAAS,6CAAwC;AAC5F;AAWA,IAAM,kBAAiD,OAAO;AAAA,EAC5D,2BAA2B,IAAI,CAAC,UAAU;AAAA,IACxC,MAAM;AAAA,IACN,EAAE,OAAO,MAAM,OAAO,UAAU,MAAM,UAAU,eAAe,CAAC,MAAM,YAAY;AAAA,EACpF,CAAC;AACH;AAEA,IAAM,gBAAgB;AACtB,IAAM,sBAAsB,oBAAI,IAAI,CAAC,QAAQ,SAAS,QAAQ,CAAC;AAQ/D,SAAS,mBAAmB,QAA+B;AAIzD,QAAM,UAAU,OAAO,WAAW,0BAA0B,EAAE;AAC9D,QAAM,QAAQ,cAAc,KAAK,OAAO;AACxC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,MAAM,CAAC,EAAE,YAAY;AACpC,SAAO,oBAAoB,IAAI,MAAM,IAAI,OAAO;AAClD;AAoBA,SAAS,SAAS,MAAsC;AACtD,SAAO,KAAK,UAAU,OAAO;AAC/B;AAEA,SAAS,eAAe,MAAkB,YAAsC;AAC9E,QAAM,OAAO,SAAS,IAAI;AAC1B,QAAM,OAAO,KAAK;AAElB,MAAI,CAAC,MAAM;AACT,eAAW,KAAK;AAAA,MACd,MAAM;AAAA,MACN,SAAS;AAAA,MACT;AAAA,IACF,CAAC;AACD;AAAA,EACF;AAEA,QAAM,OAAO,gBAAgB,IAAI;AACjC,MAAI,CAAC,MAAM;AACT,eAAW,KAAK;AAAA,MACd,MAAM;AAAA,MACN,SAAS,IAAI,IAAI,yCAAyC,4BAA4B,KAAK,IAAI,CAAC;AAAA,MAChG;AAAA,IACF,CAAC;AACD;AAAA,EACF;AAEA,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,QAAQ,KAAK,cAAc,CAAC,GAAG;AACxC,QAAI,KAAK,SAAS,qBAAqB,OAAO,KAAK,SAAS,UAAU;AAEpE,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN,SAAS,IAAI,IAAI;AAAA,QACjB;AAAA,MACF,CAAC;AACD;AAAA,IACF;AACA,QAAI,CAAC,KAAK,MAAM,SAAS,KAAK,IAAI,GAAG;AACnC,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN,SAAS,IAAI,IAAI,gCAAgC,KAAK,IAAI;AAAA,QAC1D;AAAA,MACF,CAAC;AACD;AAAA,IACF;AACA,QAAI,OAAO,KAAK,UAAU,UAAU;AAElC,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN,SAAS,IAAI,IAAI,IAAI,KAAK,IAAI;AAAA,QAC9B;AAAA,MACF,CAAC;AACD;AAAA,IACF;AACA,SAAK,IAAI,KAAK,IAAI;AAClB,QAAI,KAAK,SAAS,SAAS,KAAK,IAAI,GAAG;AACrC,YAAM,SAAS,mBAAmB,KAAK,KAAK;AAC5C,UAAI,WAAW,MAAM;AACnB,mBAAW,KAAK;AAAA,UACd,MAAM;AAAA,UACN,SAAS,IAAI,IAAI,IAAI,KAAK,IAAI,gCAAgC,MAAM;AAAA,UACpE;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,aAAW,YAAY,KAAK,OAAO;AACjC,QAAI,CAAC,KAAK,IAAI,QAAQ,GAAG;AACvB,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN,SAAS,IAAI,IAAI,yBAAyB,QAAQ;AAAA,QAClD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,CAAC,KAAK,kBAAkB,KAAK,UAAU,UAAU,KAAK,GAAG;AAC3D,eAAW,KAAK;AAAA,MACd,MAAM;AAAA,MACN,SAAS,IAAI,IAAI;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAsBO,SAAS,gBACd,KACA,SACyB;AACzB,QAAM,aAAiC,CAAC;AAExC,MAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,mBAAmB;AAC7D,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,SAAS,wBAAwB,iBAAiB;AAAA,QACpD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,eAAO,yBAAQ,EACZ,IAAI,qBAAAC,OAAW,EACf,IAAI,mBAAAC,OAAS,EACb,IAAI,mBAAAC,OAAS,EACb,MAAM,GAAG;AAAA,EACd,SAAS,OAAO;AACd,UAAM,OAAQ,MAA4B;AAC1C,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,UACT,GAAI,OAAO,SAAS,WAAW,EAAE,KAAK,IAAI,CAAC;AAAA,QAC7C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,SAAS,kBAAkB;AAC7B,UAAM,aAAa,KAAK,WAAW,CAAC;AACpC,QAAI,CAAC,cAAc,WAAW,SAAS,aAAa,WAAW,UAAU,GAAG;AAC1E,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN,SACE;AAAA,QACF,MAAM,SAAS,cAAc,IAAI;AAAA,MACnC,CAAC;AAAA,IACH;AAAA,EACF;AAIA,MAAI,YAAY;AAChB,QAAM,QAAoD,CAAC,EAAE,MAAM,MAAM,OAAO,EAAE,CAAC;AAEnF,SAAO,MAAM,SAAS,KAAK,WAAW,SAAS,gBAAgB;AAC7D,UAAM,OAAO,MAAM,IAAI;AACvB,QAAI,CAAC,KAAM;AACX,UAAM,EAAE,MAAM,MAAM,IAAI;AAExB,iBAAa;AACb,QAAI,YAAY,oBAAoB;AAClC,iBAAW,KAAK,EAAE,MAAM,cAAc,SAAS,8BAA8B,CAAC;AAC9E;AAAA,IACF;AACA,QAAI,QAAQ,oBAAoB;AAC9B,iBAAW,KAAK,EAAE,MAAM,eAAe,SAAS,4BAA4B,CAAC;AAC7E;AAAA,IACF;AAEA,UAAM,YAAY,qBAAqB,KAAK,IAAI;AAChD,QAAI,WAAW;AACb,iBAAW,KAAK,EAAE,GAAG,WAAW,MAAM,SAAS,IAAI,EAAE,CAAC;AACtD;AAAA,IACF;AAEA,QAAI,KAAK,SAAS,uBAAuB,KAAK,SAAS,qBAAqB;AAC1E,qBAAe,MAAM,UAAU;AAAA,IAIjC,WAAW,CAAC,mBAAmB,IAAI,KAAK,IAAI,GAAG;AAC7C,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN,SAAS,uBAAuB,KAAK,IAAI;AAAA,QACzC,MAAM,SAAS,IAAI;AAAA,MACrB,CAAC;AACD;AAAA,IACF;AAEA,SACG,KAAK,SAAS,UAAU,KAAK,SAAS,WAAW,KAAK,SAAS,iBAChE,OAAO,KAAK,QAAQ,UACpB;AACA,YAAM,SAAS,mBAAmB,KAAK,GAAG;AAC1C,UAAI,WAAW,MAAM;AACnB,mBAAW,KAAK;AAAA,UACd,MAAM;AAAA,UACN,SAAS,GAAG,KAAK,IAAI,+BAA+B,MAAM;AAAA,UAC1D,MAAM,SAAS,IAAI;AAAA,QACrB,CAAC;AAAA,MACH;AAAA,IACF;AAEA,QAAI,MAAM,QAAQ,KAAK,QAAQ,GAAG;AAChC,iBAAW,SAAS,KAAK,UAAU;AACjC,cAAM,KAAK,EAAE,MAAM,OAAO,OAAO,QAAQ,EAAE,CAAC;AAAA,MAC9C;AAAA,IACF;AAAA,EACF;AAEA,SAAO,WAAW,WAAW,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI,OAAO,WAAW;AAC1E;;;ACpcA,iBAA2B;AAC3B,yBAAuB;AACvB,IAAAC,qBAAsB;;;ACwEc;AAT7B,SAAS,wBACd,QACA,YACgB;AAChB,QAAM,aAAa,IAAI,IAAI,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;AAEnE,SAAO;AAAA;AAAA;AAAA,IAGL,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA,IACzD,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA,IACzD,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA,IACzD,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA,IACzD,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA,IACzD,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA,IACzD,GAAG,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,OAAG,GAAG,OAAQ,UAAS;AAAA,IACvD,GAAG,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,OAAG,GAAG,OAAQ,UAAS;AAAA,IACvD,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,MAAM,MAAM,4CAAC,SAAI,SAAQ,QAAO,KAAW,GAAG,OAAO;AAAA,IAC1E,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,SAAK,GAAG,OAAQ,UAAS;AAAA,IAC3D,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,UAAM,GAAG,OAAQ,UAAS;AAAA,IAC7D,YAAY,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,gBAAY,GAAG,OAAQ,UAAS;AAAA,IACzE,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA,IACzD,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA,IACzD,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,MAAM,4CAAC,QAAI,GAAG,OAAQ,UAAS;AAAA;AAAA;AAAA,IAGzD,OAAO,CAAC,EAAE,UAAU,GAAG,MAAM,MAC3B,4CAAC,SAAI,WAAW,eAAe,2BAA2B,YAAY,SAAS,GAC7E,sDAAC,WAAO,GAAG,OAAQ,UAAS,GAC9B;AAAA,IAEF,IAAI,CAAC,UAAU,4CAAC,QAAI,GAAG,OAAO;AAAA,IAC9B,SAAS,CAAC,EAAE,SAAS,MACnB,4CAAC,WAAM,WAAW,eAAe,wBAAwB,YAAY,OAAO,GACzE,UACH;AAAA,IAEF,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA;AAAA;AAAA;AAAA,MAIlB;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,eAAe,oBAAoB,YAAY,GAAG;AAAA,UAC7D;AAAA,UACA,oBAAkB;AAAA,UAClB,wBAAqB;AAAA,UAEpB;AAAA;AAAA,MACH;AAAA;AAAA,IAEF,WAAW,CAAC,EAAE,GAAG,MAAM;AACrB,YAAM,QAAQ,WAAW,IAAI,EAAE;AAC/B,UAAI,CAAC,MAAO,QAAO;AACnB,aACE,6CAAC,YAAO,WAAW,YAAY,QAC7B;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,MAAM;AAAA,YACX,KAAK,MAAM;AAAA,YACX,OAAO,MAAM;AAAA,YACb,QAAQ,MAAM;AAAA,YACd,SAAQ;AAAA;AAAA,QACV;AAAA,QACC,MAAM,UAAU,4CAAC,gBAAY,gBAAM,SAAQ,IAAgB;AAAA,SAC9D;AAAA,IAEJ;AAAA;AAAA;AAAA,IAGA,SAAS,CAAC,EAAE,UAAU,SAAS,MAC7B,6CAAC,aAAQ,WAAW,eAAe,yBAAyB,YAAY,OAAO,GAC7E;AAAA,kDAAC,aAAQ,WAAW,eAAe,6BAA6B,YAAY,WAAW,GACrF,sDAAC,QAAI,oBAAS,GAChB;AAAA,MACA,4CAAC,SAAI,WAAW,eAAe,2BAA2B,YAAY,SAAS,GAC5E,UACH;AAAA,OACF;AAAA,IAEF,cAAc,CAAC,EAAE,SAAS,MACxB,4CAAC,WAAM,WAAW,eAAe,8BAA8B,YAAY,YAAY,GACpF,UACH;AAAA,EAEJ;AACF;;;AC5IO,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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","names":["BlogPost","import_zod","remarkParse","remarkGfm","remarkMdx","GithubSlugger","import_remark_gfm","import_remark_mdx","import_remark_parse","import_unified","remarkParse","remarkGfm","remarkMdx","import_remark_gfm","import_jsx_runtime","remarkGfm","rehypeSlug","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","BlogPost"]}
|