@opendata-ai/openchart-vanilla 8.0.0 → 8.1.0

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/static.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  renderTileMapSVG
3
- } from "./chunk-2NAQRP66.js";
3
+ } from "./chunk-BGJT3GTB.js";
4
4
  import {
5
5
  renderChartSVG,
6
6
  resetSvgIdCounter
7
- } from "./chunk-EKAUQAAX.js";
7
+ } from "./chunk-XOZ6AM7C.js";
8
8
  import {
9
9
  SVG_NS,
10
10
  buildThemeStyleBlock
@@ -1,4 +1,6 @@
1
1
  import {
2
+ CANVAS_DEFAULT_UPDATE_MAX_MARKS,
3
+ DEFAULT_UPDATE_MAX_MARKS,
2
4
  FULL_VIEW,
3
5
  cameraTransform,
4
6
  camerasClose,
@@ -15,13 +17,14 @@ import {
15
17
  lerp,
16
18
  scrubCamera,
17
19
  storyMotion
18
- } from "../chunk-6BUCYCLD.js";
20
+ } from "../chunk-RQNAZZJM.js";
19
21
  import "../chunk-T32UHMZA.js";
20
- import "../chunk-EKAUQAAX.js";
22
+ import "../chunk-XOZ6AM7C.js";
21
23
  import "../chunk-RM5XS6NR.js";
22
24
 
23
25
  // src/story/create-chart-story.ts
24
26
  import { deepMergeSpec, isGeoMapSpec } from "@opendata-ai/openchart-core";
27
+ import { AUTO_CANVAS_THRESHOLD } from "@opendata-ai/openchart-engine";
25
28
 
26
29
  // src/story/crossfade.ts
27
30
  function crossfadeUpdate(container, applyUpdate, options = {}) {
@@ -330,6 +333,21 @@ function resolveSpecAtStep(base, steps, index) {
330
333
  }
331
334
  return spec;
332
335
  }
336
+ function morphWithinMarkCap(prevSpec, nextSpec, renderer) {
337
+ const mark = nextSpec.mark;
338
+ const markType = typeof mark === "string" ? mark : mark?.type;
339
+ if (markType !== "point" && markType !== "beeswarm") return true;
340
+ const rowCount = (s) => {
341
+ const data = s.data;
342
+ return Array.isArray(data) ? data.length : 0;
343
+ };
344
+ const count = Math.max(rowCount(prevSpec), rowCount(nextSpec));
345
+ const animation = nextSpec.animation;
346
+ const maxMarks = typeof animation === "object" && animation !== null ? animation.update?.maxMarks : void 0;
347
+ const canvas = markType === "point" && renderer !== "svg" && (renderer === "canvas" || rowCount(nextSpec) > AUTO_CANVAS_THRESHOLD);
348
+ const cap = maxMarks ?? (canvas ? CANVAS_DEFAULT_UPDATE_MAX_MARKS : DEFAULT_UPDATE_MAX_MARKS);
349
+ return count <= cap;
350
+ }
333
351
  function createChartStory(container, options, mountOptions) {
334
352
  const { spec, steps, triggerPosition = 0.4, cameraMode = "step" } = options;
335
353
  const baseSpec = spec;
@@ -469,7 +487,7 @@ function createChartStory(container, options, mountOptions) {
469
487
  return;
470
488
  }
471
489
  const prevSpec = isFirst ? null : resolveSpecAtStep(baseSpec, steps, clamped - 1);
472
- const willLikelyMorph = !isFirst && !editModeRequested && prevSpec !== null && canTransitionSpecShape(prevSpec, nextSpec);
490
+ const willLikelyMorph = !isFirst && !editModeRequested && prevSpec !== null && canTransitionSpecShape(prevSpec, nextSpec) && morphWithinMarkCap(prevSpec, nextSpec, mountOptions?.renderer ?? "auto");
473
491
  const applyUpdate = () => instance.update(nextSpec);
474
492
  if (isFirst || willLikelyMorph || editModeRequested) {
475
493
  applyUpdate();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/story/create-chart-story.ts","../../src/story/crossfade.ts","../../src/story/resolve-camera-target.ts","../../src/story/progress-math.ts","../../src/story/scroll-driver.ts","../../src/story/story-camera.ts"],"sourcesContent":["/**\n * `createChartStory`: a base spec plus an ordered list of deep-partial\n * patch steps, driven by scroll (built-in `ScrollDriver`) or directly via\n * `goTo(n)`.\n *\n * Animation-clock ownership (decision baked 2026-07-11, see\n * plans/design-evolution/11-scrollytelling.md): the data-update-transitions\n * driver in `../transition.ts` owns mark morphing; the ported tween in\n * `./tween.ts` owns camera and any scalar story-level animation. They never\n * animate the same property. `goTo` calls `ChartInstance.update()`, which\n * internally decides morph-vs-instant-swap via `canTransition`; this module\n * predicts that decision ahead of time (`canTransitionSpecShape`) only to\n * decide whether to arm the crossfade fallback, and separately drives the\n * camera tween. It does not bridge the two clocks.\n */\n\nimport type { DataRow, GeoMapSpec } from '@opendata-ai/openchart-core';\nimport { deepMergeSpec, isGeoMapSpec } from '@opendata-ai/openchart-core';\nimport { createGeoMap, type GeoMapInstance, type GeoMapMountOptions } from '../map-mount';\nimport { type ChartInstance, createChart, type MountOptions } from '../mount';\nimport { canTransitionSpecShape } from '../transition';\nimport {\n type Camera,\n camerasClose,\n dampCamera,\n FULL_VIEW,\n fitTarget,\n interpolateCamera,\n scrubCamera,\n type ViewBoxSize,\n} from './camera-math';\nimport { crossfadeUpdate } from './crossfade';\nimport { isDataCameraTarget, resolveCameraTarget } from './resolve-camera-target';\nimport { createScrollDriver } from './scroll-driver';\nimport { applyStoryCamera, readViewBox } from './story-camera';\nimport { createTween, easingFns, storyMotion } from './tween';\nimport type {\n ChartStoryInstance,\n ChartStoryOptions,\n StorySpec,\n StorySpecPatch,\n StoryStep,\n} from './types';\n\nfunction prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;\n try {\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n } catch {\n return false;\n }\n}\n\n/** Merge a step's `highlight` sugar into its `spec` patch as `encoding.color.highlight`. */\nfunction stepToPatch(step: StoryStep): StorySpecPatch {\n let patch = step.spec ?? {};\n if ('highlight' in step) {\n // `highlight: null` clears a highlight set by an earlier step. It must map\n // to `[]` (not `undefined`): deep-merge skips `undefined`, so `?? undefined`\n // would leave the prior step's highlight in place. An empty array is a real\n // value that replaces it, and the engine reads `highlight.length > 0` as\n // active, so `[]` reads as \"no highlight.\"\n const highlight = step.highlight === null ? [] : (step.highlight ?? undefined);\n patch = deepMergeSpec(patch, {\n encoding: { color: { highlight } },\n });\n }\n return patch;\n}\n\n/** Apply patches 0..index (inclusive) onto the base spec, cumulatively. */\nfunction resolveSpecAtStep(base: StorySpec, steps: StoryStep[], index: number): StorySpec {\n let spec: StorySpec = base;\n for (let i = 0; i <= index && i < steps.length; i++) {\n spec = deepMergeSpec(spec, stepToPatch(steps[i]!));\n }\n return spec;\n}\n\n/**\n * Create a scrollytelling story bound to a container. Mounts a chart with\n * the base spec, then drives it through cumulative patch steps as the\n * reader scrolls (or via `goTo`).\n *\n * `editMode` (any of `onEdit`/`onSelect`/`onDeselect`/`onTextEdit` on\n * `mountOptions`) and an active story are mutually exclusive in v1: passing\n * both warns once and disables the story's own drive loop, matching the\n * seriesSearch/edit-mode precedent in `mount.ts`.\n */\nexport function createChartStory<TData extends DataRow = DataRow>(\n container: HTMLElement,\n options: ChartStoryOptions<TData>,\n mountOptions?: MountOptions,\n): ChartStoryInstance {\n const { spec, steps, triggerPosition = 0.4, cameraMode = 'step' } = options;\n // The story machinery is spec-shape agnostic (it only deep-merges patches\n // and hands specs to `update()`, which accepts the non-generic union).\n // Widen once here so the internal helpers don't have to thread `TData`\n // through a discriminated union that TS can't narrow across variants.\n const baseSpec = spec as StorySpec;\n const isMap = isGeoMapSpec(baseSpec as unknown as Record<string, unknown>);\n\n if (isMap && cameraMode === 'scrub') {\n console.warn(\n '[openchart] cameraMode: \"scrub\" is not supported for map stories; falling back to \"step\" mode. Map camera is driven via geo.focus patches in the spec.',\n );\n }\n\n if (isMap && steps.some((s) => s.camera)) {\n console.warn(\n '[openchart] step.camera is ignored for map stories. Drive the map camera via geo.focus patches in the spec instead.',\n );\n }\n\n const editModeRequested = !!(\n mountOptions?.onEdit ||\n mountOptions?.onSelect ||\n mountOptions?.onDeselect ||\n mountOptions?.onTextEdit\n );\n if (editModeRequested) {\n console.warn(\n '[openchart] a chart story and edit mode are mutually exclusive; the story will not drive updates while editing callbacks are active.',\n );\n }\n\n let currentStep = -1;\n let destroyed = false;\n\n const initialSpec = resolveSpecAtStep(baseSpec, steps, 0);\n\n // Branch: map vs chart mount\n let instance: ChartInstance | GeoMapInstance;\n if (isMap) {\n // Not a spread: GeoMapMountOptions callback signatures differ from MountOptions.\n // When adding fields to GeoMapMountOptions, check if they should transfer here.\n const mapOpts: GeoMapMountOptions = {\n theme: mountOptions?.theme,\n darkMode: mountOptions?.darkMode,\n watermark: mountOptions?.watermark,\n responsive: mountOptions?.responsive,\n };\n instance = createGeoMap(container, initialSpec as GeoMapSpec, mapOpts);\n } else {\n instance = createChart(container, initialSpec as Exclude<StorySpec, GeoMapSpec>, mountOptions);\n }\n\n // Chart camera infra (not used for maps)\n const cameraTween = isMap\n ? null\n : createTween<Camera>({\n initial: { cx: 0, cy: 0, k: 1 },\n lerp: interpolateCamera,\n duration: storyMotion.camera,\n ease: easingFns.easeInOutCubic,\n onFrame: (camera) => {\n const vb = readViewBox(container);\n if (vb) applyStoryCamera(container, camera, vb);\n },\n });\n\n /** Fit a step's camera (data-coordinate or raw target, or full view) into the viewBox. */\n function fittedCameraForStep(step: StoryStep | undefined, vb: ViewBoxSize): Camera {\n if (!step?.camera) return fitTarget(FULL_VIEW(vb), vb);\n const target = isDataCameraTarget(step.camera)\n ? resolveCameraTarget((instance as ChartInstance).layout, step.camera)\n : step.camera;\n return fitTarget(target, vb);\n }\n\n // ---- Scrub-mode camera: self-stopping damping loop toward a live target ---\n // Step mode eases discretely on step change (the tween above). Scrub mode\n // reads continuous `stepProgress` and dwells on the active target through a\n // hold zone, transitioning in the tail of the span. The two modes never run\n // at once; the settle loop and the step tween are mutually exclusive drivers.\n let scrubCurrent: Camera | null = null;\n let scrubTarget: Camera | null = null;\n let settleRaf = 0;\n let settling = false;\n\n // Memoized per-step fitted cameras for scrub mode. The fit depends only on\n // the current layout (rebuilt on instance.update) and the viewBox size, so\n // recomputing it on every scroll frame is wasted work. Cache keyed on the\n // layout reference plus the viewBox dimensions; invalidate when either moves.\n let fittedCache: Camera[] | null = null;\n let fittedCacheLayout: unknown = null;\n let fittedCacheVb: ViewBoxSize | null = null;\n\n function fittedCamerasForScrub(vb: ViewBoxSize): Camera[] {\n if (\n fittedCache &&\n fittedCacheLayout === (instance as ChartInstance).layout &&\n fittedCacheVb &&\n fittedCacheVb.width === vb.width &&\n fittedCacheVb.height === vb.height\n ) {\n return fittedCache;\n }\n fittedCache = steps.map((step) => fittedCameraForStep(step, vb));\n fittedCacheLayout = (instance as ChartInstance).layout;\n fittedCacheVb = vb;\n return fittedCache;\n }\n\n function stopSettle(): void {\n if (settling) {\n cancelAnimationFrame(settleRaf);\n settling = false;\n }\n }\n\n function startSettle(): void {\n if (settling) return;\n settling = true;\n let last = performance.now();\n const tick = (now: number): void => {\n const target = scrubTarget;\n const current = scrubCurrent;\n const vb = readViewBox(container);\n if (!target || !current || !vb) {\n settling = false;\n return;\n }\n const dt = Math.min(now - last, 64);\n last = now;\n const next = dampCamera(current, target, storyMotion.cameraTau, dt);\n scrubCurrent = next;\n applyStoryCamera(container, next, vb);\n if (camerasClose(next, target)) {\n scrubCurrent = target;\n applyStoryCamera(container, target, vb);\n settling = false;\n return;\n }\n settleRaf = requestAnimationFrame(tick);\n };\n settleRaf = requestAnimationFrame(tick);\n }\n\n /** Step-mode camera: retargetable eased tween on step change. */\n function applyCameraForStep(step: StoryStep | undefined, snap: boolean): void {\n if (!cameraTween) return;\n const vb = readViewBox(container);\n if (!vb) return;\n cameraTween.to(fittedCameraForStep(step, vb), { snap: snap || prefersReducedMotion() });\n }\n\n /** Scrub-mode camera: continuous, driven by the scroll frame's stepProgress. */\n function applyScrubCamera(frame: { step: number; stepProgress: number }): void {\n const vb = readViewBox(container);\n if (!vb) return;\n const fitted = fittedCamerasForScrub(vb);\n if (fitted.length === 0) return;\n\n if (prefersReducedMotion()) {\n const clamped = Math.min(Math.max(frame.step, 0), fitted.length - 1);\n stopSettle();\n scrubCurrent = fitted[clamped]!;\n applyStoryCamera(container, scrubCurrent, vb);\n return;\n }\n\n scrubTarget = scrubCamera(\n frame.step,\n frame.stepProgress,\n fitted,\n 0.65,\n easingFns.easeInOutCubic,\n );\n if (scrubCurrent === null) scrubCurrent = scrubTarget;\n startSettle();\n }\n\n function goTo(index: number): void {\n if (destroyed) return;\n const clamped = Math.max(0, Math.min(index, steps.length - 1));\n if (clamped === currentStep) return;\n\n const isFirst = currentStep === -1;\n currentStep = clamped;\n\n const nextSpec = resolveSpecAtStep(baseSpec, steps, clamped);\n\n if (isMap) {\n // Maps always apply directly: update handles fill + camera via geo.focus\n (instance as GeoMapInstance).update(nextSpec as GeoMapSpec);\n return;\n }\n\n const prevSpec = isFirst ? null : resolveSpecAtStep(baseSpec, steps, clamped - 1);\n\n const willLikelyMorph =\n !isFirst &&\n !editModeRequested &&\n prevSpec !== null &&\n canTransitionSpecShape(prevSpec, nextSpec);\n\n const applyUpdate = () =>\n (instance as ChartInstance).update(nextSpec as Exclude<StorySpec, GeoMapSpec>);\n\n if (isFirst || willLikelyMorph || editModeRequested) {\n applyUpdate();\n } else {\n // Outside the morph gate (re-encode, type change, etc.): no step may\n // visibly snap, so crossfade the whole chart instead.\n crossfadeUpdate(container, applyUpdate, { reducedMotion: prefersReducedMotion() });\n }\n\n // Step mode drives the camera on each discrete step change. Scrub mode\n // drives it continuously from the scroll frame instead (see the scroll\n // subscription below), so it skips the discrete step tween here.\n if (cameraMode === 'step') applyCameraForStep(steps[clamped], isFirst);\n }\n\n const scrollDriver = editModeRequested ? null : createScrollDriver({ triggerPosition });\n let unsubscribeScroll: (() => void) | null = null;\n if (scrollDriver) {\n unsubscribeScroll = scrollDriver.progress.subscribe((frame) => {\n if (frame.step < 0) return;\n // Discrete spec steps always advance at step boundaries; the v1 non-goal\n // is continuous DATA morph scrubbing, not camera scrub. In scrub mode the\n // camera additionally follows the continuous frame.\n goTo(frame.step);\n if (!isMap && cameraMode === 'scrub') applyScrubCamera(frame);\n });\n }\n\n return {\n goTo,\n registerStep(index, el) {\n scrollDriver?.registerStep(index, el);\n return (nextEl: HTMLElement | null) => scrollDriver?.registerStep(index, nextEl);\n },\n setContainer(el) {\n scrollDriver?.setContainer(el);\n },\n get currentStep() {\n return currentStep;\n },\n get totalSteps() {\n return steps.length;\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n unsubscribeScroll?.();\n scrollDriver?.destroy();\n cameraTween?.cancel();\n stopSettle();\n instance.destroy();\n },\n };\n}\n","/**\n * Whole-chart crossfade fallback for story steps whose spec diff falls\n * outside the data-update-transitions morph gate (`canTransition` in\n * `../transition.ts`): re-encodes, type changes, and anything else that\n * would otherwise instant-snap. No step in a story may visibly snap, so\n * this ghosts the current rendered SVG over the container, lets\n * `ChartInstance.update()` perform its normal instant swap underneath,\n * then fades the ghost out to reveal the new state.\n *\n * This is deliberately NOT the mark-morphing transition driver: it never\n * interpolates geometry, only opacity. See the animation-clock ownership\n * note in the scrollytelling plan -- the transitions driver owns mark\n * morphing, this owns the pixel-level fallback when that driver declines.\n */\n\nimport { storyMotion } from './tween';\n\nexport interface CrossfadeOptions {\n /** Fade duration in ms. Default `storyMotion.crossfade`. */\n duration?: number;\n /** Skip the fade and swap instantly (reduced motion). */\n reducedMotion?: boolean;\n}\n\n/**\n * Ghost the container's current SVG, run `applyUpdate` (which mutates the\n * container to its next state), then fade the ghost out.\n */\nexport function crossfadeUpdate(\n container: HTMLElement,\n applyUpdate: () => void,\n options: CrossfadeOptions = {},\n): void {\n const duration = options.duration ?? storyMotion.crossfade;\n const svg = container.querySelector('svg');\n\n if (options.reducedMotion || !svg || duration <= 0) {\n applyUpdate();\n return;\n }\n\n const ghost = svg.cloneNode(true) as SVGElement;\n ghost.setAttribute('aria-hidden', 'true');\n ghost.style.position = 'absolute';\n ghost.style.inset = '0';\n ghost.style.width = '100%';\n ghost.style.height = '100%';\n ghost.style.pointerEvents = 'none';\n ghost.style.transition = `opacity ${duration}ms ease-out`;\n ghost.style.opacity = '1';\n\n const priorPosition = container.style.position;\n container.style.position = priorPosition || 'relative';\n container.appendChild(ghost);\n\n applyUpdate();\n\n // Force layout so the transition starts from opacity: 1 before dropping.\n void ghost.getBoundingClientRect();\n ghost.style.opacity = '0';\n\n const cleanup = () => {\n ghost.remove();\n if (!priorPosition) container.style.position = '';\n };\n ghost.addEventListener('transitionend', cleanup, { once: true });\n // Safety net in case transitionend never fires (e.g. element removed by\n // an intervening render before the transition completes).\n setTimeout(cleanup, duration + 100);\n}\n","/**\n * Resolves data-coordinate camera targets (`{ x: [a, b], y: [c, d] }`) to\n * viewBox-space `CameraTarget` rects using the compiled chart's axis ticks.\n *\n * There is no raw d3 scale on `ChartLayout` (by design -- the compiled\n * layout is the public surface, not engine internals), so resolution works\n * off `AxisTick[]`, which pairs each tick's raw data `value` with its pixel\n * `position`:\n * - Exact tick match (ordinal/categorical values, or a quantitative value\n * that happens to land on a tick): use that tick's position directly.\n * - Otherwise linear interpolation between the two nearest ticks by data\n * value. Works for quantitative and temporal axes (temporal values are\n * compared as timestamps). Ordinal axes with no exact match have no\n * well-ordered \"between\" and fall back to the nearest tick.\n */\n\nimport type { AxisLayout, AxisTick, ChartLayout } from '@opendata-ai/openchart-core';\nimport type { CameraTarget } from './camera-math';\nimport type { StoryDataCameraTarget } from './types';\n\nconst DEFAULT_PADDING = 24;\n\n/** Coerce a tick value (string, number, Date, ISO string) to a comparable number. */\nfunction toComparable(value: unknown): number | null {\n if (typeof value === 'number') return value;\n if (value instanceof Date) return value.getTime();\n if (typeof value === 'string') {\n const asDate = Date.parse(value);\n if (!Number.isNaN(asDate)) return asDate;\n }\n return null;\n}\n\n/** Resolve one data value to a pixel position along an axis, or null if unresolvable. */\nfunction resolveOnAxis(axis: AxisLayout, target: unknown): number | null {\n const ticks = axis.ticks;\n if (ticks.length === 0) return null;\n\n const exact = ticks.find((t) => t.value === target);\n if (exact) return exact.position;\n\n const targetNum = toComparable(target);\n if (targetNum === null) return null;\n\n // Ticks with comparable numeric values, sorted by data value.\n const numeric: Array<{ tick: AxisTick; value: number }> = [];\n for (const tick of ticks) {\n const v = toComparable(tick.value);\n if (v !== null) numeric.push({ tick, value: v });\n }\n if (numeric.length === 0) return null;\n numeric.sort((a, b) => a.value - b.value);\n\n if (targetNum <= numeric[0]!.value) return numeric[0]!.tick.position;\n const last = numeric.at(-1)!;\n if (targetNum >= last.value) return last.tick.position;\n\n for (let i = 0; i < numeric.length - 1; i++) {\n const lo = numeric[i]!;\n const hi = numeric[i + 1]!;\n if (targetNum >= lo.value && targetNum <= hi.value) {\n const span = hi.value - lo.value;\n const t = span === 0 ? 0 : (targetNum - lo.value) / span;\n return lo.tick.position + t * (hi.tick.position - lo.tick.position);\n }\n }\n return null;\n}\n\n/**\n * Resolve a data-coordinate camera target to viewBox space. Falls back to\n * the full chart area (padded) for axes that are missing or unresolvable,\n * so a malformed target degrades to \"camera does nothing\" rather than\n * throwing mid-scroll.\n */\nexport function resolveCameraTarget(\n layout: ChartLayout,\n target: StoryDataCameraTarget,\n): CameraTarget {\n const area = layout.area;\n const padding = target.padding ?? DEFAULT_PADDING;\n\n let x1 = area.x;\n let x2 = area.x + area.width;\n if (target.x && layout.axes.x) {\n const a = resolveOnAxis(layout.axes.x, target.x[0]);\n const b = resolveOnAxis(layout.axes.x, target.x[1]);\n if (a !== null && b !== null) {\n x1 = Math.min(a, b);\n x2 = Math.max(a, b);\n }\n }\n\n let y1 = area.y;\n let y2 = area.y + area.height;\n if (target.y && layout.axes.y) {\n const a = resolveOnAxis(layout.axes.y, target.y[0]);\n const b = resolveOnAxis(layout.axes.y, target.y[1]);\n if (a !== null && b !== null) {\n y1 = Math.min(a, b);\n y2 = Math.max(a, b);\n }\n }\n\n return {\n x: x1,\n y: y1,\n width: Math.max(x2 - x1, 1),\n height: Math.max(y2 - y1, 1),\n padding,\n };\n}\n\n/** Type guard: a camera step given in data coordinates vs. raw viewBox `CameraTarget`. */\nexport function isDataCameraTarget(\n value: StoryDataCameraTarget | CameraTarget,\n): value is StoryDataCameraTarget {\n return !('width' in value) && !('height' in value);\n}\n","import { clamp01 } from './tween';\n\n/**\n * Pure math for continuous scrollytelling progress. The `ScrollDriver`\n * measures DOM rects and feeds them here. Ported from\n * opendata/shared/lib/scrolly/progress-math.ts.\n */\n\nexport interface ScrollyFrame {\n /** Geometric active step; -1 while the trigger line is above step 0 */\n step: number;\n /** 0..1 progress of the trigger line through the active step's span */\n stepProgress: number;\n /** 0..1 traversal from first step top to last step bottom */\n progress: number;\n direction: 'down' | 'up';\n}\n\nexport type ScrollyFrameGeometry = Omit<ScrollyFrame, 'direction'>;\n\n/**\n * Compute the geometric frame from viewport-relative step tops.\n *\n * @param tops viewport-relative step top offsets, ordered by step index\n * @param lastBottom viewport-relative bottom of the final step\n * @param triggerY the trigger line's viewport y (innerHeight * triggerPosition)\n */\nexport function computeProgress(\n tops: number[],\n lastBottom: number,\n triggerY: number,\n): ScrollyFrameGeometry {\n const n = tops.length;\n if (n === 0 || triggerY < tops[0]!) {\n return { step: -1, stepProgress: 0, progress: 0 };\n }\n\n let step = n - 1;\n for (let i = 0; i < n - 1; i++) {\n if (triggerY < tops[i + 1]!) {\n step = i;\n break;\n }\n }\n\n const stepTop = tops[step]!;\n const spanEnd = step < n - 1 ? tops[step + 1]! : lastBottom;\n const span = Math.max(spanEnd - stepTop, 1);\n const firstTop = tops[0]!;\n\n return {\n step,\n stepProgress: clamp01((triggerY - stepTop) / span),\n progress: clamp01((triggerY - firstTop) / Math.max(lastBottom - firstTop, 1)),\n };\n}\n\n/**\n * Reduced-motion variant: no scrubbing. `stepProgress` pins to 0 and overall\n * progress quantizes to the step's start fraction, so consumers see discrete\n * snaps only.\n */\nexport function quantizeFrame(\n frame: ScrollyFrameGeometry,\n totalSteps: number,\n): ScrollyFrameGeometry {\n if (frame.step < 0 || totalSteps <= 0) {\n return { step: frame.step, stepProgress: 0, progress: 0 };\n }\n return {\n step: frame.step,\n stepProgress: 0,\n progress: clamp01(frame.step / totalSteps),\n };\n}\n\nexport function framesEqual(a: ScrollyFrame | null, b: ScrollyFrame): boolean {\n return (\n a !== null &&\n a.step === b.step &&\n a.stepProgress === b.stepProgress &&\n a.progress === b.progress &&\n a.direction === b.direction\n );\n}\n","/**\n * Framework-agnostic scroll-step driver: element registry + rAF-throttled\n * scroll handler + subscription store. Ported from the React\n * `use-scroll-steps` hook in opendata/shared; the store design (60fps\n * frames as subscriptions, never framework state) carries over unchanged\n * because vanilla, React, Vue, and Svelte all want the same contract.\n *\n * Measurement is lazy: with zero subscribers the scroll handler does no\n * rect reads.\n */\n\nimport { computeProgress, framesEqual, quantizeFrame, type ScrollyFrame } from './progress-math';\n\nexport interface ScrollyProgressStore {\n /** Latest frame; `{step: -1, ...}` sentinel before any measurement */\n get(): ScrollyFrame;\n /** Emits the current frame immediately on subscribe, then on every change */\n subscribe(cb: (frame: ScrollyFrame) => void): () => void;\n}\n\nexport interface ScrollDriverOptions {\n /** Fraction of viewport height where the trigger line sits. Default 0.4 */\n triggerPosition?: number;\n}\n\nexport interface ScrollDriver {\n /** Register the scrolling container that wraps all steps. */\n setContainer(el: HTMLElement | null): void;\n /** Register a step element by index. Pass `null` to unregister. */\n registerStep(index: number, el: HTMLElement | null): void;\n /** Continuous progress as a subscription store. */\n progress: ScrollyProgressStore;\n /** Scroll a step into view (center). Honors reduced motion. */\n scrollToStep(index: number): void;\n /** Force a re-measurement (e.g. after layout changes). */\n measure(): void;\n /** Tear down scroll/resize listeners and the reduced-motion media query. */\n destroy(): void;\n}\n\nconst SENTINEL_FRAME: ScrollyFrame = {\n step: -1,\n stepProgress: 0,\n progress: 0,\n direction: 'down',\n};\n\n/**\n * Create a scroll-step driver. Framework wrappers (React/Vue/Svelte hooks)\n * are thin adapters over this: they own component lifecycle, this owns the\n * measurement/subscription machinery.\n */\nexport function createScrollDriver(options: ScrollDriverOptions = {}): ScrollDriver {\n const triggerPosition = options.triggerPosition ?? 0.4;\n\n let container: HTMLElement | null = null;\n const steps = new Map<number, HTMLElement>();\n const subscribers = new Set<(frame: ScrollyFrame) => void>();\n\n let frame: ScrollyFrame = SENTINEL_FRAME;\n let direction: 'down' | 'up' = 'down';\n // Direction is tracked from the container's viewport-relative position, NOT\n // window.scrollY: in a host that scrolls an inner container rather than the\n // page (Ladle, modals, dashboard panes, most app shells) window.scrollY is\n // pinned at 0 and would report no movement. The container's rect top moves\n // whenever ANY ancestor scrolls, so it works in both cases.\n let lastTop: number | null = null;\n let reducedMotion = false;\n let ticking = false;\n\n const measureAndEmit = () => {\n if (subscribers.size === 0) return;\n if (!container || steps.size === 0) return;\n if (typeof window === 'undefined') return;\n\n const viewportH = window.innerHeight;\n const containerRect = container.getBoundingClientRect();\n // Skip when the story is more than a viewport away in either direction.\n if (containerRect.bottom < -viewportH || containerRect.top > viewportH * 2) return;\n\n const count = steps.size;\n const tops: number[] = new Array(count);\n let lastBottom = 0;\n for (let i = 0; i < count; i++) {\n const el = steps.get(i);\n if (!el) return; // sparse registration mid-mount; wait for the next tick\n const rect = el.getBoundingClientRect();\n tops[i] = rect.top;\n if (i === count - 1) lastBottom = rect.bottom;\n }\n\n const triggerY = viewportH * triggerPosition;\n let geometry = computeProgress(tops, lastBottom, triggerY);\n if (reducedMotion) {\n geometry = quantizeFrame(geometry, count);\n }\n\n const next: ScrollyFrame = { ...geometry, direction };\n if (framesEqual(frame, next)) return;\n frame = next;\n for (const cb of subscribers) cb(frame);\n };\n\n const onScroll = () => {\n if (ticking) return;\n ticking = true;\n requestAnimationFrame(() => {\n // The container rising up the viewport (top decreasing) means the reader\n // is moving down through the story.\n const currentTop = container?.getBoundingClientRect().top ?? null;\n if (currentTop !== null && lastTop !== null) {\n const delta = lastTop - currentTop;\n if (delta > 5 || delta < -5) {\n direction = delta > 5 ? 'down' : 'up';\n }\n }\n lastTop = currentTop;\n ticking = false;\n measureAndEmit();\n });\n };\n\n let mediaQuery: MediaQueryList | null = null;\n const onReducedMotionChange = (e: MediaQueryListEvent) => {\n reducedMotion = e.matches;\n measureAndEmit();\n };\n\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');\n reducedMotion = mediaQuery.matches;\n mediaQuery.addEventListener('change', onReducedMotionChange);\n }\n\n if (typeof window !== 'undefined') {\n // Capture phase on the document, not just `window` scroll. Scroll events do\n // not bubble, so a window listener ONLY fires when the page itself scrolls.\n // Any host that scrolls an inner container instead — Ladle (which sets\n // `body { overflow: hidden }`), modals, dashboard panes, most app shells —\n // would never notify the driver and the story sat frozen on step 0.\n // Capturing on the document sees scroll from any ancestor, and the\n // measurement below is already viewport-relative (getBoundingClientRect vs\n // innerHeight), so it needs no other change to work in both layouts.\n document.addEventListener('scroll', onScroll, { passive: true, capture: true });\n window.addEventListener('resize', onScroll, { passive: true });\n }\n\n const progress: ScrollyProgressStore = {\n get: () => frame,\n subscribe: (cb) => {\n // Refresh `frame` from current geometry BEFORE registering `cb`, so the\n // measure pass notifies only existing subscribers. Then hand the new\n // subscriber the current frame exactly once. Registering after the\n // measure avoids the double-invoke that happened when measureAndEmit\n // changed the frame (looping over cb) and the explicit cb(frame) fired again.\n measureAndEmit();\n subscribers.add(cb);\n cb(frame);\n return () => {\n subscribers.delete(cb);\n };\n },\n };\n\n return {\n setContainer(el) {\n container = el;\n // Seed the direction baseline so the first scroll compares against a real\n // position rather than reporting a spurious jump from null.\n lastTop = el?.getBoundingClientRect().top ?? null;\n measureAndEmit();\n },\n registerStep(index, el) {\n if (el) {\n steps.set(index, el);\n } else {\n steps.delete(index);\n }\n measureAndEmit();\n },\n progress,\n scrollToStep(index) {\n const el = steps.get(index);\n if (!el) return;\n el.scrollIntoView({\n block: 'center',\n behavior: reducedMotion ? 'auto' : 'smooth',\n });\n },\n measure() {\n measureAndEmit();\n },\n destroy() {\n if (typeof window !== 'undefined') {\n // `capture: true` must match the addEventListener call or the listener\n // is not removed.\n document.removeEventListener('scroll', onScroll, { capture: true });\n window.removeEventListener('resize', onScroll);\n }\n mediaQuery?.removeEventListener('change', onReducedMotionChange);\n subscribers.clear();\n steps.clear();\n container = null;\n },\n };\n}\n","/**\n * Applies the scrollytelling camera transform to a mounted chart's marks\n * group (`[data-oc-marks-group]`, stamped by svg-renderer.ts). Re-finds the\n * group on every apply since `ChartInstance.update()` tears down and\n * rebuilds the SVG on every render -- the previous group reference goes\n * stale the instant a step patch triggers a re-render.\n *\n * DO NOT USE THIS TO \"ZOOM IN\" ON A CARTESIAN CHART.\n *\n * This is a geometric magnification of the marks group and nothing else. The\n * axes, gridlines, and annotations are siblings of that group, so they do not\n * move: zoom a line chart to 2010-2020 and the lines blow up while the x-axis\n * still reads 2000-2020 and every annotation drifts off its data point. Stroke\n * widths scale with the transform too, so the lines visibly fatten. The chart\n * ends up misrepresenting its own axis.\n *\n * To zoom a cartesian chart, narrow the scale domain instead and let the engine\n * recompile:\n *\n * { spec: { encoding: { x: { scale: { domain: ['2019', ...], clip: true } } } } }\n *\n * That relabels the axis, re-anchors annotations, keeps stroke widths constant,\n * and still morphs (the field identity is unchanged, so `canTransitionSpecShape`\n * passes and the marks FLIP-tween into the new scale).\n *\n * The camera remains useful where there is no axis to contradict -- graph and\n * other non-cartesian views, where magnifying the geometry IS the intent.\n */\n\nimport { type Camera, cameraTransform, FULL_VIEW, type ViewBoxSize } from './camera-math';\n\n/** Read the current SVG's viewBox as a `ViewBoxSize`, or null if not yet rendered. */\nexport function readViewBox(container: HTMLElement): ViewBoxSize | null {\n const svg = container.querySelector('svg');\n const viewBoxAttr = svg?.getAttribute('viewBox');\n if (!viewBoxAttr) return null;\n const parts = viewBoxAttr.split(/\\s+/).map(Number);\n if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return null;\n return { width: parts[2]!, height: parts[3]! };\n}\n\n/** Apply a camera transform to the current marks group, if the chart is rendered. */\nexport function applyStoryCamera(container: HTMLElement, camera: Camera, vb: ViewBoxSize): void {\n const marksGroup = container.querySelector('[data-oc-marks-group]');\n if (!marksGroup) return;\n marksGroup.setAttribute('transform', cameraTransform(camera, vb));\n}\n\nexport { FULL_VIEW };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiBA,SAAS,eAAe,oBAAoB;;;ACWrC,SAAS,gBACd,WACA,aACA,UAA4B,CAAC,GACvB;AACN,QAAM,WAAW,QAAQ,YAAY,YAAY;AACjD,QAAM,MAAM,UAAU,cAAc,KAAK;AAEzC,MAAI,QAAQ,iBAAiB,CAAC,OAAO,YAAY,GAAG;AAClD,gBAAY;AACZ;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,UAAU,IAAI;AAChC,QAAM,aAAa,eAAe,MAAM;AACxC,QAAM,MAAM,WAAW;AACvB,QAAM,MAAM,QAAQ;AACpB,QAAM,MAAM,QAAQ;AACpB,QAAM,MAAM,SAAS;AACrB,QAAM,MAAM,gBAAgB;AAC5B,QAAM,MAAM,aAAa,WAAW,QAAQ;AAC5C,QAAM,MAAM,UAAU;AAEtB,QAAM,gBAAgB,UAAU,MAAM;AACtC,YAAU,MAAM,WAAW,iBAAiB;AAC5C,YAAU,YAAY,KAAK;AAE3B,cAAY;AAGZ,OAAK,MAAM,sBAAsB;AACjC,QAAM,MAAM,UAAU;AAEtB,QAAM,UAAU,MAAM;AACpB,UAAM,OAAO;AACb,QAAI,CAAC,cAAe,WAAU,MAAM,WAAW;AAAA,EACjD;AACA,QAAM,iBAAiB,iBAAiB,SAAS,EAAE,MAAM,KAAK,CAAC;AAG/D,aAAW,SAAS,WAAW,GAAG;AACpC;;;ACjDA,IAAM,kBAAkB;AAGxB,SAAS,aAAa,OAA+B;AACnD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,iBAAiB,KAAM,QAAO,MAAM,QAAQ;AAChD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,SAAS,KAAK,MAAM,KAAK;AAC/B,QAAI,CAAC,OAAO,MAAM,MAAM,EAAG,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAGA,SAAS,cAAc,MAAkB,QAAgC;AACvE,QAAM,QAAQ,KAAK;AACnB,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,QAAQ,MAAM,KAAK,CAAC,MAAM,EAAE,UAAU,MAAM;AAClD,MAAI,MAAO,QAAO,MAAM;AAExB,QAAM,YAAY,aAAa,MAAM;AACrC,MAAI,cAAc,KAAM,QAAO;AAG/B,QAAM,UAAoD,CAAC;AAC3D,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,aAAa,KAAK,KAAK;AACjC,QAAI,MAAM,KAAM,SAAQ,KAAK,EAAE,MAAM,OAAO,EAAE,CAAC;AAAA,EACjD;AACA,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAExC,MAAI,aAAa,QAAQ,CAAC,EAAG,MAAO,QAAO,QAAQ,CAAC,EAAG,KAAK;AAC5D,QAAM,OAAO,QAAQ,GAAG,EAAE;AAC1B,MAAI,aAAa,KAAK,MAAO,QAAO,KAAK,KAAK;AAE9C,WAAS,IAAI,GAAG,IAAI,QAAQ,SAAS,GAAG,KAAK;AAC3C,UAAM,KAAK,QAAQ,CAAC;AACpB,UAAM,KAAK,QAAQ,IAAI,CAAC;AACxB,QAAI,aAAa,GAAG,SAAS,aAAa,GAAG,OAAO;AAClD,YAAM,OAAO,GAAG,QAAQ,GAAG;AAC3B,YAAM,IAAI,SAAS,IAAI,KAAK,YAAY,GAAG,SAAS;AACpD,aAAO,GAAG,KAAK,WAAW,KAAK,GAAG,KAAK,WAAW,GAAG,KAAK;AAAA,IAC5D;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,oBACd,QACA,QACc;AACd,QAAM,OAAO,OAAO;AACpB,QAAM,UAAU,OAAO,WAAW;AAElC,MAAI,KAAK,KAAK;AACd,MAAI,KAAK,KAAK,IAAI,KAAK;AACvB,MAAI,OAAO,KAAK,OAAO,KAAK,GAAG;AAC7B,UAAM,IAAI,cAAc,OAAO,KAAK,GAAG,OAAO,EAAE,CAAC,CAAC;AAClD,UAAM,IAAI,cAAc,OAAO,KAAK,GAAG,OAAO,EAAE,CAAC,CAAC;AAClD,QAAI,MAAM,QAAQ,MAAM,MAAM;AAC5B,WAAK,KAAK,IAAI,GAAG,CAAC;AAClB,WAAK,KAAK,IAAI,GAAG,CAAC;AAAA,IACpB;AAAA,EACF;AAEA,MAAI,KAAK,KAAK;AACd,MAAI,KAAK,KAAK,IAAI,KAAK;AACvB,MAAI,OAAO,KAAK,OAAO,KAAK,GAAG;AAC7B,UAAM,IAAI,cAAc,OAAO,KAAK,GAAG,OAAO,EAAE,CAAC,CAAC;AAClD,UAAM,IAAI,cAAc,OAAO,KAAK,GAAG,OAAO,EAAE,CAAC,CAAC;AAClD,QAAI,MAAM,QAAQ,MAAM,MAAM;AAC5B,WAAK,KAAK,IAAI,GAAG,CAAC;AAClB,WAAK,KAAK,IAAI,GAAG,CAAC;AAAA,IACpB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG;AAAA,IACH,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;AAAA,IAC1B,QAAQ,KAAK,IAAI,KAAK,IAAI,CAAC;AAAA,IAC3B;AAAA,EACF;AACF;AAGO,SAAS,mBACd,OACgC;AAChC,SAAO,EAAE,WAAW,UAAU,EAAE,YAAY;AAC9C;;;AC3FO,SAAS,gBACd,MACA,YACA,UACsB;AACtB,QAAM,IAAI,KAAK;AACf,MAAI,MAAM,KAAK,WAAW,KAAK,CAAC,GAAI;AAClC,WAAO,EAAE,MAAM,IAAI,cAAc,GAAG,UAAU,EAAE;AAAA,EAClD;AAEA,MAAI,OAAO,IAAI;AACf,WAAS,IAAI,GAAG,IAAI,IAAI,GAAG,KAAK;AAC9B,QAAI,WAAW,KAAK,IAAI,CAAC,GAAI;AAC3B,aAAO;AACP;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,UAAU,OAAO,IAAI,IAAI,KAAK,OAAO,CAAC,IAAK;AACjD,QAAM,OAAO,KAAK,IAAI,UAAU,SAAS,CAAC;AAC1C,QAAM,WAAW,KAAK,CAAC;AAEvB,SAAO;AAAA,IACL;AAAA,IACA,cAAc,SAAS,WAAW,WAAW,IAAI;AAAA,IACjD,UAAU,SAAS,WAAW,YAAY,KAAK,IAAI,aAAa,UAAU,CAAC,CAAC;AAAA,EAC9E;AACF;AAOO,SAAS,cACd,OACA,YACsB;AACtB,MAAI,MAAM,OAAO,KAAK,cAAc,GAAG;AACrC,WAAO,EAAE,MAAM,MAAM,MAAM,cAAc,GAAG,UAAU,EAAE;AAAA,EAC1D;AACA,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,cAAc;AAAA,IACd,UAAU,QAAQ,MAAM,OAAO,UAAU;AAAA,EAC3C;AACF;AAEO,SAAS,YAAY,GAAwB,GAA0B;AAC5E,SACE,MAAM,QACN,EAAE,SAAS,EAAE,QACb,EAAE,iBAAiB,EAAE,gBACrB,EAAE,aAAa,EAAE,YACjB,EAAE,cAAc,EAAE;AAEtB;;;AC5CA,IAAM,iBAA+B;AAAA,EACnC,MAAM;AAAA,EACN,cAAc;AAAA,EACd,UAAU;AAAA,EACV,WAAW;AACb;AAOO,SAAS,mBAAmB,UAA+B,CAAC,GAAiB;AAClF,QAAM,kBAAkB,QAAQ,mBAAmB;AAEnD,MAAI,YAAgC;AACpC,QAAM,QAAQ,oBAAI,IAAyB;AAC3C,QAAM,cAAc,oBAAI,IAAmC;AAE3D,MAAI,QAAsB;AAC1B,MAAI,YAA2B;AAM/B,MAAI,UAAyB;AAC7B,MAAI,gBAAgB;AACpB,MAAI,UAAU;AAEd,QAAM,iBAAiB,MAAM;AAC3B,QAAI,YAAY,SAAS,EAAG;AAC5B,QAAI,CAAC,aAAa,MAAM,SAAS,EAAG;AACpC,QAAI,OAAO,WAAW,YAAa;AAEnC,UAAM,YAAY,OAAO;AACzB,UAAM,gBAAgB,UAAU,sBAAsB;AAEtD,QAAI,cAAc,SAAS,CAAC,aAAa,cAAc,MAAM,YAAY,EAAG;AAE5E,UAAM,QAAQ,MAAM;AACpB,UAAM,OAAiB,IAAI,MAAM,KAAK;AACtC,QAAI,aAAa;AACjB,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAM,KAAK,MAAM,IAAI,CAAC;AACtB,UAAI,CAAC,GAAI;AACT,YAAM,OAAO,GAAG,sBAAsB;AACtC,WAAK,CAAC,IAAI,KAAK;AACf,UAAI,MAAM,QAAQ,EAAG,cAAa,KAAK;AAAA,IACzC;AAEA,UAAM,WAAW,YAAY;AAC7B,QAAI,WAAW,gBAAgB,MAAM,YAAY,QAAQ;AACzD,QAAI,eAAe;AACjB,iBAAW,cAAc,UAAU,KAAK;AAAA,IAC1C;AAEA,UAAM,OAAqB,EAAE,GAAG,UAAU,UAAU;AACpD,QAAI,YAAY,OAAO,IAAI,EAAG;AAC9B,YAAQ;AACR,eAAW,MAAM,YAAa,IAAG,KAAK;AAAA,EACxC;AAEA,QAAM,WAAW,MAAM;AACrB,QAAI,QAAS;AACb,cAAU;AACV,0BAAsB,MAAM;AAG1B,YAAM,aAAa,WAAW,sBAAsB,EAAE,OAAO;AAC7D,UAAI,eAAe,QAAQ,YAAY,MAAM;AAC3C,cAAM,QAAQ,UAAU;AACxB,YAAI,QAAQ,KAAK,QAAQ,IAAI;AAC3B,sBAAY,QAAQ,IAAI,SAAS;AAAA,QACnC;AAAA,MACF;AACA,gBAAU;AACV,gBAAU;AACV,qBAAe;AAAA,IACjB,CAAC;AAAA,EACH;AAEA,MAAI,aAAoC;AACxC,QAAM,wBAAwB,CAAC,MAA2B;AACxD,oBAAgB,EAAE;AAClB,mBAAe;AAAA,EACjB;AAEA,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,iBAAa,OAAO,WAAW,kCAAkC;AACjE,oBAAgB,WAAW;AAC3B,eAAW,iBAAiB,UAAU,qBAAqB;AAAA,EAC7D;AAEA,MAAI,OAAO,WAAW,aAAa;AASjC,aAAS,iBAAiB,UAAU,UAAU,EAAE,SAAS,MAAM,SAAS,KAAK,CAAC;AAC9E,WAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;AAAA,EAC/D;AAEA,QAAM,WAAiC;AAAA,IACrC,KAAK,MAAM;AAAA,IACX,WAAW,CAAC,OAAO;AAMjB,qBAAe;AACf,kBAAY,IAAI,EAAE;AAClB,SAAG,KAAK;AACR,aAAO,MAAM;AACX,oBAAY,OAAO,EAAE;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,aAAa,IAAI;AACf,kBAAY;AAGZ,gBAAU,IAAI,sBAAsB,EAAE,OAAO;AAC7C,qBAAe;AAAA,IACjB;AAAA,IACA,aAAa,OAAO,IAAI;AACtB,UAAI,IAAI;AACN,cAAM,IAAI,OAAO,EAAE;AAAA,MACrB,OAAO;AACL,cAAM,OAAO,KAAK;AAAA,MACpB;AACA,qBAAe;AAAA,IACjB;AAAA,IACA;AAAA,IACA,aAAa,OAAO;AAClB,YAAM,KAAK,MAAM,IAAI,KAAK;AAC1B,UAAI,CAAC,GAAI;AACT,SAAG,eAAe;AAAA,QAChB,OAAO;AAAA,QACP,UAAU,gBAAgB,SAAS;AAAA,MACrC,CAAC;AAAA,IACH;AAAA,IACA,UAAU;AACR,qBAAe;AAAA,IACjB;AAAA,IACA,UAAU;AACR,UAAI,OAAO,WAAW,aAAa;AAGjC,iBAAS,oBAAoB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;AAClE,eAAO,oBAAoB,UAAU,QAAQ;AAAA,MAC/C;AACA,kBAAY,oBAAoB,UAAU,qBAAqB;AAC/D,kBAAY,MAAM;AAClB,YAAM,MAAM;AACZ,kBAAY;AAAA,IACd;AAAA,EACF;AACF;;;AC7KO,SAAS,YAAY,WAA4C;AACtE,QAAM,MAAM,UAAU,cAAc,KAAK;AACzC,QAAM,cAAc,KAAK,aAAa,SAAS;AAC/C,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,QAAQ,YAAY,MAAM,KAAK,EAAE,IAAI,MAAM;AACjD,MAAI,MAAM,WAAW,KAAK,MAAM,KAAK,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC,EAAG,QAAO;AACrE,SAAO,EAAE,OAAO,MAAM,CAAC,GAAI,QAAQ,MAAM,CAAC,EAAG;AAC/C;AAGO,SAAS,iBAAiB,WAAwB,QAAgB,IAAuB;AAC9F,QAAM,aAAa,UAAU,cAAc,uBAAuB;AAClE,MAAI,CAAC,WAAY;AACjB,aAAW,aAAa,aAAa,gBAAgB,QAAQ,EAAE,CAAC;AAClE;;;ALFA,SAAS,uBAAgC;AACvC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,WAAY,QAAO;AACrF,MAAI;AACF,WAAO,OAAO,WAAW,kCAAkC,EAAE;AAAA,EAC/D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,YAAY,MAAiC;AACpD,MAAI,QAAQ,KAAK,QAAQ,CAAC;AAC1B,MAAI,eAAe,MAAM;AAMvB,UAAM,YAAY,KAAK,cAAc,OAAO,CAAC,IAAK,KAAK,aAAa;AACpE,YAAQ,cAAc,OAAO;AAAA,MAC3B,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE;AAAA,IACnC,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAGA,SAAS,kBAAkB,MAAiB,OAAoB,OAA0B;AACxF,MAAI,OAAkB;AACtB,WAAS,IAAI,GAAG,KAAK,SAAS,IAAI,MAAM,QAAQ,KAAK;AACnD,WAAO,cAAc,MAAM,YAAY,MAAM,CAAC,CAAE,CAAC;AAAA,EACnD;AACA,SAAO;AACT;AAYO,SAAS,iBACd,WACA,SACA,cACoB;AACpB,QAAM,EAAE,MAAM,OAAO,kBAAkB,KAAK,aAAa,OAAO,IAAI;AAKpE,QAAM,WAAW;AACjB,QAAM,QAAQ,aAAa,QAA8C;AAEzE,MAAI,SAAS,eAAe,SAAS;AACnC,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,MAAI,SAAS,MAAM,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG;AACxC,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,QAAM,oBAAoB,CAAC,EACzB,cAAc,UACd,cAAc,YACd,cAAc,cACd,cAAc;AAEhB,MAAI,mBAAmB;AACrB,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AAEA,MAAI,cAAc;AAClB,MAAI,YAAY;AAEhB,QAAM,cAAc,kBAAkB,UAAU,OAAO,CAAC;AAGxD,MAAI;AACJ,MAAI,OAAO;AAGT,UAAM,UAA8B;AAAA,MAClC,OAAO,cAAc;AAAA,MACrB,UAAU,cAAc;AAAA,MACxB,WAAW,cAAc;AAAA,MACzB,YAAY,cAAc;AAAA,IAC5B;AACA,eAAW,aAAa,WAAW,aAA2B,OAAO;AAAA,EACvE,OAAO;AACL,eAAW,YAAY,WAAW,aAA+C,YAAY;AAAA,EAC/F;AAGA,QAAM,cAAc,QAChB,OACA,YAAoB;AAAA,IAClB,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,GAAG,EAAE;AAAA,IAC9B,MAAM;AAAA,IACN,UAAU,YAAY;AAAA,IACtB,MAAM,UAAU;AAAA,IAChB,SAAS,CAAC,WAAW;AACnB,YAAM,KAAK,YAAY,SAAS;AAChC,UAAI,GAAI,kBAAiB,WAAW,QAAQ,EAAE;AAAA,IAChD;AAAA,EACF,CAAC;AAGL,WAAS,oBAAoB,MAA6B,IAAyB;AACjF,QAAI,CAAC,MAAM,OAAQ,QAAO,UAAU,UAAU,EAAE,GAAG,EAAE;AACrD,UAAM,SAAS,mBAAmB,KAAK,MAAM,IACzC,oBAAqB,SAA2B,QAAQ,KAAK,MAAM,IACnE,KAAK;AACT,WAAO,UAAU,QAAQ,EAAE;AAAA,EAC7B;AAOA,MAAI,eAA8B;AAClC,MAAI,cAA6B;AACjC,MAAI,YAAY;AAChB,MAAI,WAAW;AAMf,MAAI,cAA+B;AACnC,MAAI,oBAA6B;AACjC,MAAI,gBAAoC;AAExC,WAAS,sBAAsB,IAA2B;AACxD,QACE,eACA,sBAAuB,SAA2B,UAClD,iBACA,cAAc,UAAU,GAAG,SAC3B,cAAc,WAAW,GAAG,QAC5B;AACA,aAAO;AAAA,IACT;AACA,kBAAc,MAAM,IAAI,CAAC,SAAS,oBAAoB,MAAM,EAAE,CAAC;AAC/D,wBAAqB,SAA2B;AAChD,oBAAgB;AAChB,WAAO;AAAA,EACT;AAEA,WAAS,aAAmB;AAC1B,QAAI,UAAU;AACZ,2BAAqB,SAAS;AAC9B,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,WAAS,cAAoB;AAC3B,QAAI,SAAU;AACd,eAAW;AACX,QAAI,OAAO,YAAY,IAAI;AAC3B,UAAM,OAAO,CAAC,QAAsB;AAClC,YAAM,SAAS;AACf,YAAM,UAAU;AAChB,YAAM,KAAK,YAAY,SAAS;AAChC,UAAI,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI;AAC9B,mBAAW;AACX;AAAA,MACF;AACA,YAAM,KAAK,KAAK,IAAI,MAAM,MAAM,EAAE;AAClC,aAAO;AACP,YAAM,OAAO,WAAW,SAAS,QAAQ,YAAY,WAAW,EAAE;AAClE,qBAAe;AACf,uBAAiB,WAAW,MAAM,EAAE;AACpC,UAAI,aAAa,MAAM,MAAM,GAAG;AAC9B,uBAAe;AACf,yBAAiB,WAAW,QAAQ,EAAE;AACtC,mBAAW;AACX;AAAA,MACF;AACA,kBAAY,sBAAsB,IAAI;AAAA,IACxC;AACA,gBAAY,sBAAsB,IAAI;AAAA,EACxC;AAGA,WAAS,mBAAmB,MAA6B,MAAqB;AAC5E,QAAI,CAAC,YAAa;AAClB,UAAM,KAAK,YAAY,SAAS;AAChC,QAAI,CAAC,GAAI;AACT,gBAAY,GAAG,oBAAoB,MAAM,EAAE,GAAG,EAAE,MAAM,QAAQ,qBAAqB,EAAE,CAAC;AAAA,EACxF;AAGA,WAAS,iBAAiB,OAAqD;AAC7E,UAAM,KAAK,YAAY,SAAS;AAChC,QAAI,CAAC,GAAI;AACT,UAAM,SAAS,sBAAsB,EAAE;AACvC,QAAI,OAAO,WAAW,EAAG;AAEzB,QAAI,qBAAqB,GAAG;AAC1B,YAAM,UAAU,KAAK,IAAI,KAAK,IAAI,MAAM,MAAM,CAAC,GAAG,OAAO,SAAS,CAAC;AACnE,iBAAW;AACX,qBAAe,OAAO,OAAO;AAC7B,uBAAiB,WAAW,cAAc,EAAE;AAC5C;AAAA,IACF;AAEA,kBAAc;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,iBAAiB,KAAM,gBAAe;AAC1C,gBAAY;AAAA,EACd;AAEA,WAAS,KAAK,OAAqB;AACjC,QAAI,UAAW;AACf,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,MAAM,SAAS,CAAC,CAAC;AAC7D,QAAI,YAAY,YAAa;AAE7B,UAAM,UAAU,gBAAgB;AAChC,kBAAc;AAEd,UAAM,WAAW,kBAAkB,UAAU,OAAO,OAAO;AAE3D,QAAI,OAAO;AAET,MAAC,SAA4B,OAAO,QAAsB;AAC1D;AAAA,IACF;AAEA,UAAM,WAAW,UAAU,OAAO,kBAAkB,UAAU,OAAO,UAAU,CAAC;AAEhF,UAAM,kBACJ,CAAC,WACD,CAAC,qBACD,aAAa,QACb,uBAAuB,UAAU,QAAQ;AAE3C,UAAM,cAAc,MACjB,SAA2B,OAAO,QAA0C;AAE/E,QAAI,WAAW,mBAAmB,mBAAmB;AACnD,kBAAY;AAAA,IACd,OAAO;AAGL,sBAAgB,WAAW,aAAa,EAAE,eAAe,qBAAqB,EAAE,CAAC;AAAA,IACnF;AAKA,QAAI,eAAe,OAAQ,oBAAmB,MAAM,OAAO,GAAG,OAAO;AAAA,EACvE;AAEA,QAAM,eAAe,oBAAoB,OAAO,mBAAmB,EAAE,gBAAgB,CAAC;AACtF,MAAI,oBAAyC;AAC7C,MAAI,cAAc;AAChB,wBAAoB,aAAa,SAAS,UAAU,CAAC,UAAU;AAC7D,UAAI,MAAM,OAAO,EAAG;AAIpB,WAAK,MAAM,IAAI;AACf,UAAI,CAAC,SAAS,eAAe,QAAS,kBAAiB,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA,aAAa,OAAO,IAAI;AACtB,oBAAc,aAAa,OAAO,EAAE;AACpC,aAAO,CAAC,WAA+B,cAAc,aAAa,OAAO,MAAM;AAAA,IACjF;AAAA,IACA,aAAa,IAAI;AACf,oBAAc,aAAa,EAAE;AAAA,IAC/B;AAAA,IACA,IAAI,cAAc;AAChB,aAAO;AAAA,IACT;AAAA,IACA,IAAI,aAAa;AACf,aAAO,MAAM;AAAA,IACf;AAAA,IACA,UAAU;AACR,UAAI,UAAW;AACf,kBAAY;AACZ,0BAAoB;AACpB,oBAAc,QAAQ;AACtB,mBAAa,OAAO;AACpB,iBAAW;AACX,eAAS,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/story/create-chart-story.ts","../../src/story/crossfade.ts","../../src/story/resolve-camera-target.ts","../../src/story/progress-math.ts","../../src/story/scroll-driver.ts","../../src/story/story-camera.ts"],"sourcesContent":["/**\n * `createChartStory`: a base spec plus an ordered list of deep-partial\n * patch steps, driven by scroll (built-in `ScrollDriver`) or directly via\n * `goTo(n)`.\n *\n * Animation-clock ownership (decision baked 2026-07-11, see\n * plans/design-evolution/11-scrollytelling.md): the data-update-transitions\n * driver in `../transition.ts` owns mark morphing; the ported tween in\n * `./tween.ts` owns camera and any scalar story-level animation. They never\n * animate the same property. `goTo` calls `ChartInstance.update()`, which\n * internally decides morph-vs-instant-swap via `canTransition`; this module\n * predicts that decision ahead of time (`canTransitionSpecShape`) only to\n * decide whether to arm the crossfade fallback, and separately drives the\n * camera tween. It does not bridge the two clocks.\n */\n\nimport type { DataRow, GeoMapSpec } from '@opendata-ai/openchart-core';\nimport { deepMergeSpec, isGeoMapSpec } from '@opendata-ai/openchart-core';\nimport { AUTO_CANVAS_THRESHOLD } from '@opendata-ai/openchart-engine';\nimport { createGeoMap, type GeoMapInstance, type GeoMapMountOptions } from '../map-mount';\nimport { type ChartInstance, createChart, type MountOptions } from '../mount';\nimport {\n CANVAS_DEFAULT_UPDATE_MAX_MARKS,\n canTransitionSpecShape,\n DEFAULT_UPDATE_MAX_MARKS,\n} from '../transition';\nimport {\n type Camera,\n camerasClose,\n dampCamera,\n FULL_VIEW,\n fitTarget,\n interpolateCamera,\n scrubCamera,\n type ViewBoxSize,\n} from './camera-math';\nimport { crossfadeUpdate } from './crossfade';\nimport { isDataCameraTarget, resolveCameraTarget } from './resolve-camera-target';\nimport { createScrollDriver } from './scroll-driver';\nimport { applyStoryCamera, readViewBox } from './story-camera';\nimport { createTween, easingFns, storyMotion } from './tween';\nimport type {\n ChartStoryInstance,\n ChartStoryOptions,\n StorySpec,\n StorySpecPatch,\n StoryStep,\n} from './types';\n\nfunction prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;\n try {\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n } catch {\n return false;\n }\n}\n\n/** Merge a step's `highlight` sugar into its `spec` patch as `encoding.color.highlight`. */\nfunction stepToPatch(step: StoryStep): StorySpecPatch {\n let patch = step.spec ?? {};\n if ('highlight' in step) {\n // `highlight: null` clears a highlight set by an earlier step. It must map\n // to `[]` (not `undefined`): deep-merge skips `undefined`, so `?? undefined`\n // would leave the prior step's highlight in place. An empty array is a real\n // value that replaces it, and the engine reads `highlight.length > 0` as\n // active, so `[]` reads as \"no highlight.\"\n const highlight = step.highlight === null ? [] : (step.highlight ?? undefined);\n patch = deepMergeSpec(patch, {\n encoding: { color: { highlight } },\n });\n }\n return patch;\n}\n\n/** Apply patches 0..index (inclusive) onto the base spec, cumulatively. */\nfunction resolveSpecAtStep(base: StorySpec, steps: StoryStep[], index: number): StorySpec {\n let spec: StorySpec = base;\n for (let i = 0; i <= index && i < steps.length; i++) {\n spec = deepMergeSpec(spec, stepToPatch(steps[i]!));\n }\n return spec;\n}\n\n/**\n * Predict whether a morphing update would clear the runtime mark cap\n * (`canTransition` check 8). The runtime veto fires after `render()` has\n * already swapped, so a step that predicts \"morph\" but exceeds the cap lands\n * as a hard snap; predicting it here keeps the crossfade fallback instead.\n *\n * Data rows ≈ marks only for point and beeswarm specs, so the estimate is\n * scoped to those marks. Point marks auto-promote to canvas above\n * AUTO_CANVAS_THRESHOLD (where the cap is CANVAS_DEFAULT_UPDATE_MAX_MARKS)\n * unless the host forces `renderer: 'svg'`; beeswarm never renders on canvas.\n */\nfunction morphWithinMarkCap(\n prevSpec: StorySpec,\n nextSpec: StorySpec,\n renderer: 'auto' | 'svg' | 'canvas',\n): boolean {\n const mark = (nextSpec as { mark?: string | { type?: string } }).mark;\n const markType = typeof mark === 'string' ? mark : mark?.type;\n if (markType !== 'point' && markType !== 'beeswarm') return true;\n\n const rowCount = (s: StorySpec): number => {\n const data = (s as { data?: unknown }).data;\n return Array.isArray(data) ? data.length : 0;\n };\n const count = Math.max(rowCount(prevSpec), rowCount(nextSpec));\n\n const animation = (nextSpec as { animation?: unknown }).animation;\n const maxMarks =\n typeof animation === 'object' && animation !== null\n ? (animation as { update?: { maxMarks?: number } }).update?.maxMarks\n : undefined;\n\n const canvas =\n markType === 'point' &&\n renderer !== 'svg' &&\n (renderer === 'canvas' || rowCount(nextSpec) > AUTO_CANVAS_THRESHOLD);\n const cap = maxMarks ?? (canvas ? CANVAS_DEFAULT_UPDATE_MAX_MARKS : DEFAULT_UPDATE_MAX_MARKS);\n return count <= cap;\n}\n\n/**\n * Create a scrollytelling story bound to a container. Mounts a chart with\n * the base spec, then drives it through cumulative patch steps as the\n * reader scrolls (or via `goTo`).\n *\n * `editMode` (any of `onEdit`/`onSelect`/`onDeselect`/`onTextEdit` on\n * `mountOptions`) and an active story are mutually exclusive in v1: passing\n * both warns once and disables the story's own drive loop, matching the\n * seriesSearch/edit-mode precedent in `mount.ts`.\n */\nexport function createChartStory<TData extends DataRow = DataRow>(\n container: HTMLElement,\n options: ChartStoryOptions<TData>,\n mountOptions?: MountOptions,\n): ChartStoryInstance {\n const { spec, steps, triggerPosition = 0.4, cameraMode = 'step' } = options;\n // The story machinery is spec-shape agnostic (it only deep-merges patches\n // and hands specs to `update()`, which accepts the non-generic union).\n // Widen once here so the internal helpers don't have to thread `TData`\n // through a discriminated union that TS can't narrow across variants.\n const baseSpec = spec as StorySpec;\n const isMap = isGeoMapSpec(baseSpec as unknown as Record<string, unknown>);\n\n if (isMap && cameraMode === 'scrub') {\n console.warn(\n '[openchart] cameraMode: \"scrub\" is not supported for map stories; falling back to \"step\" mode. Map camera is driven via geo.focus patches in the spec.',\n );\n }\n\n if (isMap && steps.some((s) => s.camera)) {\n console.warn(\n '[openchart] step.camera is ignored for map stories. Drive the map camera via geo.focus patches in the spec instead.',\n );\n }\n\n const editModeRequested = !!(\n mountOptions?.onEdit ||\n mountOptions?.onSelect ||\n mountOptions?.onDeselect ||\n mountOptions?.onTextEdit\n );\n if (editModeRequested) {\n console.warn(\n '[openchart] a chart story and edit mode are mutually exclusive; the story will not drive updates while editing callbacks are active.',\n );\n }\n\n let currentStep = -1;\n let destroyed = false;\n\n const initialSpec = resolveSpecAtStep(baseSpec, steps, 0);\n\n // Branch: map vs chart mount\n let instance: ChartInstance | GeoMapInstance;\n if (isMap) {\n // Not a spread: GeoMapMountOptions callback signatures differ from MountOptions.\n // When adding fields to GeoMapMountOptions, check if they should transfer here.\n const mapOpts: GeoMapMountOptions = {\n theme: mountOptions?.theme,\n darkMode: mountOptions?.darkMode,\n watermark: mountOptions?.watermark,\n responsive: mountOptions?.responsive,\n };\n instance = createGeoMap(container, initialSpec as GeoMapSpec, mapOpts);\n } else {\n instance = createChart(container, initialSpec as Exclude<StorySpec, GeoMapSpec>, mountOptions);\n }\n\n // Chart camera infra (not used for maps)\n const cameraTween = isMap\n ? null\n : createTween<Camera>({\n initial: { cx: 0, cy: 0, k: 1 },\n lerp: interpolateCamera,\n duration: storyMotion.camera,\n ease: easingFns.easeInOutCubic,\n onFrame: (camera) => {\n const vb = readViewBox(container);\n if (vb) applyStoryCamera(container, camera, vb);\n },\n });\n\n /** Fit a step's camera (data-coordinate or raw target, or full view) into the viewBox. */\n function fittedCameraForStep(step: StoryStep | undefined, vb: ViewBoxSize): Camera {\n if (!step?.camera) return fitTarget(FULL_VIEW(vb), vb);\n const target = isDataCameraTarget(step.camera)\n ? resolveCameraTarget((instance as ChartInstance).layout, step.camera)\n : step.camera;\n return fitTarget(target, vb);\n }\n\n // ---- Scrub-mode camera: self-stopping damping loop toward a live target ---\n // Step mode eases discretely on step change (the tween above). Scrub mode\n // reads continuous `stepProgress` and dwells on the active target through a\n // hold zone, transitioning in the tail of the span. The two modes never run\n // at once; the settle loop and the step tween are mutually exclusive drivers.\n let scrubCurrent: Camera | null = null;\n let scrubTarget: Camera | null = null;\n let settleRaf = 0;\n let settling = false;\n\n // Memoized per-step fitted cameras for scrub mode. The fit depends only on\n // the current layout (rebuilt on instance.update) and the viewBox size, so\n // recomputing it on every scroll frame is wasted work. Cache keyed on the\n // layout reference plus the viewBox dimensions; invalidate when either moves.\n let fittedCache: Camera[] | null = null;\n let fittedCacheLayout: unknown = null;\n let fittedCacheVb: ViewBoxSize | null = null;\n\n function fittedCamerasForScrub(vb: ViewBoxSize): Camera[] {\n if (\n fittedCache &&\n fittedCacheLayout === (instance as ChartInstance).layout &&\n fittedCacheVb &&\n fittedCacheVb.width === vb.width &&\n fittedCacheVb.height === vb.height\n ) {\n return fittedCache;\n }\n fittedCache = steps.map((step) => fittedCameraForStep(step, vb));\n fittedCacheLayout = (instance as ChartInstance).layout;\n fittedCacheVb = vb;\n return fittedCache;\n }\n\n function stopSettle(): void {\n if (settling) {\n cancelAnimationFrame(settleRaf);\n settling = false;\n }\n }\n\n function startSettle(): void {\n if (settling) return;\n settling = true;\n let last = performance.now();\n const tick = (now: number): void => {\n const target = scrubTarget;\n const current = scrubCurrent;\n const vb = readViewBox(container);\n if (!target || !current || !vb) {\n settling = false;\n return;\n }\n const dt = Math.min(now - last, 64);\n last = now;\n const next = dampCamera(current, target, storyMotion.cameraTau, dt);\n scrubCurrent = next;\n applyStoryCamera(container, next, vb);\n if (camerasClose(next, target)) {\n scrubCurrent = target;\n applyStoryCamera(container, target, vb);\n settling = false;\n return;\n }\n settleRaf = requestAnimationFrame(tick);\n };\n settleRaf = requestAnimationFrame(tick);\n }\n\n /** Step-mode camera: retargetable eased tween on step change. */\n function applyCameraForStep(step: StoryStep | undefined, snap: boolean): void {\n if (!cameraTween) return;\n const vb = readViewBox(container);\n if (!vb) return;\n cameraTween.to(fittedCameraForStep(step, vb), { snap: snap || prefersReducedMotion() });\n }\n\n /** Scrub-mode camera: continuous, driven by the scroll frame's stepProgress. */\n function applyScrubCamera(frame: { step: number; stepProgress: number }): void {\n const vb = readViewBox(container);\n if (!vb) return;\n const fitted = fittedCamerasForScrub(vb);\n if (fitted.length === 0) return;\n\n if (prefersReducedMotion()) {\n const clamped = Math.min(Math.max(frame.step, 0), fitted.length - 1);\n stopSettle();\n scrubCurrent = fitted[clamped]!;\n applyStoryCamera(container, scrubCurrent, vb);\n return;\n }\n\n scrubTarget = scrubCamera(\n frame.step,\n frame.stepProgress,\n fitted,\n 0.65,\n easingFns.easeInOutCubic,\n );\n if (scrubCurrent === null) scrubCurrent = scrubTarget;\n startSettle();\n }\n\n function goTo(index: number): void {\n if (destroyed) return;\n const clamped = Math.max(0, Math.min(index, steps.length - 1));\n if (clamped === currentStep) return;\n\n const isFirst = currentStep === -1;\n currentStep = clamped;\n\n const nextSpec = resolveSpecAtStep(baseSpec, steps, clamped);\n\n if (isMap) {\n // Maps always apply directly: update handles fill + camera via geo.focus\n (instance as GeoMapInstance).update(nextSpec as GeoMapSpec);\n return;\n }\n\n const prevSpec = isFirst ? null : resolveSpecAtStep(baseSpec, steps, clamped - 1);\n\n const willLikelyMorph =\n !isFirst &&\n !editModeRequested &&\n prevSpec !== null &&\n canTransitionSpecShape(prevSpec, nextSpec) &&\n morphWithinMarkCap(prevSpec, nextSpec, mountOptions?.renderer ?? 'auto');\n\n const applyUpdate = () =>\n (instance as ChartInstance).update(nextSpec as Exclude<StorySpec, GeoMapSpec>);\n\n if (isFirst || willLikelyMorph || editModeRequested) {\n applyUpdate();\n } else {\n // Outside the morph gate (re-encode, type change, etc.): no step may\n // visibly snap, so crossfade the whole chart instead.\n crossfadeUpdate(container, applyUpdate, { reducedMotion: prefersReducedMotion() });\n }\n\n // Step mode drives the camera on each discrete step change. Scrub mode\n // drives it continuously from the scroll frame instead (see the scroll\n // subscription below), so it skips the discrete step tween here.\n if (cameraMode === 'step') applyCameraForStep(steps[clamped], isFirst);\n }\n\n const scrollDriver = editModeRequested ? null : createScrollDriver({ triggerPosition });\n let unsubscribeScroll: (() => void) | null = null;\n if (scrollDriver) {\n unsubscribeScroll = scrollDriver.progress.subscribe((frame) => {\n if (frame.step < 0) return;\n // Discrete spec steps always advance at step boundaries; the v1 non-goal\n // is continuous DATA morph scrubbing, not camera scrub. In scrub mode the\n // camera additionally follows the continuous frame.\n goTo(frame.step);\n if (!isMap && cameraMode === 'scrub') applyScrubCamera(frame);\n });\n }\n\n return {\n goTo,\n registerStep(index, el) {\n scrollDriver?.registerStep(index, el);\n return (nextEl: HTMLElement | null) => scrollDriver?.registerStep(index, nextEl);\n },\n setContainer(el) {\n scrollDriver?.setContainer(el);\n },\n get currentStep() {\n return currentStep;\n },\n get totalSteps() {\n return steps.length;\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n unsubscribeScroll?.();\n scrollDriver?.destroy();\n cameraTween?.cancel();\n stopSettle();\n instance.destroy();\n },\n };\n}\n","/**\n * Whole-chart crossfade fallback for story steps whose spec diff falls\n * outside the data-update-transitions morph gate (`canTransition` in\n * `../transition.ts`): re-encodes, type changes, and anything else that\n * would otherwise instant-snap. No step in a story may visibly snap, so\n * this ghosts the current rendered SVG over the container, lets\n * `ChartInstance.update()` perform its normal instant swap underneath,\n * then fades the ghost out to reveal the new state.\n *\n * This is deliberately NOT the mark-morphing transition driver: it never\n * interpolates geometry, only opacity. See the animation-clock ownership\n * note in the scrollytelling plan -- the transitions driver owns mark\n * morphing, this owns the pixel-level fallback when that driver declines.\n */\n\nimport { storyMotion } from './tween';\n\nexport interface CrossfadeOptions {\n /** Fade duration in ms. Default `storyMotion.crossfade`. */\n duration?: number;\n /** Skip the fade and swap instantly (reduced motion). */\n reducedMotion?: boolean;\n}\n\n/**\n * Ghost the container's current SVG, run `applyUpdate` (which mutates the\n * container to its next state), then fade the ghost out.\n */\nexport function crossfadeUpdate(\n container: HTMLElement,\n applyUpdate: () => void,\n options: CrossfadeOptions = {},\n): void {\n const duration = options.duration ?? storyMotion.crossfade;\n const svg = container.querySelector('svg');\n\n if (options.reducedMotion || !svg || duration <= 0) {\n applyUpdate();\n return;\n }\n\n const ghost = svg.cloneNode(true) as SVGElement;\n ghost.setAttribute('aria-hidden', 'true');\n ghost.style.position = 'absolute';\n ghost.style.inset = '0';\n ghost.style.width = '100%';\n ghost.style.height = '100%';\n ghost.style.pointerEvents = 'none';\n ghost.style.transition = `opacity ${duration}ms ease-out`;\n ghost.style.opacity = '1';\n\n const priorPosition = container.style.position;\n container.style.position = priorPosition || 'relative';\n container.appendChild(ghost);\n\n applyUpdate();\n\n // Force layout so the transition starts from opacity: 1 before dropping.\n void ghost.getBoundingClientRect();\n ghost.style.opacity = '0';\n\n const cleanup = () => {\n ghost.remove();\n if (!priorPosition) container.style.position = '';\n };\n ghost.addEventListener('transitionend', cleanup, { once: true });\n // Safety net in case transitionend never fires (e.g. element removed by\n // an intervening render before the transition completes).\n setTimeout(cleanup, duration + 100);\n}\n","/**\n * Resolves data-coordinate camera targets (`{ x: [a, b], y: [c, d] }`) to\n * viewBox-space `CameraTarget` rects using the compiled chart's axis ticks.\n *\n * There is no raw d3 scale on `ChartLayout` (by design -- the compiled\n * layout is the public surface, not engine internals), so resolution works\n * off `AxisTick[]`, which pairs each tick's raw data `value` with its pixel\n * `position`:\n * - Exact tick match (ordinal/categorical values, or a quantitative value\n * that happens to land on a tick): use that tick's position directly.\n * - Otherwise linear interpolation between the two nearest ticks by data\n * value. Works for quantitative and temporal axes (temporal values are\n * compared as timestamps). Ordinal axes with no exact match have no\n * well-ordered \"between\" and fall back to the nearest tick.\n */\n\nimport type { AxisLayout, AxisTick, ChartLayout } from '@opendata-ai/openchart-core';\nimport type { CameraTarget } from './camera-math';\nimport type { StoryDataCameraTarget } from './types';\n\nconst DEFAULT_PADDING = 24;\n\n/** Coerce a tick value (string, number, Date, ISO string) to a comparable number. */\nfunction toComparable(value: unknown): number | null {\n if (typeof value === 'number') return value;\n if (value instanceof Date) return value.getTime();\n if (typeof value === 'string') {\n const asDate = Date.parse(value);\n if (!Number.isNaN(asDate)) return asDate;\n }\n return null;\n}\n\n/** Resolve one data value to a pixel position along an axis, or null if unresolvable. */\nfunction resolveOnAxis(axis: AxisLayout, target: unknown): number | null {\n const ticks = axis.ticks;\n if (ticks.length === 0) return null;\n\n const exact = ticks.find((t) => t.value === target);\n if (exact) return exact.position;\n\n const targetNum = toComparable(target);\n if (targetNum === null) return null;\n\n // Ticks with comparable numeric values, sorted by data value.\n const numeric: Array<{ tick: AxisTick; value: number }> = [];\n for (const tick of ticks) {\n const v = toComparable(tick.value);\n if (v !== null) numeric.push({ tick, value: v });\n }\n if (numeric.length === 0) return null;\n numeric.sort((a, b) => a.value - b.value);\n\n if (targetNum <= numeric[0]!.value) return numeric[0]!.tick.position;\n const last = numeric.at(-1)!;\n if (targetNum >= last.value) return last.tick.position;\n\n for (let i = 0; i < numeric.length - 1; i++) {\n const lo = numeric[i]!;\n const hi = numeric[i + 1]!;\n if (targetNum >= lo.value && targetNum <= hi.value) {\n const span = hi.value - lo.value;\n const t = span === 0 ? 0 : (targetNum - lo.value) / span;\n return lo.tick.position + t * (hi.tick.position - lo.tick.position);\n }\n }\n return null;\n}\n\n/**\n * Resolve a data-coordinate camera target to viewBox space. Falls back to\n * the full chart area (padded) for axes that are missing or unresolvable,\n * so a malformed target degrades to \"camera does nothing\" rather than\n * throwing mid-scroll.\n */\nexport function resolveCameraTarget(\n layout: ChartLayout,\n target: StoryDataCameraTarget,\n): CameraTarget {\n const area = layout.area;\n const padding = target.padding ?? DEFAULT_PADDING;\n\n let x1 = area.x;\n let x2 = area.x + area.width;\n if (target.x && layout.axes.x) {\n const a = resolveOnAxis(layout.axes.x, target.x[0]);\n const b = resolveOnAxis(layout.axes.x, target.x[1]);\n if (a !== null && b !== null) {\n x1 = Math.min(a, b);\n x2 = Math.max(a, b);\n }\n }\n\n let y1 = area.y;\n let y2 = area.y + area.height;\n if (target.y && layout.axes.y) {\n const a = resolveOnAxis(layout.axes.y, target.y[0]);\n const b = resolveOnAxis(layout.axes.y, target.y[1]);\n if (a !== null && b !== null) {\n y1 = Math.min(a, b);\n y2 = Math.max(a, b);\n }\n }\n\n return {\n x: x1,\n y: y1,\n width: Math.max(x2 - x1, 1),\n height: Math.max(y2 - y1, 1),\n padding,\n };\n}\n\n/** Type guard: a camera step given in data coordinates vs. raw viewBox `CameraTarget`. */\nexport function isDataCameraTarget(\n value: StoryDataCameraTarget | CameraTarget,\n): value is StoryDataCameraTarget {\n return !('width' in value) && !('height' in value);\n}\n","import { clamp01 } from './tween';\n\n/**\n * Pure math for continuous scrollytelling progress. The `ScrollDriver`\n * measures DOM rects and feeds them here. Ported from\n * opendata/shared/lib/scrolly/progress-math.ts.\n */\n\nexport interface ScrollyFrame {\n /** Geometric active step; -1 while the trigger line is above step 0 */\n step: number;\n /** 0..1 progress of the trigger line through the active step's span */\n stepProgress: number;\n /** 0..1 traversal from first step top to last step bottom */\n progress: number;\n direction: 'down' | 'up';\n}\n\nexport type ScrollyFrameGeometry = Omit<ScrollyFrame, 'direction'>;\n\n/**\n * Compute the geometric frame from viewport-relative step tops.\n *\n * @param tops viewport-relative step top offsets, ordered by step index\n * @param lastBottom viewport-relative bottom of the final step\n * @param triggerY the trigger line's viewport y (innerHeight * triggerPosition)\n */\nexport function computeProgress(\n tops: number[],\n lastBottom: number,\n triggerY: number,\n): ScrollyFrameGeometry {\n const n = tops.length;\n if (n === 0 || triggerY < tops[0]!) {\n return { step: -1, stepProgress: 0, progress: 0 };\n }\n\n let step = n - 1;\n for (let i = 0; i < n - 1; i++) {\n if (triggerY < tops[i + 1]!) {\n step = i;\n break;\n }\n }\n\n const stepTop = tops[step]!;\n const spanEnd = step < n - 1 ? tops[step + 1]! : lastBottom;\n const span = Math.max(spanEnd - stepTop, 1);\n const firstTop = tops[0]!;\n\n return {\n step,\n stepProgress: clamp01((triggerY - stepTop) / span),\n progress: clamp01((triggerY - firstTop) / Math.max(lastBottom - firstTop, 1)),\n };\n}\n\n/**\n * Reduced-motion variant: no scrubbing. `stepProgress` pins to 0 and overall\n * progress quantizes to the step's start fraction, so consumers see discrete\n * snaps only.\n */\nexport function quantizeFrame(\n frame: ScrollyFrameGeometry,\n totalSteps: number,\n): ScrollyFrameGeometry {\n if (frame.step < 0 || totalSteps <= 0) {\n return { step: frame.step, stepProgress: 0, progress: 0 };\n }\n return {\n step: frame.step,\n stepProgress: 0,\n progress: clamp01(frame.step / totalSteps),\n };\n}\n\nexport function framesEqual(a: ScrollyFrame | null, b: ScrollyFrame): boolean {\n return (\n a !== null &&\n a.step === b.step &&\n a.stepProgress === b.stepProgress &&\n a.progress === b.progress &&\n a.direction === b.direction\n );\n}\n","/**\n * Framework-agnostic scroll-step driver: element registry + rAF-throttled\n * scroll handler + subscription store. Ported from the React\n * `use-scroll-steps` hook in opendata/shared; the store design (60fps\n * frames as subscriptions, never framework state) carries over unchanged\n * because vanilla, React, Vue, and Svelte all want the same contract.\n *\n * Measurement is lazy: with zero subscribers the scroll handler does no\n * rect reads.\n */\n\nimport { computeProgress, framesEqual, quantizeFrame, type ScrollyFrame } from './progress-math';\n\nexport interface ScrollyProgressStore {\n /** Latest frame; `{step: -1, ...}` sentinel before any measurement */\n get(): ScrollyFrame;\n /** Emits the current frame immediately on subscribe, then on every change */\n subscribe(cb: (frame: ScrollyFrame) => void): () => void;\n}\n\nexport interface ScrollDriverOptions {\n /** Fraction of viewport height where the trigger line sits. Default 0.4 */\n triggerPosition?: number;\n}\n\nexport interface ScrollDriver {\n /** Register the scrolling container that wraps all steps. */\n setContainer(el: HTMLElement | null): void;\n /** Register a step element by index. Pass `null` to unregister. */\n registerStep(index: number, el: HTMLElement | null): void;\n /** Continuous progress as a subscription store. */\n progress: ScrollyProgressStore;\n /** Scroll a step into view (center). Honors reduced motion. */\n scrollToStep(index: number): void;\n /** Force a re-measurement (e.g. after layout changes). */\n measure(): void;\n /** Tear down scroll/resize listeners and the reduced-motion media query. */\n destroy(): void;\n}\n\nconst SENTINEL_FRAME: ScrollyFrame = {\n step: -1,\n stepProgress: 0,\n progress: 0,\n direction: 'down',\n};\n\n/**\n * Create a scroll-step driver. Framework wrappers (React/Vue/Svelte hooks)\n * are thin adapters over this: they own component lifecycle, this owns the\n * measurement/subscription machinery.\n */\nexport function createScrollDriver(options: ScrollDriverOptions = {}): ScrollDriver {\n const triggerPosition = options.triggerPosition ?? 0.4;\n\n let container: HTMLElement | null = null;\n const steps = new Map<number, HTMLElement>();\n const subscribers = new Set<(frame: ScrollyFrame) => void>();\n\n let frame: ScrollyFrame = SENTINEL_FRAME;\n let direction: 'down' | 'up' = 'down';\n // Direction is tracked from the container's viewport-relative position, NOT\n // window.scrollY: in a host that scrolls an inner container rather than the\n // page (Ladle, modals, dashboard panes, most app shells) window.scrollY is\n // pinned at 0 and would report no movement. The container's rect top moves\n // whenever ANY ancestor scrolls, so it works in both cases.\n let lastTop: number | null = null;\n let reducedMotion = false;\n let ticking = false;\n\n const measureAndEmit = () => {\n if (subscribers.size === 0) return;\n if (!container || steps.size === 0) return;\n if (typeof window === 'undefined') return;\n\n const viewportH = window.innerHeight;\n const containerRect = container.getBoundingClientRect();\n // Skip when the story is more than a viewport away in either direction.\n if (containerRect.bottom < -viewportH || containerRect.top > viewportH * 2) return;\n\n const count = steps.size;\n const tops: number[] = new Array(count);\n let lastBottom = 0;\n for (let i = 0; i < count; i++) {\n const el = steps.get(i);\n if (!el) return; // sparse registration mid-mount; wait for the next tick\n const rect = el.getBoundingClientRect();\n tops[i] = rect.top;\n if (i === count - 1) lastBottom = rect.bottom;\n }\n\n const triggerY = viewportH * triggerPosition;\n let geometry = computeProgress(tops, lastBottom, triggerY);\n if (reducedMotion) {\n geometry = quantizeFrame(geometry, count);\n }\n\n const next: ScrollyFrame = { ...geometry, direction };\n if (framesEqual(frame, next)) return;\n frame = next;\n for (const cb of subscribers) cb(frame);\n };\n\n const onScroll = () => {\n if (ticking) return;\n ticking = true;\n requestAnimationFrame(() => {\n // The container rising up the viewport (top decreasing) means the reader\n // is moving down through the story.\n const currentTop = container?.getBoundingClientRect().top ?? null;\n if (currentTop !== null && lastTop !== null) {\n const delta = lastTop - currentTop;\n if (delta > 5 || delta < -5) {\n direction = delta > 5 ? 'down' : 'up';\n }\n }\n lastTop = currentTop;\n ticking = false;\n measureAndEmit();\n });\n };\n\n let mediaQuery: MediaQueryList | null = null;\n const onReducedMotionChange = (e: MediaQueryListEvent) => {\n reducedMotion = e.matches;\n measureAndEmit();\n };\n\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');\n reducedMotion = mediaQuery.matches;\n mediaQuery.addEventListener('change', onReducedMotionChange);\n }\n\n if (typeof window !== 'undefined') {\n // Capture phase on the document, not just `window` scroll. Scroll events do\n // not bubble, so a window listener ONLY fires when the page itself scrolls.\n // Any host that scrolls an inner container instead — Ladle (which sets\n // `body { overflow: hidden }`), modals, dashboard panes, most app shells —\n // would never notify the driver and the story sat frozen on step 0.\n // Capturing on the document sees scroll from any ancestor, and the\n // measurement below is already viewport-relative (getBoundingClientRect vs\n // innerHeight), so it needs no other change to work in both layouts.\n document.addEventListener('scroll', onScroll, { passive: true, capture: true });\n window.addEventListener('resize', onScroll, { passive: true });\n }\n\n const progress: ScrollyProgressStore = {\n get: () => frame,\n subscribe: (cb) => {\n // Refresh `frame` from current geometry BEFORE registering `cb`, so the\n // measure pass notifies only existing subscribers. Then hand the new\n // subscriber the current frame exactly once. Registering after the\n // measure avoids the double-invoke that happened when measureAndEmit\n // changed the frame (looping over cb) and the explicit cb(frame) fired again.\n measureAndEmit();\n subscribers.add(cb);\n cb(frame);\n return () => {\n subscribers.delete(cb);\n };\n },\n };\n\n return {\n setContainer(el) {\n container = el;\n // Seed the direction baseline so the first scroll compares against a real\n // position rather than reporting a spurious jump from null.\n lastTop = el?.getBoundingClientRect().top ?? null;\n measureAndEmit();\n },\n registerStep(index, el) {\n if (el) {\n steps.set(index, el);\n } else {\n steps.delete(index);\n }\n measureAndEmit();\n },\n progress,\n scrollToStep(index) {\n const el = steps.get(index);\n if (!el) return;\n el.scrollIntoView({\n block: 'center',\n behavior: reducedMotion ? 'auto' : 'smooth',\n });\n },\n measure() {\n measureAndEmit();\n },\n destroy() {\n if (typeof window !== 'undefined') {\n // `capture: true` must match the addEventListener call or the listener\n // is not removed.\n document.removeEventListener('scroll', onScroll, { capture: true });\n window.removeEventListener('resize', onScroll);\n }\n mediaQuery?.removeEventListener('change', onReducedMotionChange);\n subscribers.clear();\n steps.clear();\n container = null;\n },\n };\n}\n","/**\n * Applies the scrollytelling camera transform to a mounted chart's marks\n * group (`[data-oc-marks-group]`, stamped by svg-renderer.ts). Re-finds the\n * group on every apply since `ChartInstance.update()` tears down and\n * rebuilds the SVG on every render -- the previous group reference goes\n * stale the instant a step patch triggers a re-render.\n *\n * DO NOT USE THIS TO \"ZOOM IN\" ON A CARTESIAN CHART.\n *\n * This is a geometric magnification of the marks group and nothing else. The\n * axes, gridlines, and annotations are siblings of that group, so they do not\n * move: zoom a line chart to 2010-2020 and the lines blow up while the x-axis\n * still reads 2000-2020 and every annotation drifts off its data point. Stroke\n * widths scale with the transform too, so the lines visibly fatten. The chart\n * ends up misrepresenting its own axis.\n *\n * To zoom a cartesian chart, narrow the scale domain instead and let the engine\n * recompile:\n *\n * { spec: { encoding: { x: { scale: { domain: ['2019', ...], clip: true } } } } }\n *\n * That relabels the axis, re-anchors annotations, keeps stroke widths constant,\n * and still morphs (the field identity is unchanged, so `canTransitionSpecShape`\n * passes and the marks FLIP-tween into the new scale).\n *\n * The camera remains useful where there is no axis to contradict -- graph and\n * other non-cartesian views, where magnifying the geometry IS the intent.\n */\n\nimport { type Camera, cameraTransform, FULL_VIEW, type ViewBoxSize } from './camera-math';\n\n/** Read the current SVG's viewBox as a `ViewBoxSize`, or null if not yet rendered. */\nexport function readViewBox(container: HTMLElement): ViewBoxSize | null {\n const svg = container.querySelector('svg');\n const viewBoxAttr = svg?.getAttribute('viewBox');\n if (!viewBoxAttr) return null;\n const parts = viewBoxAttr.split(/\\s+/).map(Number);\n if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return null;\n return { width: parts[2]!, height: parts[3]! };\n}\n\n/** Apply a camera transform to the current marks group, if the chart is rendered. */\nexport function applyStoryCamera(container: HTMLElement, camera: Camera, vb: ViewBoxSize): void {\n const marksGroup = container.querySelector('[data-oc-marks-group]');\n if (!marksGroup) return;\n marksGroup.setAttribute('transform', cameraTransform(camera, vb));\n}\n\nexport { FULL_VIEW };\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"mappings":"AAWA,iBEEE,62DEFA,4lBJAF,eQNE,mIAeA,6BAQA,qEAUA,yDAOA,uCAIA,qCASA,gJAgBA,qGAcA,4EAQA,yFRrFF,qBGNE,6CAIA,2KAQA,wIAOA,+GAMA,+HAQA,wFAOA,oDAYA,8CAIA,wHAQA,4HAQA,2EAMA,6EAMA,4EAWA,8EAgBA,sDAIA,sEAMA,4EAIA,wEAaA,sFAUA,sFASA,gCAMA,mFIhKA,ogBAsBE,4FAOA,iFAOA,gMAWA,iGAMA,0EAKA,mHAQA,yJAUA,oKAUA,uMAkBF,qFAOA,4FDlHA,2EAKA,gCAIA,2CIJA,kJAaA,iHASA,wDAIA,sRAeA,6RAoBA,uFAKA,iGAKA,gDAMA,qEAMA,oIAWA,0DAKA,2RAiBA,yKAWA,0GAMA,wCAIA,mFTtIA,kDAKA,+CAKA,qGASA,wHAUA,yGAWA,qFAKA,0MAYA,oRAeA,6DAOA,mXAoBA,0DAIA,yFAKA,yDIpHA,iGAKE,qFAMA,yDAIA,4DAKA,gGAAA,gGAOA,oJASA,gFAOA,mDAIA,sEAKA,iFAKA,2CAKF,sNAOA,kPAOA,2IAKA,gJAKA,iCASE,0FAAA,0FAaF,oLAYE,mLAAA,kLAaA,yFAKA,qFAKA,4CAAA,2CAOA,+FAKA,6DAMA,4FAKA,+DAaF,iPAaA,wFAAA,8EAAA,yFAAA,+EAMA,+IAOA,+BAGE,wCASF,oJAQE,6OAaE,oEAKA,2GAKA,+FAOJ,4DAIA,+CASA,gCAIA,iHAUA,gDASA,+DAKE,kEAOF,iFAOA,mGAWA,2DAIE,qCAKF,8CAQA,+BASE,qDAAA,qDAMA,qCAUA,6CAIA,kEASF,2EASA,oHQjYA,qIASA,kEAOA,2CAIA,4GASE,8bAkBA,2RAaF,qPAcA,qLAiBA,6DAIE,kEAIA,+FAOF,4CAIA,iCAIA,0HAOA,+EAQA,sEAMA,oDAKE,0OAUE,2GAQJ,qEAYA,iKFrKA,oEXCF,oBSJI,4JAMA,4JAOA,8NAOA,yOAOA,wRAAA,yRAcA,4RAaA,uRAYA,4RAAA,+RAaA,uKAAA,2KK/EF,8GAgBA,kHAgBA,iHAeA,4FAgBA,8IAaA,iGAaA,yDAUA,sGAaA,uFAWA,yFAUA,sGFpIE,yLAAA,wLAOA,qFAAA,oFASA,oRAYA,8MAOA,8MASA,kNAUA,qOAAA,sOAQA,+JAAA,+JAWA,8NAOA,8NAOA,8NAOA,iMAWA,sKAOA,wJAQA,gLASA,8JAKA,+EAKA,2KAQA,mQAYA,0NAQA,8PAYA,uLAMA,6KAeF,wJZrMF,yBeDE,uCACE,6LAAA,+LAAA,qMAAA","sources":["../src/styles/index.css","../src/styles/you-draw-it.css","../src/styles/tokens.css","../src/styles/chrome.css","../src/styles/dark.css","../src/styles/table.css","../src/styles/legend.css","../src/styles/tooltip.css","../src/styles/base.css","../src/styles/table-animation.css","../src/styles/series-search.css","../src/styles/sparkline.css","../src/styles/keyframes.css","../src/styles/graph.css","../src/styles/animation.css","../src/styles/reduced-motion.css"],"sourcesContent":["/**\n * @opendata-ai/openchart-core styles\n *\n * CSS with oc- prefix for all class names.\n * CSS custom properties for theme overrides.\n * Dark mode via .oc-dark class on the container.\n * Cascade layers: oc.tokens, oc.base, oc.components, oc.animation, oc.reduced-motion.\n */\n\n/* For optimal typography, load Inter: https://fonts.google.com/specimen/Inter */\n\n@layer oc.tokens, oc.base, oc.components, oc.animation, oc.reduced-motion;\n\n@import \"./tokens.css\";\n@import \"./dark.css\";\n@import \"./base.css\";\n@import \"./chrome.css\";\n@import \"./tooltip.css\";\n@import \"./legend.css\";\n@import \"./series-search.css\";\n@import \"./you-draw-it.css\";\n@import \"./table.css\";\n@import \"./table-animation.css\";\n@import \"./graph.css\";\n@import \"./sparkline.css\";\n@import \"./keyframes.css\";\n@import \"./animation.css\";\n@import \"./reduced-motion.css\";\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * You draw it (youDrawIt)\n *\n * NYT-style draw-then-reveal. SVG-space elements (hatched region, guess path,\n * reveal clip) render inside the chart SVG; the prompt + skip-to-reveal button\n * are an absolutely-positioned HTML overlay the vanilla adapter sizes over the\n * drawing region (inline left/top/width/height). All colors come from --oc-*\n * tokens so they flip in .oc-dark.\n * --------------------------------------------------------------------------- */\n\n /* Hatched \"draw here\" region. Fill comes from an SVG <pattern> (line color =\n * the target series color); this only governs overall opacity + fade-out. */\n .oc-ydi-region {\n opacity: 0.35;\n transition: opacity 0.4s ease;\n }\n\n .oc-ydi-region[data-ydi-hidden=\"true\"] {\n opacity: 0;\n }\n\n /* The vertical rule at `from` where drawing begins. */\n .oc-ydi-boundary {\n stroke: var(--oc-text-muted);\n stroke-width: 1;\n stroke-dasharray: 3 3;\n stroke-opacity: 0.6;\n }\n\n /* Reader's guess: a distinct pen-like stroke that reads as \"yours\" against the\n * solid real line. Dashed + focus-blue so it stays legible in both modes. */\n .oc-ydi-guess {\n stroke: var(--oc-focus);\n stroke-width: 2.5;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 1 6;\n }\n\n /* Host-supplied comparison line (\"what everyone else guessed\"). Muted so it\n * sits behind the reader's guess and the real line. */\n .oc-ydi-comparison {\n stroke: var(--oc-text-muted);\n stroke-width: 1.5;\n stroke-opacity: 0.5;\n stroke-dasharray: 4 4;\n }\n\n /* Reveal clip rect: the width animates from the `from` boundary to the full\n * area on reveal, wiping the real line in left-to-right. The vanilla adapter\n * adds .oc-ydi-clip-animate right before setting the final width; reduced\n * motion skips the class so the width snaps. */\n .oc-ydi-clip-rect.oc-ydi-clip-animate {\n transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1);\n }\n\n /* HTML controls overlay: prompt (top) + reveal button (bottom-right). */\n .oc-ydi-controls {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-end;\n pointer-events: none;\n font-family: var(--oc-font-family);\n padding: 8px;\n box-sizing: border-box;\n z-index: 10;\n }\n\n .oc-ydi-prompt {\n align-self: center;\n max-width: 100%;\n padding: 6px 12px;\n border-radius: 6px;\n background: var(--oc-card);\n border: 1px solid var(--oc-border);\n color: var(--oc-text-secondary);\n font-size: 13px;\n font-weight: 500;\n line-height: 16px;\n text-align: center;\n box-shadow: var(--oc-tooltip-shadow);\n }\n\n .oc-ydi-controls.oc-ydi-revealed .oc-ydi-prompt {\n display: none;\n }\n\n /* Skip-to-reveal button. pointer-events re-enabled (the overlay disables it)\n * so it stays clickable and keyboard-reachable. Min height enforced inline by\n * the adapter (>= 24px effective touch target). */\n .oc-ydi-reveal-button {\n pointer-events: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 6px 14px;\n border: 1px solid var(--oc-border);\n border-radius: 6px;\n background: var(--oc-bg);\n color: var(--oc-text);\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n box-shadow: var(--oc-tooltip-shadow);\n transition:\n background-color 0.15s,\n border-color 0.15s;\n }\n\n .oc-ydi-reveal-button:hover {\n background: var(--oc-hover-bg);\n }\n\n .oc-ydi-reveal-button:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 2px;\n }\n\n .oc-ydi-reveal-button:disabled {\n opacity: 0.5;\n cursor: default;\n }\n}\n","/* GENERATED FILE — do not edit. Source: packages/core/src/styles/token-definitions.ts. Regenerate: bun run generate:tokens */\n\n@layer oc.tokens {\n /* ---------------------------------------------------------------------------\n * Custom properties (light mode defaults)\n *\n * These are FALLBACK defaults. At runtime, the JS theme engine stamps the\n * resolved theme as inline --oc-* custom properties on each .oc-root\n * container (see packages/vanilla/src/theme-tokens.ts). These CSS values\n * serve as fallbacks for contexts where JS hasn't mounted yet (SSR, static\n * HTML examples).\n * --------------------------------------------------------------------------- */\n\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root {\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n\n /* Opt out of iOS text auto-inflation so measured widths match rendered text. */\n -webkit-text-size-adjust: 100%;\n text-size-adjust: 100%;\n\n --oc-font-family:\n \"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n --oc-font-mono: \"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace;\n\n /* Animation easing presets via CSS linear() */\n --oc-ease-smooth: linear(\n 0,\n 0.157,\n 0.438,\n 0.64,\n 0.766,\n 0.85,\n 0.906,\n 0.941,\n 0.964,\n 0.978,\n 0.988,\n 0.994,\n 0.998,\n 1\n );\n --oc-ease-snappy: linear(\n 0,\n 0.012,\n 0.048,\n 0.108,\n 0.194,\n 0.302,\n 0.426,\n 0.559,\n 0.69,\n 0.808,\n 0.905,\n 0.973,\n 1.013,\n 1.028,\n 1.023,\n 1.006,\n 0.984,\n 0.966,\n 0.957,\n 0.957,\n 0.964,\n 0.975,\n 0.986,\n 0.995,\n 1,\n 1.003,\n 1.002,\n 1,\n 0.998,\n 0.998,\n 0.999,\n 1\n );\n\n /* Animation timing defaults */\n --oc-animation-duration: 500ms;\n --oc-animation-stagger: 80ms;\n --oc-annotation-delay: 200ms;\n\n /* Typography scale (editorial design system) */\n --oc-title-size: 26px;\n --oc-title-weight: 590;\n --oc-title-tracking: -0.022em;\n --oc-subtitle-size: 14px;\n --oc-subtitle-weight: 450;\n --oc-source-size: 11px;\n --oc-source-weight: 450;\n --oc-body-size: 13px;\n --oc-eyebrow-size: 11px;\n --oc-eyebrow-weight: 550;\n --oc-eyebrow-tracking: 0.08em;\n\n /* Surfaces (light mode defaults) */\n --oc-bg: #ffffff;\n --oc-card: #ffffff;\n --oc-secondary: #f4f4f5; /* zinc-100, raised surface */\n\n /*\n * Text levels. Names invert across modes: in light mode \"muted\" sits at\n * a lighter zinc step than \"subtle\" because both are picked relative to\n * the active background, not from a fixed lightness ladder. The intent\n * is \"muted = first step away from primary text\"; \"subtle = next step\n * down\"; etc.\n *\n * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,\n * subtle=zinc-400, faint=zinc-300\n * Dark mode (dark.css): inverts the surface tokens but keeps the same\n * muted -> subtle -> faint progression away from primary.\n */\n --oc-text: #09090b;\n --oc-text-secondary: #3f3f46;\n --oc-text-muted: #71717a;\n --oc-text-faint: #d4d4d8;\n\n /* Lines */\n --oc-gridline: rgba(0, 0, 0, 0.1);\n --oc-axis: rgba(0, 0, 0, 0.1);\n --oc-border: rgba(0, 0, 0, 0.08);\n --oc-border-radius: 2px;\n\n /* Brand and semantic */\n --oc-accent: #06b6d4;\n --oc-accent-strong: #0891b2; /* darker cyan for line strokes on light bg */\n --oc-positive: #10b981;\n --oc-negative: #e11d48;\n --oc-focus: #3b82f6;\n /* static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode */\n --oc-focus-ring: rgba(59, 130, 246, 0.1);\n --oc-focus-ring-strong: rgba(59, 130, 246, 0.25);\n --oc-editable-hover: rgba(79, 70, 229, 0.35); /* edit-mode hover outline, indigo one-off */\n\n /* Spacing scale (4px base) — only --oc-space-2/4 are consumed (JS-stamped) */\n --oc-space-2: 8px;\n --oc-space-4: 16px;\n\n /* Interactive states */\n --oc-hover-bg: rgba(0, 0, 0, 0.025);\n --oc-tooltip-bg: rgba(255, 255, 255, 0.88);\n --oc-tooltip-border: rgba(0, 0, 0, 0.08);\n --oc-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12);\n --oc-tooltip-text: #09090b;\n --oc-legend-text: #3f3f46;\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Chrome (eyebrow, title, subtitle, source, byline, footer, brand)\n * --------------------------------------------------------------------------- */\n\n .oc-chrome {\n font-family: var(--oc-font-family);\n }\n\n .oc-eyebrow {\n font-size: var(--oc-eyebrow-size);\n font-weight: var(--oc-eyebrow-weight);\n letter-spacing: var(--oc-eyebrow-tracking);\n text-transform: uppercase;\n fill: var(--oc-accent);\n }\n\n .oc-title {\n font-size: var(--oc-title-size);\n font-weight: var(--oc-title-weight);\n letter-spacing: var(--oc-title-tracking);\n fill: var(--oc-text);\n }\n\n .oc-subtitle {\n font-size: var(--oc-subtitle-size);\n font-weight: var(--oc-subtitle-weight);\n fill: var(--oc-text-muted);\n }\n\n .oc-source,\n .oc-byline,\n .oc-footer {\n font-size: var(--oc-source-size);\n font-weight: var(--oc-source-weight);\n fill: var(--oc-text-muted);\n }\n\n .oc-brand {\n font-size: 11px;\n font-weight: 510;\n letter-spacing: 0.02em;\n fill: var(--oc-text-faint);\n }\n\n .oc-brand-dot {\n fill: var(--oc-accent);\n }\n\n .oc-eyebrow-dot {\n fill: var(--oc-accent);\n }\n\n /* ---------------------------------------------------------------------------\n * Metric bar (.oc-metrics) — optional row of label/value cells above chart\n * --------------------------------------------------------------------------- */\n\n .oc-metrics {\n font-family: var(--oc-font-family);\n }\n\n .oc-metric-label {\n font-size: 10px;\n font-weight: 510;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n fill: var(--oc-text-muted);\n }\n\n .oc-metric-value {\n font-size: 22px;\n font-weight: 510;\n letter-spacing: -0.01em;\n fill: var(--oc-text);\n font-variant-numeric: tabular-nums;\n }\n\n .oc-metric-delta-up {\n fill: var(--oc-positive);\n font-size: 12px;\n font-weight: 510;\n }\n\n .oc-metric-delta-down {\n fill: var(--oc-negative);\n font-size: 12px;\n font-weight: 510;\n }\n\n .oc-metric-secondary {\n fill: var(--oc-positive);\n font-size: 12px;\n font-weight: 400;\n }\n\n /* ---------------------------------------------------------------------------\n * Axis tick labels — inline variant sits above each gridline at the\n * chart-area left edge (no left gutter). Used by line/area y-axes.\n * --------------------------------------------------------------------------- */\n\n .oc-axis-tick-inline {\n font-size: 11px;\n font-weight: 400;\n fill: var(--oc-text-muted);\n }\n\n /* ---------------------------------------------------------------------------\n * Endpoint labels (right-side per-series column for line/area charts)\n *\n * Swatch idiom: a thicker colored stroke segment with an optional thinner\n * lighter strip below. Mirrors the refreshed traditional legend swatch so a\n * single chart never shows two swatch idioms (mock-2 alignment). Per-series\n * color is stamped inline by the renderer; CSS tokens here cover the muted\n * value text and the leader line so theme overrides flow through naturally.\n * --------------------------------------------------------------------------- */\n\n .oc-endpoint-labels {\n font-family: var(--oc-font-family);\n }\n\n .oc-endpoint-label {\n /* Color set inline per series; this fallback only kicks in if the renderer\n ever omits the inline fill. */\n fill: var(--oc-endpoint-label-color, var(--oc-text));\n }\n\n .oc-endpoint-value {\n fill: var(--oc-endpoint-value-color, var(--oc-text-muted));\n }\n\n .oc-endpoint-leader {\n stroke: var(--oc-endpoint-leader-color, currentColor);\n }\n\n /* .oc-endpoint-marker — fill/stroke set inline (open-ring style: bg fill, series stroke). */\n /* .oc-endpoint-swatch — color set inline per series. */\n\n /* ---------------------------------------------------------------------------\n * Annotation dot + subtitle (text-annotation extensions)\n * --------------------------------------------------------------------------- */\n\n /* .oc-annotation-dot — fill/stroke resolved by the engine (open-ring default = bg fill, series stroke). */\n\n .oc-annotation-subtitle {\n fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted));\n }\n\n /* ---------------------------------------------------------------------------\n * Table footer chrome\n * --------------------------------------------------------------------------- */\n\n /* Footer row: source/footer text on the left, brand watermark on the right,\n * sharing a single baseline. gap keeps them apart if the row gets narrow. */\n .oc-table-footer-row {\n display: flex;\n align-items: baseline;\n gap: 16px;\n padding: 16px 16px 4px;\n }\n\n /* Chrome block sits inside the flex row now, so it no longer owns the\n * footer's top padding or horizontal inset (the row provides both). */\n .oc-chrome-footer {\n padding-top: 0;\n }\n\n /* Brand watermark: pushed to the right edge (margin-left:auto holds it there\n * even when the source text is absent), never shrinks below its own text. */\n .oc-table-footer-row .oc-table-ref {\n margin-left: auto;\n flex-shrink: 0;\n text-align: right;\n }\n}\n","/* GENERATED FILE — do not edit. Source: packages/core/src/styles/token-definitions.ts. Regenerate: bun run generate:tokens */\n\n@layer oc.tokens {\n /* ---------------------------------------------------------------------------\n * Dark mode overrides (fallback defaults)\n *\n * At runtime, the JS theme engine stamps dark-adapted --oc-* custom\n * properties on each .oc-root container. These CSS values serve as\n * fallbacks for contexts where JS hasn't mounted yet.\n * --------------------------------------------------------------------------- */\n\n .oc-dark {\n /* Surfaces (zinc-based achromatic ramp) */\n --oc-bg: #09090b;\n --oc-card: #111113;\n --oc-secondary: #27272a;\n\n /* Text levels — see tokens.css for the cross-mode naming rationale */\n --oc-text: #f7f8f8;\n --oc-text-secondary: #d0d6e0;\n --oc-text-muted: #a1a1aa;\n --oc-text-faint: #52525b;\n\n /* Lines */\n --oc-gridline: rgba(255, 255, 255, 0.05);\n --oc-axis: rgba(255, 255, 255, 0.1);\n --oc-border: rgba(255, 255, 255, 0.1);\n\n /* Brand and semantic — accent stays cyan (no darkening on dark bg) */\n --oc-accent: #06b6d4;\n --oc-accent-strong: #06b6d4;\n --oc-positive: #34d399;\n --oc-negative: #fb7185;\n --oc-focus: #60a5fa;\n\n /* Interactive states */\n --oc-hover-bg: rgba(255, 255, 255, 0.05);\n --oc-tooltip-bg: rgba(17, 17, 19, 0.92);\n --oc-tooltip-border: rgba(255, 255, 255, 0.08);\n --oc-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4);\n --oc-tooltip-text: #f7f8f8;\n --oc-legend-text: #d0d6e0;\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Table base\n * --------------------------------------------------------------------------- */\n\n .oc-table-wrapper {\n font-family: var(--oc-font-family);\n color: var(--oc-text);\n background: var(--oc-bg);\n\n & > .oc-chrome {\n margin-bottom: 16px;\n padding-left: 16px;\n padding-right: 16px;\n }\n\n & > .oc-chrome:first-child {\n padding-top: 4px;\n }\n\n & table {\n width: 100%;\n border-collapse: collapse;\n }\n\n & th,\n & td {\n padding: 10px 16px;\n text-align: left;\n border-bottom: 1px solid var(--oc-border);\n }\n\n & th {\n font-weight: 600;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--oc-text-secondary);\n white-space: nowrap;\n }\n\n & thead {\n position: sticky;\n top: 0;\n z-index: 2;\n background: var(--oc-bg);\n }\n\n & thead th {\n border-bottom-width: 2px;\n }\n\n & td {\n font-size: 14px;\n font-variant-numeric: tabular-nums;\n }\n\n & th:focus {\n outline: 2px solid var(--oc-focus);\n outline-offset: -2px;\n }\n\n & tbody:focus {\n outline: none;\n }\n }\n\n .oc-table-title {\n margin-bottom: 4px;\n font-size: var(--oc-title-computed-size, var(--oc-title-size));\n font-weight: var(--oc-title-computed-weight, var(--oc-title-weight));\n color: var(--oc-title-computed-color, var(--oc-text));\n }\n\n .oc-table-subtitle {\n margin-bottom: 8px;\n font-size: var(--oc-subtitle-computed-size, var(--oc-subtitle-size));\n font-weight: var(--oc-subtitle-computed-weight, var(--oc-subtitle-weight));\n color: var(--oc-subtitle-computed-color, var(--oc-text-secondary));\n }\n\n .oc-table-source {\n font-size: var(--oc-source-computed-size, var(--oc-source-size));\n color: var(--oc-source-computed-color, var(--oc-text-muted));\n }\n\n .oc-table-footer-text {\n font-size: var(--oc-footer-computed-size, var(--oc-source-size));\n color: var(--oc-footer-computed-color, var(--oc-text-muted));\n }\n\n .oc-table-scroll {\n overflow-x: auto;\n }\n\n /* ---------------------------------------------------------------------------\n * Sticky first column\n * --------------------------------------------------------------------------- */\n\n .oc-table--sticky {\n & th:first-child,\n & td:first-child {\n position: sticky;\n left: 0;\n z-index: 1;\n background: var(--oc-bg);\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Sort controls\n * --------------------------------------------------------------------------- */\n\n .oc-table-sort-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px;\n margin-left: 6px;\n display: inline-flex;\n flex-direction: column;\n align-items: center;\n vertical-align: middle;\n gap: 2px;\n\n &::before,\n &::after {\n content: \"\";\n display: block;\n width: 0;\n height: 0;\n border-left: 5px solid transparent;\n border-right: 5px solid transparent;\n transition:\n opacity 0.15s,\n border-color 0.15s;\n }\n\n &::before {\n border-bottom: 4.5px solid var(--oc-text-secondary);\n opacity: 0.45;\n }\n\n &::after {\n border-top: 4.5px solid var(--oc-text-secondary);\n opacity: 0.45;\n }\n\n &:hover::before,\n &:hover::after {\n opacity: 0.75;\n }\n }\n\n th[aria-sort=\"ascending\"] .oc-table-sort-btn {\n &::before {\n opacity: 1;\n border-bottom-color: var(--oc-text);\n }\n\n &::after {\n opacity: 0.15;\n }\n }\n\n th[aria-sort=\"descending\"] .oc-table-sort-btn {\n &::after {\n opacity: 1;\n border-top-color: var(--oc-text);\n }\n\n &::before {\n opacity: 0.15;\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Search\n * --------------------------------------------------------------------------- */\n\n /* Shared search-input skin (table + series-search).\n Padding excluded: it diverges (table: 8px 12px; series: 0 10px 0 28px)\n and series-search.css is imported before table.css — a padding declaration\n here would beat the series-search override by source order. */\n .oc-table-search input,\n .oc-series-search-input {\n width: 100%;\n border: 1px solid var(--oc-border);\n border-radius: 6px;\n font-size: 13px;\n font-family: inherit;\n background: var(--oc-bg);\n color: var(--oc-text);\n box-sizing: border-box;\n transition: border-color 0.15s;\n }\n\n .oc-table-search input::placeholder,\n .oc-series-search-input::placeholder {\n color: var(--oc-text-muted);\n font-size: 13px;\n }\n\n .oc-table-search input:focus,\n .oc-series-search-input:focus {\n outline: none;\n border-color: var(--oc-focus);\n box-shadow: 0 0 0 3px var(--oc-focus-ring);\n }\n\n .oc-table-search {\n padding: 8px 0;\n\n & input {\n padding: 8px 12px;\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Pagination\n * --------------------------------------------------------------------------- */\n\n .oc-table-pagination {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 0 4px;\n font-size: 13px;\n color: var(--oc-text-secondary);\n\n & button {\n padding: 6px 14px;\n border: 1px solid var(--oc-border);\n border-radius: 6px;\n background: var(--oc-bg);\n color: var(--oc-text);\n cursor: pointer;\n font-size: 13px;\n font-family: inherit;\n transition:\n background 0.15s,\n border-color 0.15s;\n\n &:disabled {\n opacity: 0.35;\n cursor: not-allowed;\n }\n\n &:hover:not(:disabled) {\n background: var(--oc-hover-bg);\n border-color: var(--oc-axis);\n }\n\n &:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 1px;\n }\n }\n }\n\n .oc-table-pagination-info {\n font-variant-numeric: tabular-nums;\n }\n\n .oc-table-pagination-btns {\n display: flex;\n gap: 8px;\n }\n\n /* ---------------------------------------------------------------------------\n * Bar cells\n * --------------------------------------------------------------------------- */\n\n .oc-table-bar {\n position: relative;\n }\n\n .oc-table-bar-fill {\n position: absolute;\n top: 6px;\n left: 0;\n bottom: 6px;\n border-radius: 2px;\n opacity: 0.15;\n pointer-events: none;\n }\n\n .oc-table-bar-value {\n position: relative;\n z-index: 1;\n }\n\n /* ---------------------------------------------------------------------------\n * Sparkline cells\n * --------------------------------------------------------------------------- */\n\n .oc-table-sparkline {\n display: block;\n width: 100%;\n position: relative;\n\n & svg {\n display: block;\n width: 100%;\n overflow: visible;\n }\n }\n\n .oc-table-sparkline-dot {\n position: absolute;\n border-radius: 50%;\n width: 5px;\n height: 5px;\n }\n\n .oc-table-sparkline-labels {\n display: flex;\n justify-content: space-between;\n font-size: 11px;\n line-height: 1;\n }\n\n /* ---------------------------------------------------------------------------\n * Image cells\n * --------------------------------------------------------------------------- */\n\n .oc-table-image {\n display: inline-block;\n vertical-align: middle;\n\n & img {\n object-fit: cover;\n }\n }\n\n .oc-table-image-rounded img {\n border-radius: 50%;\n }\n\n /* ---------------------------------------------------------------------------\n * Flag cells\n * --------------------------------------------------------------------------- */\n\n .oc-table-flag {\n font-size: 1.2em;\n }\n\n /* ---------------------------------------------------------------------------\n * Compact mode\n * --------------------------------------------------------------------------- */\n\n .oc-table--compact {\n & th,\n & td {\n padding: 4px 8px;\n font-size: 13px;\n }\n\n & th {\n font-size: 11px;\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Row hover (when onRowClick is set)\n * --------------------------------------------------------------------------- */\n\n .oc-table--clickable tbody {\n & tr {\n cursor: pointer;\n }\n\n & tr:hover {\n background: var(--oc-hover-bg);\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Keyboard cell focus indicator\n * --------------------------------------------------------------------------- */\n\n .oc-table-cell-focus {\n outline: 2px solid var(--oc-focus);\n outline-offset: -2px;\n }\n\n /* ---------------------------------------------------------------------------\n * Empty state\n * --------------------------------------------------------------------------- */\n\n .oc-table-empty {\n padding: 32px 16px;\n text-align: center;\n color: var(--oc-text-secondary);\n font-size: 14px;\n font-style: italic;\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Legend\n * --------------------------------------------------------------------------- */\n\n .oc-legend {\n font-family: var(--oc-font-family);\n font-size: var(--oc-body-size);\n }\n\n .oc-legend-entry {\n cursor: default;\n }\n\n .oc-legend text {\n fill: var(--oc-legend-text);\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Tooltip\n * Editorial card matching the new design system: zinc surface, cyan accent,\n * Inter Variable, mono numerics. Sized for a slice tooltip that lists every\n * series at the snapped x.\n * --------------------------------------------------------------------------- */\n\n .oc-tooltip {\n position: absolute;\n display: none;\n pointer-events: none;\n z-index: 1000;\n background: var(--oc-tooltip-bg);\n backdrop-filter: blur(14px);\n border: 1px solid var(--oc-tooltip-border);\n border-radius: var(--oc-border-radius, 2px);\n box-shadow: var(--oc-tooltip-shadow);\n color: var(--oc-tooltip-text);\n font-family: var(--oc-font-family);\n font-size: 12px;\n padding: 0;\n max-width: 280px;\n min-width: 160px;\n line-height: 1.4;\n animation: oc-tooltip-in 120ms ease-out;\n transition:\n left 100ms ease-in-out,\n top 100ms ease-in-out;\n\n & .oc-tooltip-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 6px;\n }\n\n & .oc-tooltip-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n }\n\n & .oc-tooltip-title {\n font-weight: 590;\n font-size: 11px;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--oc-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n & .oc-tooltip-body {\n padding: 6px 12px 10px;\n border-top: 1px solid var(--oc-tooltip-border);\n }\n\n /* No separator when body is the only child */\n & .oc-tooltip-body:first-child {\n border-top: none;\n padding-top: 10px;\n }\n\n & .oc-tooltip-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 16px;\n padding: 2px 0;\n }\n\n & .oc-tooltip-row-swatch {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n margin-right: 6px;\n flex-shrink: 0;\n transform: translateY(-1px);\n }\n\n & .oc-tooltip-label {\n color: var(--oc-text-secondary);\n font-size: 12px;\n font-weight: 400;\n white-space: nowrap;\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n }\n\n & .oc-tooltip-value {\n font-weight: 510;\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n color: var(--oc-tooltip-text);\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Crosshair guideline + per-series snap dots (line/area charts)\n * Animate the snap between adjacent points so the indicator slides instead\n * of jumping. ~50ms ease-in-out keeps the motion crisp without lagging.\n * --------------------------------------------------------------------------- */\n\n .oc-crosshair {\n pointer-events: none;\n transition:\n x1 50ms ease-in-out,\n x2 50ms ease-in-out;\n }\n\n .oc-snap-dots circle {\n pointer-events: none;\n transition:\n cx 50ms ease-in-out,\n cy 50ms ease-in-out;\n }\n}\n","@layer oc.base {\n /* ---------------------------------------------------------------------------\n * Wrapper roots\n * --------------------------------------------------------------------------- */\n\n .oc-chart-root {\n width: 100%;\n height: 100%;\n }\n\n .oc-table-root,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-barlist-root,\n .oc-tilemap-root,\n .oc-map-root {\n width: 100%;\n height: 100%;\n }\n\n .oc-table-root {\n overflow: auto;\n }\n\n /* ---------------------------------------------------------------------------\n * Chart container\n * --------------------------------------------------------------------------- */\n\n .oc-chart {\n font-family: var(--oc-font-family);\n display: block;\n width: 100%;\n }\n\n /* ---------------------------------------------------------------------------\n * BarList SVG container\n * --------------------------------------------------------------------------- */\n\n .oc-tilemap,\n .oc-map {\n display: block;\n width: 100%;\n height: auto;\n }\n\n .oc-map-feature {\n transition: opacity 400ms ease;\n }\n\n .oc-barlist {\n display: block;\n width: 100%;\n }\n\n /* ---------------------------------------------------------------------------\n * Screen reader only utility\n * --------------------------------------------------------------------------- */\n\n .oc-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border-width: 0;\n }\n\n /* ---------------------------------------------------------------------------\n * Editable hover feedback\n * --------------------------------------------------------------------------- */\n\n .oc-editable-hover {\n outline: 1.5px solid var(--oc-editable-hover);\n outline-offset: 2px;\n border-radius: 2px;\n }\n\n /* ---------------------------------------------------------------------------\n * Keyboard focus indicators\n *\n * --oc-focus carries a light value in tokens.css and a dark value in\n * dark.css, so both rings stay visible in either mode.\n * --------------------------------------------------------------------------- */\n\n /* Container ring when the chart itself receives keyboard focus. */\n .oc-root:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 2px;\n }\n\n /* Mark ring during arrow-key navigation (see wireKeyboardNav). Outline on\n * SVG elements draws around the mark's bounding box, same technique as\n * .oc-editable-hover above. */\n .oc-mark-focused {\n outline: 2px solid var(--oc-focus);\n outline-offset: 1px;\n border-radius: 2px;\n }\n}\n","@layer oc.animation {\n /* ---------------------------------------------------------------------------\n * Table entrance animations (.oc-table-wrapper.oc-animate)\n * --------------------------------------------------------------------------- */\n\n .oc-table-wrapper.oc-animate {\n /* Chrome (title/subtitle): fade + slide up */\n & > .oc-chrome {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n }\n\n /* Table header: quick fade before rows start */\n & thead {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n }\n\n /* Row entrance: slide up only, no opacity (cells handle their own fading\n to avoid opacity compounding between row and cell animations) */\n & tbody tr {\n animation: oc-table-enter-row var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-row-index, 0));\n }\n\n /* Cell text: fade in synced with row slide */\n & tbody td {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.5)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-row-index, 0));\n }\n\n /* Heatmap/category cell backgrounds: longer fade, delayed after row appears */\n & td.oc-table-heatmap,\n & td.oc-table-category {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.7)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-row-index, 0) +\n var(--oc-animation-duration) *\n 0.3\n );\n }\n\n /* Bar fill: clip-path grow only (no opacity change to preserve resting\n opacity: 0.15 from table.css) */\n & .oc-table-bar-fill {\n animation: oc-table-enter-bar-fill calc(var(--oc-animation-duration) * 0.8)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-row-index, 0) +\n var(--oc-animation-duration) *\n 0.3\n );\n }\n\n /* Sparkline SVG: clip-path reveal left-to-right. Targets the SVG directly\n (not the wrapper) to avoid clipping absolutely-positioned dots and labels. */\n & .oc-table-sparkline > svg {\n animation: oc-enter-line calc(var(--oc-animation-duration) * 0.8)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-row-index, 0) +\n var(--oc-animation-duration) *\n 0.4\n );\n }\n\n /* Sparkline dots and labels: fade in after the line draws through */\n & .oc-table-sparkline-dot,\n & .oc-table-sparkline-labels {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.3)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-row-index, 0) +\n var(--oc-animation-duration) *\n 0.8\n );\n }\n\n /* Search and pagination: quick fade on mount */\n & .oc-table-search,\n & .oc-table-pagination {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.5)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n }\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Series search (seriesSearch)\n *\n * DOM combobox overlaid on the SVG band the engine reserves below chrome.\n * The wrapper is absolutely positioned by the vanilla adapter (inline\n * left/top/width/height), so everything here is visual only. All colors come\n * from --oc-* tokens, which flip in .oc-dark.\n * --------------------------------------------------------------------------- */\n\n .oc-series-search {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n font-family: var(--oc-font-family);\n z-index: 10;\n }\n\n /* Chips: removable filter tokens for the current selection. They flow to the\n * left of the input and scroll horizontally rather than wrapping over the\n * chart when a reader picks many series. */\n .oc-series-search-chips {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n overflow-x: auto;\n scrollbar-width: none;\n }\n\n .oc-series-search-chips::-webkit-scrollbar {\n display: none;\n }\n\n .oc-series-search-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 4px 3px 10px;\n border: 1px solid var(--oc-border);\n border-radius: 999px;\n background: var(--oc-secondary);\n color: var(--oc-text-secondary);\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n white-space: nowrap;\n }\n\n .oc-series-search-chip-remove {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* 16px glyph, but a >=24px hit area (WCAG 2.5.5 target size) via padding\n pulled back with negative margin so the chip layout stays tight. */\n width: 16px;\n height: 16px;\n padding: 4px;\n margin: -4px -2px -4px -4px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--oc-text-muted);\n cursor: pointer;\n transition:\n color 0.15s,\n background-color 0.15s;\n }\n\n .oc-series-search-chip-remove:hover {\n color: var(--oc-text);\n background: var(--oc-hover-bg);\n }\n\n .oc-series-search-chip-remove:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 1px;\n }\n\n .oc-series-search-chip-remove svg {\n display: block;\n }\n\n /* Combobox: input + popup listbox. Styling matches the table search input\n * (border, radius, focus ring) so the two search surfaces read as one system. */\n .oc-series-search-box {\n position: relative;\n flex: 0 1 220px;\n min-width: 120px;\n }\n\n .oc-series-search-icon {\n position: absolute;\n left: 9px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--oc-text-muted);\n pointer-events: none;\n }\n\n /* Shared skin provided by the grouped rule in table.css.\n Only divergent props live here. */\n .oc-series-search-input {\n height: 32px;\n padding: 0 10px 0 28px;\n }\n\n .oc-series-search-listbox {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n min-width: 100%;\n max-height: 240px;\n overflow-y: auto;\n margin: 0;\n padding: 4px;\n list-style: none;\n border: 1px solid var(--oc-border);\n border-radius: 8px;\n background: var(--oc-card);\n box-shadow: var(--oc-tooltip-shadow);\n z-index: 20;\n }\n\n .oc-series-search-option {\n padding: 6px 10px;\n border-radius: 4px;\n font-size: 13px;\n color: var(--oc-text);\n cursor: pointer;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .oc-series-search-option[aria-selected=\"true\"],\n .oc-series-search-option:hover {\n background: var(--oc-hover-bg);\n }\n\n /* The typed substring within each suggestion. */\n .oc-series-search-match {\n font-weight: 600;\n }\n\n .oc-series-search-empty {\n padding: 6px 10px;\n font-size: 12px;\n color: var(--oc-text-muted);\n }\n}\n","@layer oc.components {\n /**\n * Sparkline display mode.\n *\n * Stamped on the SVG root as data-display=\"sparkline\" by the renderer when\n * the spec sets display: 'sparkline'. Strips any inherited padding/margin\n * so the mark renders truly edge-to-edge in tight container layouts (KPI\n * cards, table cells, dashboard tiles).\n */\n\n .oc-chart[data-display=\"sparkline\"] {\n display: block;\n margin: 0;\n padding: 0;\n }\n}\n","@layer oc.animation {\n /* ---------------------------------------------------------------------------\n * Animation keyframes\n * --------------------------------------------------------------------------- */\n\n /* Bar entrance: clip-path reveal bottom-to-top + fade in (vertical columns).\n Bars grow upward from baseline with a soft opacity fade. */\n @keyframes oc-enter-bar {\n from {\n clip-path: inset(100% 0 0 0);\n opacity: 0;\n }\n 75% {\n opacity: 1;\n }\n to {\n clip-path: inset(0 0 0 0);\n opacity: 1;\n }\n }\n\n /* Bar entrance: clip-path reveal left-to-right + fade in (horizontal bars).\n Bars grow rightward from axis with a soft opacity fade. */\n @keyframes oc-enter-bar-h {\n from {\n clip-path: inset(0 100% 0 0);\n opacity: 0;\n }\n 75% {\n opacity: 1;\n }\n to {\n clip-path: inset(0 0 0 0);\n opacity: 1;\n }\n }\n\n /* Line/area entrance: clip-path reveal left-to-right + fade in.\n Lines draw in following reading direction with a soft lead-in. */\n @keyframes oc-enter-line {\n from {\n clip-path: inset(0 100% 0 0);\n opacity: 0;\n }\n 15% {\n opacity: 1;\n }\n to {\n clip-path: inset(0 0 0 0);\n opacity: 1;\n }\n }\n\n /* Point/arc entrance: scale up + fade in from center */\n @keyframes oc-enter-point {\n from {\n opacity: 0;\n transform: scale(0.3);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n /* Map feature entrance: fade in while transitioning fill from a neutral base\n to the data-driven color stored in --oc-feature-fill. The target opacity is\n --oc-feature-target-opacity (default 1) so features under a focus dim\n animate straight to their dimmed rest opacity instead of hitting 1 and then\n snapping down when the dim is applied post-animation. */\n @keyframes oc-enter-map-fill {\n from {\n fill: var(--oc-secondary);\n opacity: 0;\n }\n to {\n fill: var(--oc-feature-fill);\n opacity: var(--oc-feature-target-opacity, 1);\n }\n }\n\n /* Map point pop-in: gentle scale from 0.75 + fade. Uses transform-origin to\n scale from the circle's own center (cx/cy) rather than the SVG origin. */\n @keyframes oc-enter-map-point {\n from {\n opacity: 0;\n transform: scale(0.75);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n /* Pure opacity fade (no transform). Safe for elements with existing transforms\n like arc groups that use translate() for positioning. */\n @keyframes oc-enter-fade-only {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n /* Fade + subtle slide up for text, rule, tick, annotations */\n @keyframes oc-enter-fade {\n from {\n opacity: 0;\n transform: translateY(4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n /* Table row entrance: slide up only, no opacity (cells fade independently\n to avoid opacity compounding between row and cell animations) */\n @keyframes oc-table-enter-row {\n from {\n transform: translateY(6px);\n }\n to {\n transform: translateY(0);\n }\n }\n\n /* Table bar fill entrance: clip-path grow only, no opacity\n (preserves resting opacity: 0.15 from table.css) */\n @keyframes oc-table-enter-bar-fill {\n from {\n clip-path: inset(0 100% 0 0);\n }\n to {\n clip-path: inset(0 0 0 0);\n }\n }\n\n /* Tooltip entrance */\n @keyframes oc-tooltip-in {\n from {\n opacity: 0;\n transform: translateY(2px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Graph\n * --------------------------------------------------------------------------- */\n\n .oc-graph-wrapper {\n position: relative;\n overflow: hidden;\n background: var(--oc-bg);\n font-family: var(--oc-font-family);\n width: 100%;\n height: 100%;\n }\n\n .oc-graph-canvas {\n display: block;\n width: 100%;\n height: 100%;\n cursor: grab;\n }\n\n .oc-graph-canvas--dragging {\n cursor: grabbing;\n }\n\n .oc-graph-chrome {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n z-index: 2;\n padding: 16px 16px 8px;\n pointer-events: none;\n\n & .oc-title {\n font-size: var(--oc-title-size);\n font-weight: var(--oc-title-weight);\n letter-spacing: var(--oc-title-tracking);\n color: var(--oc-text);\n margin: 0 0 4px;\n --_stroke: color-mix(in srgb, var(--oc-bg) 80%, transparent);\n text-shadow:\n -2px -2px 0 var(--_stroke),\n 2px -2px 0 var(--_stroke),\n -2px 2px 0 var(--_stroke),\n 2px 2px 0 var(--_stroke),\n 0 -2px 0 var(--_stroke),\n 0 2px 0 var(--_stroke),\n -2px 0 0 var(--_stroke),\n 2px 0 0 var(--_stroke);\n }\n\n & .oc-subtitle {\n font-size: var(--oc-subtitle-size);\n color: var(--oc-text-secondary);\n margin: 0;\n --_stroke: color-mix(in srgb, var(--oc-bg) 80%, transparent);\n text-shadow:\n -1px -1px 0 var(--_stroke),\n 1px -1px 0 var(--_stroke),\n -1px 1px 0 var(--_stroke),\n 1px 1px 0 var(--_stroke);\n }\n }\n\n .oc-graph-legend {\n position: absolute;\n top: 8px;\n right: 8px;\n background: var(--oc-bg);\n border: 1px solid var(--oc-border);\n border-radius: var(--oc-border-radius);\n padding: 8px 12px;\n font-size: 12px;\n color: var(--oc-text-secondary);\n max-height: 200px;\n overflow-y: auto;\n }\n\n .oc-graph-legend-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 0;\n /* Button reset for interactive rows (rendered as <button>). */\n width: 100%;\n margin: 0;\n background: none;\n border: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: default;\n }\n\n /* Interactive legend rows are buttons; give them affordance + a focus ring. */\n button.oc-graph-legend-item {\n cursor: pointer;\n border-radius: 4px;\n\n &:hover {\n background: var(--oc-focus-ring);\n }\n\n &:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 1px;\n }\n }\n\n /* De-emphasized (toggled-off) category. */\n .oc-graph-legend-item--inactive {\n opacity: 0.45;\n }\n\n .oc-graph-legend-label {\n flex: 1 1 auto;\n }\n\n .oc-graph-legend-count {\n color: var(--oc-text-secondary);\n font-variant-numeric: tabular-nums;\n margin-left: auto;\n padding-left: 8px;\n }\n\n .oc-graph-legend-swatch {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n }\n\n /* Edge-legend swatches render as a short line, not a dot. */\n .oc-graph-legend-swatch--line {\n width: 12px;\n height: 3px;\n border-radius: 1px;\n }\n\n .oc-graph-search {\n position: absolute;\n top: 8px;\n left: 8px;\n\n & input {\n font-family: var(--oc-font-family);\n font-size: var(--oc-body-size);\n padding: 6px 10px;\n border: 1px solid var(--oc-border);\n border-radius: var(--oc-border-radius);\n background: var(--oc-bg);\n color: var(--oc-text);\n outline: none;\n\n &:focus {\n border-color: var(--oc-focus);\n box-shadow: 0 0 0 2px var(--oc-focus-ring-strong);\n }\n }\n }\n\n /* Dark mode graph overrides (darker bg for canvas-based rendering) */\n .oc-dark .oc-graph-wrapper,\n .oc-graph-wrapper.oc-dark {\n /* GitHub-dark canvas background, intentionally not a token */\n --oc-bg: #0d1117;\n }\n\n /*\n * graph-mount.ts stamps .oc-dark on both .oc-graph-wrapper and\n * .oc-graph-container simultaneously, so the self-variant\n * (.oc-dark.oc-graph-wrapper) and descendant variant\n * (.oc-dark .oc-graph-legend) resolve identically.\n */\n .oc-dark .oc-graph-legend,\n .oc-dark.oc-graph-wrapper .oc-graph-legend,\n .oc-dark .oc-graph-search input {\n background: rgba(13, 17, 23, 0.85);\n border-color: var(--oc-border);\n }\n}\n","@layer oc.animation {\n /* ---------------------------------------------------------------------------\n * Animation scoped rules (.oc-animate enables animation on the chart root)\n * --------------------------------------------------------------------------- */\n\n .oc-animate {\n /* Vertical bars (default): smooth ease-out, no overshoot.\n oc-mark-bar is reserved for future mark type aliases. */\n & .oc-mark-rect rect,\n & .oc-mark-bar rect {\n animation: oc-enter-bar var(--oc-animation-duration) var(--oc-ease-smooth) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Horizontal bars (data-orient is on the mark group, not the SVG root) */\n & .oc-mark-rect[data-orient=\"horizontal\"] rect,\n & .oc-mark-bar[data-orient=\"horizontal\"] rect {\n animation-name: oc-enter-bar-h;\n }\n\n /* Stacked bar/column segments: chain sequentially so each segment starts\n right when the previous one finishes, creating one fluid reveal.\n Uses linear easing so handoffs between segments are seamless (no\n deceleration/acceleration stutter at segment boundaries). */\n & .oc-mark-rect[data-stack-pos] rect {\n animation-duration: var(--oc-stack-segment-duration, 150ms);\n animation-timing-function: linear;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-mark-index, 0) +\n var(--oc-stack-pos, 0) *\n var(--oc-stack-segment-duration, 150ms)\n );\n }\n\n /* Line marks: entire group clips left-to-right (no WAAPI needed) */\n & .oc-mark-line {\n animation: oc-enter-line var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Area marks: entire group clips left-to-right + fades in */\n & .oc-mark-area {\n animation: oc-enter-line var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Arc marks (pie/donut slices): simple fade in.\n Scale transforms break arc positioning because arcs use translate()\n on parent groups. A clean fade is more elegant for pie/donut anyway. */\n & .oc-mark-arc {\n animation: oc-enter-fade-only var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Point marks (scatter, dot).\n Points render as bare <circle> elements with class oc-mark-point directly,\n not wrapped in a group, so we target circle.oc-mark-point (not descendant).\n Duration is 40% of the configured duration (quick pop-in relative to other marks). */\n & circle.oc-mark-point,\n & circle.oc-mark-circle {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Points on line/area charts: delay so they pop in as the line draws through. */\n & .oc-mark-line ~ circle.oc-mark-point,\n & .oc-mark-area ~ circle.oc-mark-point {\n animation-delay: calc(\n var(--oc-animation-duration) *\n 0.35 +\n var(--oc-animation-stagger) *\n var(--oc-mark-index, 0)\n );\n }\n\n /* Text marks: fade + slight slide up */\n & .oc-mark-text text {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Rule marks: fade in */\n & .oc-mark-rule line {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.5)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Tick marks: fade in */\n & .oc-mark-tick line {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.5)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Data labels: fade after their parent mark */\n & .oc-mark-label {\n animation: oc-enter-fade 300ms var(--oc-ease-smooth) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-mark-index, 0) +\n var(--oc-animation-duration) *\n 0.7\n );\n }\n\n /* Annotations: fade in after marks finish */\n & .oc-annotation {\n animation: oc-enter-fade 400ms var(--oc-ease-smooth) both;\n animation-delay: calc(var(--oc-animation-duration) + var(--oc-annotation-delay, 200ms));\n }\n\n /* Tilemap tiles: fade in with jittered stagger for organic feel.\n Per-tile delay is computed in the renderer with pseudo-random variation. */\n & .oc-tilemap-tile {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.35) ease-in both;\n animation-delay: var(--oc-tile-delay, 0ms);\n }\n\n /* Map features: fill color reveal with shuffled stagger.\n Per-feature --oc-map-delay is computed in the renderer via a seeded\n Fisher-Yates shuffle so features pop in organically. */\n & .oc-map-feature {\n animation: oc-enter-map-fill var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: var(--oc-map-delay, 0ms);\n }\n\n /* Bulk mode: high feature counts (counties, ~3k+) get a single group\n fade instead of per-feature fill animations. opacity is GPU-compositable\n so this stays smooth regardless of child count. */\n & .oc-map-features[data-bulk-animate] {\n animation: oc-enter-fade-only var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n }\n\n & .oc-map-features[data-bulk-animate] .oc-map-feature {\n animation: none;\n }\n\n /* Map borders: fade in near the end of the feature sweep */\n & .oc-map-borders path {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4) ease-in both;\n animation-delay: calc(var(--oc-animation-duration) * 0.7);\n }\n\n /* Map points: gentle scale (0.75->1) + fade pop-in after borders.\n transform-origin set per-circle in the renderer so scale happens\n around each circle's own center, not the SVG origin. */\n & .oc-map-point {\n animation: oc-enter-map-point calc(var(--oc-animation-duration) * 0.4)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-duration) *\n 0.8 +\n var(--oc-mark-index, 0) *\n var(--oc-point-stagger, 60ms)\n );\n }\n\n /* Sankey nodes: fade in with stagger by column depth (left-to-right) */\n & .oc-sankey-node rect {\n animation: oc-enter-fade-only var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Sankey links: fade in after nodes, with stagger.\n Links delay by 30% of duration so nodes appear first, then links flow in. */\n & .oc-sankey-link path {\n animation: oc-enter-fade-only var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-duration) *\n 0.3 +\n var(--oc-animation-stagger) *\n var(--oc-mark-index, 0)\n );\n }\n\n /* Barlist rows: fade + slide up per row, bars grow left-to-right */\n & .oc-barlist-row {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: var(--oc-row-delay, 0ms);\n }\n\n & .oc-barlist-bar {\n animation: oc-enter-bar-h var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: var(--oc-row-delay, 0ms);\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Sparkline mode: pair the line/area reveal with a stronger ease-out so it\n * doesn't read as a uniform left-to-right swipe. cubic-bezier(0.16, 1, 0.3, 1)\n * is \"expo-out\" — fast initial draw that decelerates noticeably at the end,\n * giving the trend a hand-drawn feel. Duration is bumped via the engine\n * (compile.ts) so the cleanup timer stays in sync.\n * --------------------------------------------------------------------------- */\n\n .oc-animate[data-display=\"sparkline\"] .oc-mark-line,\n .oc-animate[data-display=\"sparkline\"] .oc-mark-area {\n animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n }\n}\n","/* ---------------------------------------------------------------------------\n * Reduced motion\n *\n * Catch-all: last-declared layer beats every earlier layer regardless of\n * specificity. No !important, no hand-maintained selector mirror.\n *\n * Selector list matches CSS_TOKEN_ROOT_SELECTORS in token-definitions.ts.\n * --------------------------------------------------------------------------- */\n\n@layer oc.reduced-motion {\n @media (prefers-reduced-motion: reduce) {\n :is(\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root\n ),\n :is(\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root\n )\n *,\n :is(\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root\n )\n *::before,\n :is(\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root\n )\n *::after {\n animation: none;\n transition: none;\n }\n }\n}\n"],"names":[]}
1
+ {"version":3,"mappings":"AAWA,iBCEE,62DGFA,4lBJAF,eKNE,mIAeA,6BAQA,qEAUA,yDAOA,uCAIA,qCASA,gJAgBA,qGAcA,4EAQA,yFLrFF,qBENE,6CAIA,2KAQA,wIAOA,+GAMA,+HAQA,wFAOA,oDAYA,8CAIA,wHAQA,4HAQA,2EAMA,6EAMA,4EAWA,8EAgBA,sDAIA,sEAMA,4EAIA,wEAaA,sFAUA,sFASA,gCAMA,mFIhKA,ogBAsBE,4FAOA,iFAOA,gMAWA,iGAMA,0EAKA,mHAQA,yJAUA,oKAUA,uMAkBF,qFAOA,4FIlHA,2EAKA,gCAIA,2CCJA,kJAaA,iHASA,wDAIA,sRAeA,6RAoBA,uFAKA,iGAKA,gDAMA,qEAMA,oIAWA,0DAKA,2RAiBA,yKAWA,0GAMA,wCAIA,mFRtIA,kDAKA,+CAKA,qGASA,wHAUA,yGAWA,qFAKA,0MAYA,oRAeA,6DAOA,mXAoBA,0DAIA,yFAKA,yDIpHA,iGAKE,qFAMA,yDAIA,4DAKA,gGAAA,gGAOA,oJASA,gFAOA,mDAIA,sEAKA,iFAKA,2CAKF,sNAOA,kPAOA,2IAKA,gJAKA,iCASE,0FAAA,0FAaF,oLAYE,mLAAA,kLAaA,yFAKA,qFAKA,4CAAA,2CAOA,+FAKA,6DAMA,4FAKA,+DAaF,iPAaA,wFAAA,8EAAA,yFAAA,+EAMA,+IAOA,+BAGE,wCASF,oJAQE,6OAaE,oEAKA,2GAKA,+FAOJ,4DAIA,+CASA,gCAIA,iHAUA,gDASA,+DAKE,kEAOF,iFAOA,mGAWA,2DAIE,qCAKF,8CAQA,+BASE,qDAAA,qDAMA,qCAUA,6CAIA,kEASF,2EASA,oHMjYA,qIASA,kEAOA,2CAIA,4GASE,8bAkBA,2RAaF,qPAcA,qLAiBA,6DAIE,kEAIA,+FAOF,4CAIA,iCAIA,0HAOA,+EAQA,sEAMA,oDAKE,0OAUE,2GAQJ,qEAYA,iKLrKA,oERCF,oBSJI,4JAMA,4JAOA,8NAOA,yOAOA,wRAAA,yRAcA,4RAaA,uRAYA,4RAAA,+RAaA,uKAAA,2KG/EF,8GAgBA,kHAgBA,iHAeA,4FAgBA,8IAaA,iGAaA,yDAUA,sGAaA,uFAWA,yFAUA,sGEpIE,yLAAA,wLAOA,qFAAA,oFASA,oRAYA,8MAOA,8MASA,kNAUA,qOAAA,sOAQA,+JAAA,+JAWA,8NAOA,8NAOA,8NAOA,iMAWA,sKAOA,wJAQA,gLASA,8JAKA,+EAKA,2KAQA,mQAYA,0NAQA,8PAYA,uLAMA,6KAeF,wJdrMF,yBeDE,uCACE,6LAAA,+LAAA,qMAAA","sources":["../src/styles/index.css","../src/styles/tokens.css","../src/styles/chrome.css","../src/styles/you-draw-it.css","../src/styles/dark.css","../src/styles/base.css","../src/styles/tooltip.css","../src/styles/table.css","../src/styles/sparkline.css","../src/styles/table-animation.css","../src/styles/legend.css","../src/styles/series-search.css","../src/styles/keyframes.css","../src/styles/graph.css","../src/styles/animation.css","../src/styles/reduced-motion.css"],"sourcesContent":["/**\n * @opendata-ai/openchart-core styles\n *\n * CSS with oc- prefix for all class names.\n * CSS custom properties for theme overrides.\n * Dark mode via .oc-dark class on the container.\n * Cascade layers: oc.tokens, oc.base, oc.components, oc.animation, oc.reduced-motion.\n */\n\n/* For optimal typography, load Inter: https://fonts.google.com/specimen/Inter */\n\n@layer oc.tokens, oc.base, oc.components, oc.animation, oc.reduced-motion;\n\n@import \"./tokens.css\";\n@import \"./dark.css\";\n@import \"./base.css\";\n@import \"./chrome.css\";\n@import \"./tooltip.css\";\n@import \"./legend.css\";\n@import \"./series-search.css\";\n@import \"./you-draw-it.css\";\n@import \"./table.css\";\n@import \"./table-animation.css\";\n@import \"./graph.css\";\n@import \"./sparkline.css\";\n@import \"./keyframes.css\";\n@import \"./animation.css\";\n@import \"./reduced-motion.css\";\n","/* GENERATED FILE — do not edit. Source: packages/core/src/styles/token-definitions.ts. Regenerate: bun run generate:tokens */\n\n@layer oc.tokens {\n /* ---------------------------------------------------------------------------\n * Custom properties (light mode defaults)\n *\n * These are FALLBACK defaults. At runtime, the JS theme engine stamps the\n * resolved theme as inline --oc-* custom properties on each .oc-root\n * container (see packages/vanilla/src/theme-tokens.ts). These CSS values\n * serve as fallbacks for contexts where JS hasn't mounted yet (SSR, static\n * HTML examples).\n * --------------------------------------------------------------------------- */\n\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root {\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n\n /* Opt out of iOS text auto-inflation so measured widths match rendered text. */\n -webkit-text-size-adjust: 100%;\n text-size-adjust: 100%;\n\n --oc-font-family:\n \"Inter Variable\", Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n --oc-font-mono: \"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace;\n\n /* Animation easing presets via CSS linear() */\n --oc-ease-smooth: linear(\n 0,\n 0.157,\n 0.438,\n 0.64,\n 0.766,\n 0.85,\n 0.906,\n 0.941,\n 0.964,\n 0.978,\n 0.988,\n 0.994,\n 0.998,\n 1\n );\n --oc-ease-snappy: linear(\n 0,\n 0.012,\n 0.048,\n 0.108,\n 0.194,\n 0.302,\n 0.426,\n 0.559,\n 0.69,\n 0.808,\n 0.905,\n 0.973,\n 1.013,\n 1.028,\n 1.023,\n 1.006,\n 0.984,\n 0.966,\n 0.957,\n 0.957,\n 0.964,\n 0.975,\n 0.986,\n 0.995,\n 1,\n 1.003,\n 1.002,\n 1,\n 0.998,\n 0.998,\n 0.999,\n 1\n );\n\n /* Animation timing defaults */\n --oc-animation-duration: 500ms;\n --oc-animation-stagger: 80ms;\n --oc-annotation-delay: 200ms;\n\n /* Typography scale (editorial design system) */\n --oc-title-size: 26px;\n --oc-title-weight: 590;\n --oc-title-tracking: -0.022em;\n --oc-subtitle-size: 14px;\n --oc-subtitle-weight: 450;\n --oc-source-size: 11px;\n --oc-source-weight: 450;\n --oc-body-size: 13px;\n --oc-eyebrow-size: 11px;\n --oc-eyebrow-weight: 550;\n --oc-eyebrow-tracking: 0.08em;\n\n /* Surfaces (light mode defaults) */\n --oc-bg: #ffffff;\n --oc-card: #ffffff;\n --oc-secondary: #f4f4f5; /* zinc-100, raised surface */\n\n /*\n * Text levels. Names invert across modes: in light mode \"muted\" sits at\n * a lighter zinc step than \"subtle\" because both are picked relative to\n * the active background, not from a fixed lightness ladder. The intent\n * is \"muted = first step away from primary text\"; \"subtle = next step\n * down\"; etc.\n *\n * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,\n * subtle=zinc-400, faint=zinc-300\n * Dark mode (dark.css): inverts the surface tokens but keeps the same\n * muted -> subtle -> faint progression away from primary.\n */\n --oc-text: #09090b;\n --oc-text-secondary: #3f3f46;\n --oc-text-muted: #71717a;\n --oc-text-faint: #d4d4d8;\n\n /* Lines */\n --oc-gridline: rgba(0, 0, 0, 0.1);\n --oc-axis: rgba(0, 0, 0, 0.1);\n --oc-border: rgba(0, 0, 0, 0.08);\n --oc-border-radius: 2px;\n\n /* Brand and semantic */\n --oc-accent: #06b6d4;\n --oc-accent-strong: #0891b2; /* darker cyan for line strokes on light bg */\n --oc-positive: #10b981;\n --oc-negative: #e11d48;\n --oc-focus: #3b82f6;\n /* static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode */\n --oc-focus-ring: rgba(59, 130, 246, 0.1);\n --oc-focus-ring-strong: rgba(59, 130, 246, 0.25);\n --oc-editable-hover: rgba(79, 70, 229, 0.35); /* edit-mode hover outline, indigo one-off */\n\n /* Spacing scale (4px base) — only --oc-space-2/4 are consumed (JS-stamped) */\n --oc-space-2: 8px;\n --oc-space-4: 16px;\n\n /* Interactive states */\n --oc-hover-bg: rgba(0, 0, 0, 0.025);\n --oc-tooltip-bg: rgba(255, 255, 255, 0.88);\n --oc-tooltip-border: rgba(0, 0, 0, 0.08);\n --oc-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12);\n --oc-tooltip-text: #09090b;\n --oc-legend-text: #3f3f46;\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Chrome (eyebrow, title, subtitle, source, byline, footer, brand)\n * --------------------------------------------------------------------------- */\n\n .oc-chrome {\n font-family: var(--oc-font-family);\n }\n\n .oc-eyebrow {\n font-size: var(--oc-eyebrow-size);\n font-weight: var(--oc-eyebrow-weight);\n letter-spacing: var(--oc-eyebrow-tracking);\n text-transform: uppercase;\n fill: var(--oc-accent);\n }\n\n .oc-title {\n font-size: var(--oc-title-size);\n font-weight: var(--oc-title-weight);\n letter-spacing: var(--oc-title-tracking);\n fill: var(--oc-text);\n }\n\n .oc-subtitle {\n font-size: var(--oc-subtitle-size);\n font-weight: var(--oc-subtitle-weight);\n fill: var(--oc-text-muted);\n }\n\n .oc-source,\n .oc-byline,\n .oc-footer {\n font-size: var(--oc-source-size);\n font-weight: var(--oc-source-weight);\n fill: var(--oc-text-muted);\n }\n\n .oc-brand {\n font-size: 11px;\n font-weight: 510;\n letter-spacing: 0.02em;\n fill: var(--oc-text-faint);\n }\n\n .oc-brand-dot {\n fill: var(--oc-accent);\n }\n\n .oc-eyebrow-dot {\n fill: var(--oc-accent);\n }\n\n /* ---------------------------------------------------------------------------\n * Metric bar (.oc-metrics) — optional row of label/value cells above chart\n * --------------------------------------------------------------------------- */\n\n .oc-metrics {\n font-family: var(--oc-font-family);\n }\n\n .oc-metric-label {\n font-size: 10px;\n font-weight: 510;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n fill: var(--oc-text-muted);\n }\n\n .oc-metric-value {\n font-size: 22px;\n font-weight: 510;\n letter-spacing: -0.01em;\n fill: var(--oc-text);\n font-variant-numeric: tabular-nums;\n }\n\n .oc-metric-delta-up {\n fill: var(--oc-positive);\n font-size: 12px;\n font-weight: 510;\n }\n\n .oc-metric-delta-down {\n fill: var(--oc-negative);\n font-size: 12px;\n font-weight: 510;\n }\n\n .oc-metric-secondary {\n fill: var(--oc-positive);\n font-size: 12px;\n font-weight: 400;\n }\n\n /* ---------------------------------------------------------------------------\n * Axis tick labels — inline variant sits above each gridline at the\n * chart-area left edge (no left gutter). Used by line/area y-axes.\n * --------------------------------------------------------------------------- */\n\n .oc-axis-tick-inline {\n font-size: 11px;\n font-weight: 400;\n fill: var(--oc-text-muted);\n }\n\n /* ---------------------------------------------------------------------------\n * Endpoint labels (right-side per-series column for line/area charts)\n *\n * Swatch idiom: a thicker colored stroke segment with an optional thinner\n * lighter strip below. Mirrors the refreshed traditional legend swatch so a\n * single chart never shows two swatch idioms (mock-2 alignment). Per-series\n * color is stamped inline by the renderer; CSS tokens here cover the muted\n * value text and the leader line so theme overrides flow through naturally.\n * --------------------------------------------------------------------------- */\n\n .oc-endpoint-labels {\n font-family: var(--oc-font-family);\n }\n\n .oc-endpoint-label {\n /* Color set inline per series; this fallback only kicks in if the renderer\n ever omits the inline fill. */\n fill: var(--oc-endpoint-label-color, var(--oc-text));\n }\n\n .oc-endpoint-value {\n fill: var(--oc-endpoint-value-color, var(--oc-text-muted));\n }\n\n .oc-endpoint-leader {\n stroke: var(--oc-endpoint-leader-color, currentColor);\n }\n\n /* .oc-endpoint-marker — fill/stroke set inline (open-ring style: bg fill, series stroke). */\n /* .oc-endpoint-swatch — color set inline per series. */\n\n /* ---------------------------------------------------------------------------\n * Annotation dot + subtitle (text-annotation extensions)\n * --------------------------------------------------------------------------- */\n\n /* .oc-annotation-dot — fill/stroke resolved by the engine (open-ring default = bg fill, series stroke). */\n\n .oc-annotation-subtitle {\n fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted));\n }\n\n /* ---------------------------------------------------------------------------\n * Table footer chrome\n * --------------------------------------------------------------------------- */\n\n /* Footer row: source/footer text on the left, brand watermark on the right,\n * sharing a single baseline. gap keeps them apart if the row gets narrow. */\n .oc-table-footer-row {\n display: flex;\n align-items: baseline;\n gap: 16px;\n padding: 16px 16px 4px;\n }\n\n /* Chrome block sits inside the flex row now, so it no longer owns the\n * footer's top padding or horizontal inset (the row provides both). */\n .oc-chrome-footer {\n padding-top: 0;\n }\n\n /* Brand watermark: pushed to the right edge (margin-left:auto holds it there\n * even when the source text is absent), never shrinks below its own text. */\n .oc-table-footer-row .oc-table-ref {\n margin-left: auto;\n flex-shrink: 0;\n text-align: right;\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * You draw it (youDrawIt)\n *\n * NYT-style draw-then-reveal. SVG-space elements (hatched region, guess path,\n * reveal clip) render inside the chart SVG; the prompt + skip-to-reveal button\n * are an absolutely-positioned HTML overlay the vanilla adapter sizes over the\n * drawing region (inline left/top/width/height). All colors come from --oc-*\n * tokens so they flip in .oc-dark.\n * --------------------------------------------------------------------------- */\n\n /* Hatched \"draw here\" region. Fill comes from an SVG <pattern> (line color =\n * the target series color); this only governs overall opacity + fade-out. */\n .oc-ydi-region {\n opacity: 0.35;\n transition: opacity 0.4s ease;\n }\n\n .oc-ydi-region[data-ydi-hidden=\"true\"] {\n opacity: 0;\n }\n\n /* The vertical rule at `from` where drawing begins. */\n .oc-ydi-boundary {\n stroke: var(--oc-text-muted);\n stroke-width: 1;\n stroke-dasharray: 3 3;\n stroke-opacity: 0.6;\n }\n\n /* Reader's guess: a distinct pen-like stroke that reads as \"yours\" against the\n * solid real line. Dashed + focus-blue so it stays legible in both modes. */\n .oc-ydi-guess {\n stroke: var(--oc-focus);\n stroke-width: 2.5;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 1 6;\n }\n\n /* Host-supplied comparison line (\"what everyone else guessed\"). Muted so it\n * sits behind the reader's guess and the real line. */\n .oc-ydi-comparison {\n stroke: var(--oc-text-muted);\n stroke-width: 1.5;\n stroke-opacity: 0.5;\n stroke-dasharray: 4 4;\n }\n\n /* Reveal clip rect: the width animates from the `from` boundary to the full\n * area on reveal, wiping the real line in left-to-right. The vanilla adapter\n * adds .oc-ydi-clip-animate right before setting the final width; reduced\n * motion skips the class so the width snaps. */\n .oc-ydi-clip-rect.oc-ydi-clip-animate {\n transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1);\n }\n\n /* HTML controls overlay: prompt (top) + reveal button (bottom-right). */\n .oc-ydi-controls {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: flex-end;\n pointer-events: none;\n font-family: var(--oc-font-family);\n padding: 8px;\n box-sizing: border-box;\n z-index: 10;\n }\n\n .oc-ydi-prompt {\n align-self: center;\n max-width: 100%;\n padding: 6px 12px;\n border-radius: 6px;\n background: var(--oc-card);\n border: 1px solid var(--oc-border);\n color: var(--oc-text-secondary);\n font-size: 13px;\n font-weight: 500;\n line-height: 16px;\n text-align: center;\n box-shadow: var(--oc-tooltip-shadow);\n }\n\n .oc-ydi-controls.oc-ydi-revealed .oc-ydi-prompt {\n display: none;\n }\n\n /* Skip-to-reveal button. pointer-events re-enabled (the overlay disables it)\n * so it stays clickable and keyboard-reachable. Min height enforced inline by\n * the adapter (>= 24px effective touch target). */\n .oc-ydi-reveal-button {\n pointer-events: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 6px 14px;\n border: 1px solid var(--oc-border);\n border-radius: 6px;\n background: var(--oc-bg);\n color: var(--oc-text);\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n box-shadow: var(--oc-tooltip-shadow);\n transition:\n background-color 0.15s,\n border-color 0.15s;\n }\n\n .oc-ydi-reveal-button:hover {\n background: var(--oc-hover-bg);\n }\n\n .oc-ydi-reveal-button:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 2px;\n }\n\n .oc-ydi-reveal-button:disabled {\n opacity: 0.5;\n cursor: default;\n }\n}\n","/* GENERATED FILE — do not edit. Source: packages/core/src/styles/token-definitions.ts. Regenerate: bun run generate:tokens */\n\n@layer oc.tokens {\n /* ---------------------------------------------------------------------------\n * Dark mode overrides (fallback defaults)\n *\n * At runtime, the JS theme engine stamps dark-adapted --oc-* custom\n * properties on each .oc-root container. These CSS values serve as\n * fallbacks for contexts where JS hasn't mounted yet.\n * --------------------------------------------------------------------------- */\n\n .oc-dark {\n /* Surfaces (zinc-based achromatic ramp) */\n --oc-bg: #09090b;\n --oc-card: #111113;\n --oc-secondary: #27272a;\n\n /* Text levels — see tokens.css for the cross-mode naming rationale */\n --oc-text: #f7f8f8;\n --oc-text-secondary: #d0d6e0;\n --oc-text-muted: #a1a1aa;\n --oc-text-faint: #52525b;\n\n /* Lines */\n --oc-gridline: rgba(255, 255, 255, 0.05);\n --oc-axis: rgba(255, 255, 255, 0.1);\n --oc-border: rgba(255, 255, 255, 0.1);\n\n /* Brand and semantic — accent stays cyan (no darkening on dark bg) */\n --oc-accent: #06b6d4;\n --oc-accent-strong: #06b6d4;\n --oc-positive: #34d399;\n --oc-negative: #fb7185;\n --oc-focus: #60a5fa;\n\n /* Interactive states */\n --oc-hover-bg: rgba(255, 255, 255, 0.05);\n --oc-tooltip-bg: rgba(17, 17, 19, 0.92);\n --oc-tooltip-border: rgba(255, 255, 255, 0.08);\n --oc-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.4);\n --oc-tooltip-text: #f7f8f8;\n --oc-legend-text: #d0d6e0;\n }\n}\n","@layer oc.base {\n /* ---------------------------------------------------------------------------\n * Wrapper roots\n * --------------------------------------------------------------------------- */\n\n .oc-chart-root {\n width: 100%;\n height: 100%;\n }\n\n .oc-table-root,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-barlist-root,\n .oc-tilemap-root,\n .oc-map-root {\n width: 100%;\n height: 100%;\n }\n\n .oc-table-root {\n overflow: auto;\n }\n\n /* ---------------------------------------------------------------------------\n * Chart container\n * --------------------------------------------------------------------------- */\n\n .oc-chart {\n font-family: var(--oc-font-family);\n display: block;\n width: 100%;\n }\n\n /* ---------------------------------------------------------------------------\n * BarList SVG container\n * --------------------------------------------------------------------------- */\n\n .oc-tilemap,\n .oc-map {\n display: block;\n width: 100%;\n height: auto;\n }\n\n .oc-map-feature {\n transition: opacity 400ms ease;\n }\n\n .oc-barlist {\n display: block;\n width: 100%;\n }\n\n /* ---------------------------------------------------------------------------\n * Screen reader only utility\n * --------------------------------------------------------------------------- */\n\n .oc-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border-width: 0;\n }\n\n /* ---------------------------------------------------------------------------\n * Editable hover feedback\n * --------------------------------------------------------------------------- */\n\n .oc-editable-hover {\n outline: 1.5px solid var(--oc-editable-hover);\n outline-offset: 2px;\n border-radius: 2px;\n }\n\n /* ---------------------------------------------------------------------------\n * Keyboard focus indicators\n *\n * --oc-focus carries a light value in tokens.css and a dark value in\n * dark.css, so both rings stay visible in either mode.\n * --------------------------------------------------------------------------- */\n\n /* Container ring when the chart itself receives keyboard focus. */\n .oc-root:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 2px;\n }\n\n /* Mark ring during arrow-key navigation (see wireKeyboardNav). Outline on\n * SVG elements draws around the mark's bounding box, same technique as\n * .oc-editable-hover above. */\n .oc-mark-focused {\n outline: 2px solid var(--oc-focus);\n outline-offset: 1px;\n border-radius: 2px;\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Tooltip\n * Editorial card matching the new design system: zinc surface, cyan accent,\n * Inter Variable, mono numerics. Sized for a slice tooltip that lists every\n * series at the snapped x.\n * --------------------------------------------------------------------------- */\n\n .oc-tooltip {\n position: absolute;\n display: none;\n pointer-events: none;\n z-index: 1000;\n background: var(--oc-tooltip-bg);\n backdrop-filter: blur(14px);\n border: 1px solid var(--oc-tooltip-border);\n border-radius: var(--oc-border-radius, 2px);\n box-shadow: var(--oc-tooltip-shadow);\n color: var(--oc-tooltip-text);\n font-family: var(--oc-font-family);\n font-size: 12px;\n padding: 0;\n max-width: 280px;\n min-width: 160px;\n line-height: 1.4;\n animation: oc-tooltip-in 120ms ease-out;\n transition:\n left 100ms ease-in-out,\n top 100ms ease-in-out;\n\n & .oc-tooltip-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 6px;\n }\n\n & .oc-tooltip-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n }\n\n & .oc-tooltip-title {\n font-weight: 590;\n font-size: 11px;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--oc-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n & .oc-tooltip-body {\n padding: 6px 12px 10px;\n border-top: 1px solid var(--oc-tooltip-border);\n }\n\n /* No separator when body is the only child */\n & .oc-tooltip-body:first-child {\n border-top: none;\n padding-top: 10px;\n }\n\n & .oc-tooltip-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 16px;\n padding: 2px 0;\n }\n\n & .oc-tooltip-row-swatch {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n margin-right: 6px;\n flex-shrink: 0;\n transform: translateY(-1px);\n }\n\n & .oc-tooltip-label {\n color: var(--oc-text-secondary);\n font-size: 12px;\n font-weight: 400;\n white-space: nowrap;\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n }\n\n & .oc-tooltip-value {\n font-weight: 510;\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n color: var(--oc-tooltip-text);\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Crosshair guideline + per-series snap dots (line/area charts)\n * Animate the snap between adjacent points so the indicator slides instead\n * of jumping. ~50ms ease-in-out keeps the motion crisp without lagging.\n * --------------------------------------------------------------------------- */\n\n .oc-crosshair {\n pointer-events: none;\n transition:\n x1 50ms ease-in-out,\n x2 50ms ease-in-out;\n }\n\n .oc-snap-dots circle {\n pointer-events: none;\n transition:\n cx 50ms ease-in-out,\n cy 50ms ease-in-out;\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Table base\n * --------------------------------------------------------------------------- */\n\n .oc-table-wrapper {\n font-family: var(--oc-font-family);\n color: var(--oc-text);\n background: var(--oc-bg);\n\n & > .oc-chrome {\n margin-bottom: 16px;\n padding-left: 16px;\n padding-right: 16px;\n }\n\n & > .oc-chrome:first-child {\n padding-top: 4px;\n }\n\n & table {\n width: 100%;\n border-collapse: collapse;\n }\n\n & th,\n & td {\n padding: 10px 16px;\n text-align: left;\n border-bottom: 1px solid var(--oc-border);\n }\n\n & th {\n font-weight: 600;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--oc-text-secondary);\n white-space: nowrap;\n }\n\n & thead {\n position: sticky;\n top: 0;\n z-index: 2;\n background: var(--oc-bg);\n }\n\n & thead th {\n border-bottom-width: 2px;\n }\n\n & td {\n font-size: 14px;\n font-variant-numeric: tabular-nums;\n }\n\n & th:focus {\n outline: 2px solid var(--oc-focus);\n outline-offset: -2px;\n }\n\n & tbody:focus {\n outline: none;\n }\n }\n\n .oc-table-title {\n margin-bottom: 4px;\n font-size: var(--oc-title-computed-size, var(--oc-title-size));\n font-weight: var(--oc-title-computed-weight, var(--oc-title-weight));\n color: var(--oc-title-computed-color, var(--oc-text));\n }\n\n .oc-table-subtitle {\n margin-bottom: 8px;\n font-size: var(--oc-subtitle-computed-size, var(--oc-subtitle-size));\n font-weight: var(--oc-subtitle-computed-weight, var(--oc-subtitle-weight));\n color: var(--oc-subtitle-computed-color, var(--oc-text-secondary));\n }\n\n .oc-table-source {\n font-size: var(--oc-source-computed-size, var(--oc-source-size));\n color: var(--oc-source-computed-color, var(--oc-text-muted));\n }\n\n .oc-table-footer-text {\n font-size: var(--oc-footer-computed-size, var(--oc-source-size));\n color: var(--oc-footer-computed-color, var(--oc-text-muted));\n }\n\n .oc-table-scroll {\n overflow-x: auto;\n }\n\n /* ---------------------------------------------------------------------------\n * Sticky first column\n * --------------------------------------------------------------------------- */\n\n .oc-table--sticky {\n & th:first-child,\n & td:first-child {\n position: sticky;\n left: 0;\n z-index: 1;\n background: var(--oc-bg);\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Sort controls\n * --------------------------------------------------------------------------- */\n\n .oc-table-sort-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px;\n margin-left: 6px;\n display: inline-flex;\n flex-direction: column;\n align-items: center;\n vertical-align: middle;\n gap: 2px;\n\n &::before,\n &::after {\n content: \"\";\n display: block;\n width: 0;\n height: 0;\n border-left: 5px solid transparent;\n border-right: 5px solid transparent;\n transition:\n opacity 0.15s,\n border-color 0.15s;\n }\n\n &::before {\n border-bottom: 4.5px solid var(--oc-text-secondary);\n opacity: 0.45;\n }\n\n &::after {\n border-top: 4.5px solid var(--oc-text-secondary);\n opacity: 0.45;\n }\n\n &:hover::before,\n &:hover::after {\n opacity: 0.75;\n }\n }\n\n th[aria-sort=\"ascending\"] .oc-table-sort-btn {\n &::before {\n opacity: 1;\n border-bottom-color: var(--oc-text);\n }\n\n &::after {\n opacity: 0.15;\n }\n }\n\n th[aria-sort=\"descending\"] .oc-table-sort-btn {\n &::after {\n opacity: 1;\n border-top-color: var(--oc-text);\n }\n\n &::before {\n opacity: 0.15;\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Search\n * --------------------------------------------------------------------------- */\n\n /* Shared search-input skin (table + series-search).\n Padding excluded: it diverges (table: 8px 12px; series: 0 10px 0 28px)\n and series-search.css is imported before table.css — a padding declaration\n here would beat the series-search override by source order. */\n .oc-table-search input,\n .oc-series-search-input {\n width: 100%;\n border: 1px solid var(--oc-border);\n border-radius: 6px;\n font-size: 13px;\n font-family: inherit;\n background: var(--oc-bg);\n color: var(--oc-text);\n box-sizing: border-box;\n transition: border-color 0.15s;\n }\n\n .oc-table-search input::placeholder,\n .oc-series-search-input::placeholder {\n color: var(--oc-text-muted);\n font-size: 13px;\n }\n\n .oc-table-search input:focus,\n .oc-series-search-input:focus {\n outline: none;\n border-color: var(--oc-focus);\n box-shadow: 0 0 0 3px var(--oc-focus-ring);\n }\n\n .oc-table-search {\n padding: 8px 0;\n\n & input {\n padding: 8px 12px;\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Pagination\n * --------------------------------------------------------------------------- */\n\n .oc-table-pagination {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 0 4px;\n font-size: 13px;\n color: var(--oc-text-secondary);\n\n & button {\n padding: 6px 14px;\n border: 1px solid var(--oc-border);\n border-radius: 6px;\n background: var(--oc-bg);\n color: var(--oc-text);\n cursor: pointer;\n font-size: 13px;\n font-family: inherit;\n transition:\n background 0.15s,\n border-color 0.15s;\n\n &:disabled {\n opacity: 0.35;\n cursor: not-allowed;\n }\n\n &:hover:not(:disabled) {\n background: var(--oc-hover-bg);\n border-color: var(--oc-axis);\n }\n\n &:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 1px;\n }\n }\n }\n\n .oc-table-pagination-info {\n font-variant-numeric: tabular-nums;\n }\n\n .oc-table-pagination-btns {\n display: flex;\n gap: 8px;\n }\n\n /* ---------------------------------------------------------------------------\n * Bar cells\n * --------------------------------------------------------------------------- */\n\n .oc-table-bar {\n position: relative;\n }\n\n .oc-table-bar-fill {\n position: absolute;\n top: 6px;\n left: 0;\n bottom: 6px;\n border-radius: 2px;\n opacity: 0.15;\n pointer-events: none;\n }\n\n .oc-table-bar-value {\n position: relative;\n z-index: 1;\n }\n\n /* ---------------------------------------------------------------------------\n * Sparkline cells\n * --------------------------------------------------------------------------- */\n\n .oc-table-sparkline {\n display: block;\n width: 100%;\n position: relative;\n\n & svg {\n display: block;\n width: 100%;\n overflow: visible;\n }\n }\n\n .oc-table-sparkline-dot {\n position: absolute;\n border-radius: 50%;\n width: 5px;\n height: 5px;\n }\n\n .oc-table-sparkline-labels {\n display: flex;\n justify-content: space-between;\n font-size: 11px;\n line-height: 1;\n }\n\n /* ---------------------------------------------------------------------------\n * Image cells\n * --------------------------------------------------------------------------- */\n\n .oc-table-image {\n display: inline-block;\n vertical-align: middle;\n\n & img {\n object-fit: cover;\n }\n }\n\n .oc-table-image-rounded img {\n border-radius: 50%;\n }\n\n /* ---------------------------------------------------------------------------\n * Flag cells\n * --------------------------------------------------------------------------- */\n\n .oc-table-flag {\n font-size: 1.2em;\n }\n\n /* ---------------------------------------------------------------------------\n * Compact mode\n * --------------------------------------------------------------------------- */\n\n .oc-table--compact {\n & th,\n & td {\n padding: 4px 8px;\n font-size: 13px;\n }\n\n & th {\n font-size: 11px;\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Row hover (when onRowClick is set)\n * --------------------------------------------------------------------------- */\n\n .oc-table--clickable tbody {\n & tr {\n cursor: pointer;\n }\n\n & tr:hover {\n background: var(--oc-hover-bg);\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Keyboard cell focus indicator\n * --------------------------------------------------------------------------- */\n\n .oc-table-cell-focus {\n outline: 2px solid var(--oc-focus);\n outline-offset: -2px;\n }\n\n /* ---------------------------------------------------------------------------\n * Empty state\n * --------------------------------------------------------------------------- */\n\n .oc-table-empty {\n padding: 32px 16px;\n text-align: center;\n color: var(--oc-text-secondary);\n font-size: 14px;\n font-style: italic;\n }\n}\n","@layer oc.components {\n /**\n * Sparkline display mode.\n *\n * Stamped on the SVG root as data-display=\"sparkline\" by the renderer when\n * the spec sets display: 'sparkline'. Strips any inherited padding/margin\n * so the mark renders truly edge-to-edge in tight container layouts (KPI\n * cards, table cells, dashboard tiles).\n */\n\n .oc-chart[data-display=\"sparkline\"] {\n display: block;\n margin: 0;\n padding: 0;\n }\n}\n","@layer oc.animation {\n /* ---------------------------------------------------------------------------\n * Table entrance animations (.oc-table-wrapper.oc-animate)\n * --------------------------------------------------------------------------- */\n\n .oc-table-wrapper.oc-animate {\n /* Chrome (title/subtitle): fade + slide up */\n & > .oc-chrome {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n }\n\n /* Table header: quick fade before rows start */\n & thead {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n }\n\n /* Row entrance: slide up only, no opacity (cells handle their own fading\n to avoid opacity compounding between row and cell animations) */\n & tbody tr {\n animation: oc-table-enter-row var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-row-index, 0));\n }\n\n /* Cell text: fade in synced with row slide */\n & tbody td {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.5)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-row-index, 0));\n }\n\n /* Heatmap/category cell backgrounds: longer fade, delayed after row appears */\n & td.oc-table-heatmap,\n & td.oc-table-category {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.7)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-row-index, 0) +\n var(--oc-animation-duration) *\n 0.3\n );\n }\n\n /* Bar fill: clip-path grow only (no opacity change to preserve resting\n opacity: 0.15 from table.css) */\n & .oc-table-bar-fill {\n animation: oc-table-enter-bar-fill calc(var(--oc-animation-duration) * 0.8)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-row-index, 0) +\n var(--oc-animation-duration) *\n 0.3\n );\n }\n\n /* Sparkline SVG: clip-path reveal left-to-right. Targets the SVG directly\n (not the wrapper) to avoid clipping absolutely-positioned dots and labels. */\n & .oc-table-sparkline > svg {\n animation: oc-enter-line calc(var(--oc-animation-duration) * 0.8)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-row-index, 0) +\n var(--oc-animation-duration) *\n 0.4\n );\n }\n\n /* Sparkline dots and labels: fade in after the line draws through */\n & .oc-table-sparkline-dot,\n & .oc-table-sparkline-labels {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.3)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-row-index, 0) +\n var(--oc-animation-duration) *\n 0.8\n );\n }\n\n /* Search and pagination: quick fade on mount */\n & .oc-table-search,\n & .oc-table-pagination {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.5)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n }\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Legend\n * --------------------------------------------------------------------------- */\n\n .oc-legend {\n font-family: var(--oc-font-family);\n font-size: var(--oc-body-size);\n }\n\n .oc-legend-entry {\n cursor: default;\n }\n\n .oc-legend text {\n fill: var(--oc-legend-text);\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Series search (seriesSearch)\n *\n * DOM combobox overlaid on the SVG band the engine reserves below chrome.\n * The wrapper is absolutely positioned by the vanilla adapter (inline\n * left/top/width/height), so everything here is visual only. All colors come\n * from --oc-* tokens, which flip in .oc-dark.\n * --------------------------------------------------------------------------- */\n\n .oc-series-search {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n font-family: var(--oc-font-family);\n z-index: 10;\n }\n\n /* Chips: removable filter tokens for the current selection. They flow to the\n * left of the input and scroll horizontally rather than wrapping over the\n * chart when a reader picks many series. */\n .oc-series-search-chips {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n overflow-x: auto;\n scrollbar-width: none;\n }\n\n .oc-series-search-chips::-webkit-scrollbar {\n display: none;\n }\n\n .oc-series-search-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 4px 3px 10px;\n border: 1px solid var(--oc-border);\n border-radius: 999px;\n background: var(--oc-secondary);\n color: var(--oc-text-secondary);\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n white-space: nowrap;\n }\n\n .oc-series-search-chip-remove {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* 16px glyph, but a >=24px hit area (WCAG 2.5.5 target size) via padding\n pulled back with negative margin so the chip layout stays tight. */\n width: 16px;\n height: 16px;\n padding: 4px;\n margin: -4px -2px -4px -4px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--oc-text-muted);\n cursor: pointer;\n transition:\n color 0.15s,\n background-color 0.15s;\n }\n\n .oc-series-search-chip-remove:hover {\n color: var(--oc-text);\n background: var(--oc-hover-bg);\n }\n\n .oc-series-search-chip-remove:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 1px;\n }\n\n .oc-series-search-chip-remove svg {\n display: block;\n }\n\n /* Combobox: input + popup listbox. Styling matches the table search input\n * (border, radius, focus ring) so the two search surfaces read as one system. */\n .oc-series-search-box {\n position: relative;\n flex: 0 1 220px;\n min-width: 120px;\n }\n\n .oc-series-search-icon {\n position: absolute;\n left: 9px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--oc-text-muted);\n pointer-events: none;\n }\n\n /* Shared skin provided by the grouped rule in table.css.\n Only divergent props live here. */\n .oc-series-search-input {\n height: 32px;\n padding: 0 10px 0 28px;\n }\n\n .oc-series-search-listbox {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n min-width: 100%;\n max-height: 240px;\n overflow-y: auto;\n margin: 0;\n padding: 4px;\n list-style: none;\n border: 1px solid var(--oc-border);\n border-radius: 8px;\n background: var(--oc-card);\n box-shadow: var(--oc-tooltip-shadow);\n z-index: 20;\n }\n\n .oc-series-search-option {\n padding: 6px 10px;\n border-radius: 4px;\n font-size: 13px;\n color: var(--oc-text);\n cursor: pointer;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .oc-series-search-option[aria-selected=\"true\"],\n .oc-series-search-option:hover {\n background: var(--oc-hover-bg);\n }\n\n /* The typed substring within each suggestion. */\n .oc-series-search-match {\n font-weight: 600;\n }\n\n .oc-series-search-empty {\n padding: 6px 10px;\n font-size: 12px;\n color: var(--oc-text-muted);\n }\n}\n","@layer oc.animation {\n /* ---------------------------------------------------------------------------\n * Animation keyframes\n * --------------------------------------------------------------------------- */\n\n /* Bar entrance: clip-path reveal bottom-to-top + fade in (vertical columns).\n Bars grow upward from baseline with a soft opacity fade. */\n @keyframes oc-enter-bar {\n from {\n clip-path: inset(100% 0 0 0);\n opacity: 0;\n }\n 75% {\n opacity: 1;\n }\n to {\n clip-path: inset(0 0 0 0);\n opacity: 1;\n }\n }\n\n /* Bar entrance: clip-path reveal left-to-right + fade in (horizontal bars).\n Bars grow rightward from axis with a soft opacity fade. */\n @keyframes oc-enter-bar-h {\n from {\n clip-path: inset(0 100% 0 0);\n opacity: 0;\n }\n 75% {\n opacity: 1;\n }\n to {\n clip-path: inset(0 0 0 0);\n opacity: 1;\n }\n }\n\n /* Line/area entrance: clip-path reveal left-to-right + fade in.\n Lines draw in following reading direction with a soft lead-in. */\n @keyframes oc-enter-line {\n from {\n clip-path: inset(0 100% 0 0);\n opacity: 0;\n }\n 15% {\n opacity: 1;\n }\n to {\n clip-path: inset(0 0 0 0);\n opacity: 1;\n }\n }\n\n /* Point/arc entrance: scale up + fade in from center */\n @keyframes oc-enter-point {\n from {\n opacity: 0;\n transform: scale(0.3);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n /* Map feature entrance: fade in while transitioning fill from a neutral base\n to the data-driven color stored in --oc-feature-fill. The target opacity is\n --oc-feature-target-opacity (default 1) so features under a focus dim\n animate straight to their dimmed rest opacity instead of hitting 1 and then\n snapping down when the dim is applied post-animation. */\n @keyframes oc-enter-map-fill {\n from {\n fill: var(--oc-secondary);\n opacity: 0;\n }\n to {\n fill: var(--oc-feature-fill);\n opacity: var(--oc-feature-target-opacity, 1);\n }\n }\n\n /* Map point pop-in: gentle scale from 0.75 + fade. Uses transform-origin to\n scale from the circle's own center (cx/cy) rather than the SVG origin. */\n @keyframes oc-enter-map-point {\n from {\n opacity: 0;\n transform: scale(0.75);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n /* Pure opacity fade (no transform). Safe for elements with existing transforms\n like arc groups that use translate() for positioning. */\n @keyframes oc-enter-fade-only {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n /* Fade + subtle slide up for text, rule, tick, annotations */\n @keyframes oc-enter-fade {\n from {\n opacity: 0;\n transform: translateY(4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n /* Table row entrance: slide up only, no opacity (cells fade independently\n to avoid opacity compounding between row and cell animations) */\n @keyframes oc-table-enter-row {\n from {\n transform: translateY(6px);\n }\n to {\n transform: translateY(0);\n }\n }\n\n /* Table bar fill entrance: clip-path grow only, no opacity\n (preserves resting opacity: 0.15 from table.css) */\n @keyframes oc-table-enter-bar-fill {\n from {\n clip-path: inset(0 100% 0 0);\n }\n to {\n clip-path: inset(0 0 0 0);\n }\n }\n\n /* Tooltip entrance */\n @keyframes oc-tooltip-in {\n from {\n opacity: 0;\n transform: translateY(2px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n}\n","@layer oc.components {\n /* ---------------------------------------------------------------------------\n * Graph\n * --------------------------------------------------------------------------- */\n\n .oc-graph-wrapper {\n position: relative;\n overflow: hidden;\n background: var(--oc-bg);\n font-family: var(--oc-font-family);\n width: 100%;\n height: 100%;\n }\n\n .oc-graph-canvas {\n display: block;\n width: 100%;\n height: 100%;\n cursor: grab;\n }\n\n .oc-graph-canvas--dragging {\n cursor: grabbing;\n }\n\n .oc-graph-chrome {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n z-index: 2;\n padding: 16px 16px 8px;\n pointer-events: none;\n\n & .oc-title {\n font-size: var(--oc-title-size);\n font-weight: var(--oc-title-weight);\n letter-spacing: var(--oc-title-tracking);\n color: var(--oc-text);\n margin: 0 0 4px;\n --_stroke: color-mix(in srgb, var(--oc-bg) 80%, transparent);\n text-shadow:\n -2px -2px 0 var(--_stroke),\n 2px -2px 0 var(--_stroke),\n -2px 2px 0 var(--_stroke),\n 2px 2px 0 var(--_stroke),\n 0 -2px 0 var(--_stroke),\n 0 2px 0 var(--_stroke),\n -2px 0 0 var(--_stroke),\n 2px 0 0 var(--_stroke);\n }\n\n & .oc-subtitle {\n font-size: var(--oc-subtitle-size);\n color: var(--oc-text-secondary);\n margin: 0;\n --_stroke: color-mix(in srgb, var(--oc-bg) 80%, transparent);\n text-shadow:\n -1px -1px 0 var(--_stroke),\n 1px -1px 0 var(--_stroke),\n -1px 1px 0 var(--_stroke),\n 1px 1px 0 var(--_stroke);\n }\n }\n\n .oc-graph-legend {\n position: absolute;\n top: 8px;\n right: 8px;\n background: var(--oc-bg);\n border: 1px solid var(--oc-border);\n border-radius: var(--oc-border-radius);\n padding: 8px 12px;\n font-size: 12px;\n color: var(--oc-text-secondary);\n max-height: 200px;\n overflow-y: auto;\n }\n\n .oc-graph-legend-item {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 2px 0;\n /* Button reset for interactive rows (rendered as <button>). */\n width: 100%;\n margin: 0;\n background: none;\n border: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: default;\n }\n\n /* Interactive legend rows are buttons; give them affordance + a focus ring. */\n button.oc-graph-legend-item {\n cursor: pointer;\n border-radius: 4px;\n\n &:hover {\n background: var(--oc-focus-ring);\n }\n\n &:focus-visible {\n outline: 2px solid var(--oc-focus);\n outline-offset: 1px;\n }\n }\n\n /* De-emphasized (toggled-off) category. */\n .oc-graph-legend-item--inactive {\n opacity: 0.45;\n }\n\n .oc-graph-legend-label {\n flex: 1 1 auto;\n }\n\n .oc-graph-legend-count {\n color: var(--oc-text-secondary);\n font-variant-numeric: tabular-nums;\n margin-left: auto;\n padding-left: 8px;\n }\n\n .oc-graph-legend-swatch {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n }\n\n /* Edge-legend swatches render as a short line, not a dot. */\n .oc-graph-legend-swatch--line {\n width: 12px;\n height: 3px;\n border-radius: 1px;\n }\n\n .oc-graph-search {\n position: absolute;\n top: 8px;\n left: 8px;\n\n & input {\n font-family: var(--oc-font-family);\n font-size: var(--oc-body-size);\n padding: 6px 10px;\n border: 1px solid var(--oc-border);\n border-radius: var(--oc-border-radius);\n background: var(--oc-bg);\n color: var(--oc-text);\n outline: none;\n\n &:focus {\n border-color: var(--oc-focus);\n box-shadow: 0 0 0 2px var(--oc-focus-ring-strong);\n }\n }\n }\n\n /* Dark mode graph overrides (darker bg for canvas-based rendering) */\n .oc-dark .oc-graph-wrapper,\n .oc-graph-wrapper.oc-dark {\n /* GitHub-dark canvas background, intentionally not a token */\n --oc-bg: #0d1117;\n }\n\n /*\n * graph-mount.ts stamps .oc-dark on both .oc-graph-wrapper and\n * .oc-graph-container simultaneously, so the self-variant\n * (.oc-dark.oc-graph-wrapper) and descendant variant\n * (.oc-dark .oc-graph-legend) resolve identically.\n */\n .oc-dark .oc-graph-legend,\n .oc-dark.oc-graph-wrapper .oc-graph-legend,\n .oc-dark .oc-graph-search input {\n background: rgba(13, 17, 23, 0.85);\n border-color: var(--oc-border);\n }\n}\n","@layer oc.animation {\n /* ---------------------------------------------------------------------------\n * Animation scoped rules (.oc-animate enables animation on the chart root)\n * --------------------------------------------------------------------------- */\n\n .oc-animate {\n /* Vertical bars (default): smooth ease-out, no overshoot.\n oc-mark-bar is reserved for future mark type aliases. */\n & .oc-mark-rect rect,\n & .oc-mark-bar rect {\n animation: oc-enter-bar var(--oc-animation-duration) var(--oc-ease-smooth) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Horizontal bars (data-orient is on the mark group, not the SVG root) */\n & .oc-mark-rect[data-orient=\"horizontal\"] rect,\n & .oc-mark-bar[data-orient=\"horizontal\"] rect {\n animation-name: oc-enter-bar-h;\n }\n\n /* Stacked bar/column segments: chain sequentially so each segment starts\n right when the previous one finishes, creating one fluid reveal.\n Uses linear easing so handoffs between segments are seamless (no\n deceleration/acceleration stutter at segment boundaries). */\n & .oc-mark-rect[data-stack-pos] rect {\n animation-duration: var(--oc-stack-segment-duration, 150ms);\n animation-timing-function: linear;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-mark-index, 0) +\n var(--oc-stack-pos, 0) *\n var(--oc-stack-segment-duration, 150ms)\n );\n }\n\n /* Line marks: entire group clips left-to-right (no WAAPI needed) */\n & .oc-mark-line {\n animation: oc-enter-line var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Area marks: entire group clips left-to-right + fades in */\n & .oc-mark-area {\n animation: oc-enter-line var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Arc marks (pie/donut slices): simple fade in.\n Scale transforms break arc positioning because arcs use translate()\n on parent groups. A clean fade is more elegant for pie/donut anyway. */\n & .oc-mark-arc {\n animation: oc-enter-fade-only var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Point marks (scatter, dot).\n Points render as bare <circle> elements with class oc-mark-point directly,\n not wrapped in a group, so we target circle.oc-mark-point (not descendant).\n Duration is 40% of the configured duration (quick pop-in relative to other marks). */\n & circle.oc-mark-point,\n & circle.oc-mark-circle {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Points on line/area charts: delay so they pop in as the line draws through. */\n & .oc-mark-line ~ circle.oc-mark-point,\n & .oc-mark-area ~ circle.oc-mark-point {\n animation-delay: calc(\n var(--oc-animation-duration) *\n 0.35 +\n var(--oc-animation-stagger) *\n var(--oc-mark-index, 0)\n );\n }\n\n /* Text marks: fade + slight slide up */\n & .oc-mark-text text {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Rule marks: fade in */\n & .oc-mark-rule line {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.5)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Tick marks: fade in */\n & .oc-mark-tick line {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.5)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Data labels: fade after their parent mark */\n & .oc-mark-label {\n animation: oc-enter-fade 300ms var(--oc-ease-smooth) both;\n animation-delay: calc(\n var(--oc-animation-stagger) *\n var(--oc-mark-index, 0) +\n var(--oc-animation-duration) *\n 0.7\n );\n }\n\n /* Annotations: fade in after marks finish */\n & .oc-annotation {\n animation: oc-enter-fade 400ms var(--oc-ease-smooth) both;\n animation-delay: calc(var(--oc-animation-duration) + var(--oc-annotation-delay, 200ms));\n }\n\n /* Tilemap tiles: fade in with jittered stagger for organic feel.\n Per-tile delay is computed in the renderer with pseudo-random variation. */\n & .oc-tilemap-tile {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.35) ease-in both;\n animation-delay: var(--oc-tile-delay, 0ms);\n }\n\n /* Map features: fill color reveal with shuffled stagger.\n Per-feature --oc-map-delay is computed in the renderer via a seeded\n Fisher-Yates shuffle so features pop in organically. */\n & .oc-map-feature {\n animation: oc-enter-map-fill var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: var(--oc-map-delay, 0ms);\n }\n\n /* Bulk mode: high feature counts (counties, ~3k+) get a single group\n fade instead of per-feature fill animations. opacity is GPU-compositable\n so this stays smooth regardless of child count. */\n & .oc-map-features[data-bulk-animate] {\n animation: oc-enter-fade-only var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n }\n\n & .oc-map-features[data-bulk-animate] .oc-map-feature {\n animation: none;\n }\n\n /* Map borders: fade in near the end of the feature sweep */\n & .oc-map-borders path {\n animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4) ease-in both;\n animation-delay: calc(var(--oc-animation-duration) * 0.7);\n }\n\n /* Map points: gentle scale (0.75->1) + fade pop-in after borders.\n transform-origin set per-circle in the renderer so scale happens\n around each circle's own center, not the SVG origin. */\n & .oc-map-point {\n animation: oc-enter-map-point calc(var(--oc-animation-duration) * 0.4)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-duration) *\n 0.8 +\n var(--oc-mark-index, 0) *\n var(--oc-point-stagger, 60ms)\n );\n }\n\n /* Sankey nodes: fade in with stagger by column depth (left-to-right) */\n & .oc-sankey-node rect {\n animation: oc-enter-fade-only var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));\n }\n\n /* Sankey links: fade in after nodes, with stagger.\n Links delay by 30% of duration so nodes appear first, then links flow in. */\n & .oc-sankey-link path {\n animation: oc-enter-fade-only var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: calc(\n var(--oc-animation-duration) *\n 0.3 +\n var(--oc-animation-stagger) *\n var(--oc-mark-index, 0)\n );\n }\n\n /* Barlist rows: fade + slide up per row, bars grow left-to-right */\n & .oc-barlist-row {\n animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: var(--oc-row-delay, 0ms);\n }\n\n & .oc-barlist-bar {\n animation: oc-enter-bar-h var(--oc-animation-duration)\n var(--oc-animation-ease, var(--oc-ease-smooth)) both;\n animation-delay: var(--oc-row-delay, 0ms);\n }\n }\n\n /* ---------------------------------------------------------------------------\n * Sparkline mode: pair the line/area reveal with a stronger ease-out so it\n * doesn't read as a uniform left-to-right swipe. cubic-bezier(0.16, 1, 0.3, 1)\n * is \"expo-out\" — fast initial draw that decelerates noticeably at the end,\n * giving the trend a hand-drawn feel. Duration is bumped via the engine\n * (compile.ts) so the cleanup timer stays in sync.\n * --------------------------------------------------------------------------- */\n\n .oc-animate[data-display=\"sparkline\"] .oc-mark-line,\n .oc-animate[data-display=\"sparkline\"] .oc-mark-area {\n animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n }\n}\n","/* ---------------------------------------------------------------------------\n * Reduced motion\n *\n * Catch-all: last-declared layer beats every earlier layer regardless of\n * specificity. No !important, no hand-maintained selector mirror.\n *\n * Selector list matches CSS_TOKEN_ROOT_SELECTORS in token-definitions.ts.\n * --------------------------------------------------------------------------- */\n\n@layer oc.reduced-motion {\n @media (prefers-reduced-motion: reduce) {\n :is(\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root\n ),\n :is(\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root\n )\n *,\n :is(\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root\n )\n *::before,\n :is(\n .oc-root,\n .oc-chart-root,\n .oc-table-wrapper,\n .oc-table-root,\n .oc-graph-wrapper,\n .oc-graph-root,\n .oc-sankey-root,\n .oc-tilemap-root,\n .oc-barlist-root,\n .oc-map-root\n )\n *::after {\n animation: none;\n transition: none;\n }\n }\n}\n"],"names":[]}