claude-artifact-framework 0.3.0 → 0.4.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/README.md CHANGED
@@ -12,7 +12,7 @@ mirrors npm packages automatically.
12
12
  ### As a global script (`<script>` tag)
13
13
 
14
14
  ```html
15
- <script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.3.0/dist/index.global.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.4.0/dist/index.global.js"></script>
16
16
  <script>
17
17
  const { storage, createStore, createPersistedStore, createSharedStore } = ArtifactKit;
18
18
  </script>
@@ -27,11 +27,11 @@ mirrors npm packages automatically.
27
27
  createStore,
28
28
  createPersistedStore,
29
29
  createSharedStore,
30
- } from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.3.0/dist/index.esm.js";
30
+ } from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.4.0/dist/index.esm.js";
31
31
  </script>
32
32
  ```
33
33
 
34
- Pin a version (`@0.3.0`) for reproducible artifacts, or drop the version
34
+ Pin a version (`@0.4.0`) for reproducible artifacts, or drop the version
35
35
  (`claude-artifact-framework/dist/...`) to always get the latest release.
36
36
 
37
37
  ### Inside a React artifact
@@ -123,6 +123,58 @@ persistence path as everything else. `id` is assigned by the framework and
123
123
  reserved. Optional: `title(r)` / `subtitle(r)` for rows, `sort(a, b)`,
124
124
  `summary(records)` for stats above the list, `empty` for the empty-state text.
125
125
 
126
+ ### The `steps` layout — wizards, quizzes, staged calculators
127
+
128
+ Declare the sequence; next/back/progress/result are framework-owned. Step
129
+ fields write into the same flat `data` pool, so the result step is just a
130
+ function of data. Next is gated on the current step's validation; the current
131
+ step survives a reload.
132
+
133
+ ```js
134
+ ArtifactKit.createApp({
135
+ title: "Quiz de riesgo",
136
+ layout: "steps",
137
+ steps: [
138
+ { title: "Tu edad", fields: [{ key: "edad", type: "number", required: true, min: 18 }] },
139
+ { title: "Tu perfil", fields: [{ key: "perfil", type: "select", required: true, options: ["conservador", "moderado", "agresivo"] }] },
140
+ { title: "Resultado", result: (d) => [{ label: "Puntaje", value: score(d), big: true }] },
141
+ ],
142
+ });
143
+ ```
144
+
145
+ Field keys must be unique across all steps (they share one data pool); a
146
+ duplicate throws at creation, as does an unknown step key.
147
+
148
+ ### The `chart` block — bar, line, donut
149
+
150
+ Same vocabulary as `output` (items of `{ label, value }` plus an optional
151
+ `format`), so declaring one teaches nothing new. Colors derive from the theme
152
+ seed by golden-angle rotation — a chart can't clash with the app it lives in.
153
+
154
+ ```js
155
+ blocks: [
156
+ { chart: (d) => ({ type: "bar", format: "money", items: monthlyTotals(d) }), title: "Ventas", wide: true },
157
+ { chart: { type: "donut", items: [{ label: "Comida", value: 45 }, { label: "Otros", value: 55 }] } },
158
+ ]
159
+ ```
160
+
161
+ ### The `timer` block — countdowns that persist their effects
162
+
163
+ The interval is framework-owned and cleaned up on unmount (a leaked
164
+ `setInterval` is a classic half-built artifact bug). The countdown itself is
165
+ deliberately ephemeral; what you declare is what happens when it finishes —
166
+ `onDone` runs through `update()`, so its effects persist like any other change.
167
+
168
+ ```js
169
+ blocks: [
170
+ { timer: { minutes: 25, label: "Focus", onDone: (d) => { d.completados = (d.completados || 0) + 1; } } },
171
+ { output: (d) => [{ label: "Pomodoros", value: d.completados || 0, big: true }] },
172
+ ]
173
+ ```
174
+
175
+ `text` blocks also accept a function of data — `{ text: (d) => "Hola " + d.nombre }` —
176
+ with the same contract as `output`.
177
+
126
178
  ## `storage`
127
179
 
128
180
  Thin wrapper over `window.storage`, the persistence API Claude injects into a
package/dist/index.esm.js CHANGED
@@ -342,6 +342,11 @@ function themeCss(seed) {
342
342
  :root[data-theme="light"] { ${vars(p.light)} }
343
343
  `;
344
344
  }
345
+ function seriesColors(seed, n) {
346
+ const [h0, s0] = rgbToHsl(hexToRgb(seed || "#3b6ef6"));
347
+ const s = clamp(s0, 45, 70);
348
+ return Array.from({ length: n }, (_, i) => hsl((h0 + i * 137.5) % 360, s, 52));
349
+ }
345
350
 
346
351
  // src/blocks.js
347
352
  function formatValue(value, format) {
@@ -476,8 +481,8 @@ function OutputBlock({ spec, ctx }) {
476
481
  )
477
482
  );
478
483
  }
479
- function TextBlock({ spec }) {
480
- const body = typeof spec.text === "function" ? spec.text() : spec.text;
484
+ function TextBlock({ spec, ctx }) {
485
+ const body = typeof spec.text === "function" ? spec.text(ctx.data) : spec.text;
481
486
  return createElement(
482
487
  "div",
483
488
  { className: "caf-block caf-text" },
@@ -509,6 +514,161 @@ function ListBlock({ spec, ctx }) {
509
514
  )
510
515
  );
511
516
  }
517
+ var CHART_TYPES = ["bar", "line", "donut"];
518
+ function ChartBlock({ spec, ctx }) {
519
+ const c = typeof spec.chart === "function" ? spec.chart(ctx.data) : spec.chart;
520
+ const items = c && c.items || [];
521
+ if (!items.length) {
522
+ return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "No data to chart yet.");
523
+ }
524
+ const type = c.type || "bar";
525
+ if (!CHART_TYPES.includes(type)) {
526
+ throw new Error(`unknown chart type "${type}". Valid types: ${CHART_TYPES.join(", ")}.`);
527
+ }
528
+ const colors = ctx.colors(items.length);
529
+ const body = type === "bar" ? barChart(items, c, colors) : type === "line" ? lineChart(items, c) : donutChart(items, c, colors);
530
+ return createElement(
531
+ "div",
532
+ { className: "caf-block caf-chart" },
533
+ spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
534
+ body
535
+ );
536
+ }
537
+ function barChart(items, c, colors) {
538
+ const max = Math.max(...items.map((it) => Number(it.value) || 0), 0) || 1;
539
+ return createElement(
540
+ "div",
541
+ { className: "caf-bars" },
542
+ items.map(
543
+ (it, i) => createElement(
544
+ "div",
545
+ { key: it.label ?? i, className: "caf-bar-row" },
546
+ createElement("span", { className: "caf-bar-label" }, it.label),
547
+ createElement(
548
+ "div",
549
+ { className: "caf-bar-track" },
550
+ createElement("div", {
551
+ className: "caf-bar-fill",
552
+ style: { width: `${Math.max(2, (Number(it.value) || 0) / max * 100)}%`, background: colors[i] }
553
+ })
554
+ ),
555
+ createElement("span", { className: "caf-bar-value" }, formatValue(it.value, c.format))
556
+ )
557
+ )
558
+ );
559
+ }
560
+ function lineChart(items, c) {
561
+ const values = items.map((it) => Number(it.value) || 0);
562
+ const min = Math.min(...values);
563
+ const max = Math.max(...values);
564
+ const span = max - min || 1;
565
+ const W = 100;
566
+ const H = 36;
567
+ const pts = values.map((v, i) => [
568
+ values.length === 1 ? W / 2 : i / (values.length - 1) * W,
569
+ H - 3 - (v - min) / span * (H - 6)
570
+ ]);
571
+ const last = pts[pts.length - 1];
572
+ return createElement(
573
+ "div",
574
+ { className: "caf-line" },
575
+ createElement(
576
+ "svg",
577
+ { viewBox: `0 0 ${W} ${H}`, className: "caf-line-svg", preserveAspectRatio: "none", "aria-hidden": "true" },
578
+ [0.25, 0.5, 0.75].map(
579
+ (f) => createElement("line", { key: f, x1: 0, x2: W, y1: H * f, y2: H * f, className: "caf-line-grid" })
580
+ ),
581
+ createElement("polyline", { points: pts.map((p) => p.join(",")).join(" "), className: "caf-line-path" }),
582
+ createElement("circle", { cx: last[0], cy: last[1], r: 1.6, className: "caf-line-dot" })
583
+ ),
584
+ createElement(
585
+ "div",
586
+ { className: "caf-line-meta" },
587
+ createElement("span", { className: "caf-hint" }, String(items[0].label ?? "")),
588
+ createElement("span", { className: "caf-line-last" }, formatValue(values[values.length - 1], c.format)),
589
+ createElement("span", { className: "caf-hint" }, String(items[items.length - 1].label ?? ""))
590
+ )
591
+ );
592
+ }
593
+ function donutChart(items, c, colors) {
594
+ const total = items.reduce((s, it) => s + (Number(it.value) || 0), 0) || 1;
595
+ const R = 15.9155;
596
+ let offset = 25;
597
+ const segments = items.map((it, i) => {
598
+ const pct = (Number(it.value) || 0) / total * 100;
599
+ const seg = createElement("circle", {
600
+ key: it.label ?? i,
601
+ cx: 21,
602
+ cy: 21,
603
+ r: R,
604
+ className: "caf-donut-seg",
605
+ stroke: colors[i],
606
+ strokeDasharray: `${pct} ${100 - pct}`,
607
+ strokeDashoffset: offset
608
+ });
609
+ offset -= pct;
610
+ return seg;
611
+ });
612
+ return createElement(
613
+ "div",
614
+ { className: "caf-donut" },
615
+ createElement("svg", { viewBox: "0 0 42 42", className: "caf-donut-svg", "aria-hidden": "true" }, segments),
616
+ createElement(
617
+ "div",
618
+ { className: "caf-donut-legend" },
619
+ items.map(
620
+ (it, i) => createElement(
621
+ "div",
622
+ { key: it.label ?? i, className: "caf-legend-row" },
623
+ createElement("span", { className: "caf-legend-swatch", style: { background: colors[i] } }),
624
+ createElement("span", { className: "caf-legend-label" }, it.label),
625
+ createElement("span", { className: "caf-legend-value" }, formatValue(it.value, c.format))
626
+ )
627
+ )
628
+ )
629
+ );
630
+ }
631
+ function TimerBlock({ spec, ctx }) {
632
+ const t = spec.timer || {};
633
+ const total = Math.max(1, Math.round((t.minutes || 0) * 60 + (t.seconds || 0)) || 300);
634
+ const [left, setLeft] = useState(total);
635
+ const [running, setRunning] = useState(false);
636
+ useEffect(() => {
637
+ if (!running) return;
638
+ const id = setInterval(() => setLeft((prev) => Math.max(0, prev - 1)), 1e3);
639
+ return () => clearInterval(id);
640
+ }, [running]);
641
+ useEffect(() => {
642
+ if (left === 0 && running) {
643
+ setRunning(false);
644
+ if (typeof t.onDone === "function") ctx.update((d) => t.onDone(d));
645
+ }
646
+ }, [left, running]);
647
+ const mm = String(Math.floor(left / 60)).padStart(2, "0");
648
+ const ss = String(left % 60).padStart(2, "0");
649
+ const done = left === 0;
650
+ return createElement(
651
+ "div",
652
+ { className: "caf-block caf-timer" },
653
+ spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
654
+ t.label ? createElement("span", { className: "caf-hint" }, t.label) : null,
655
+ createElement("div", { className: done ? "caf-timer-time caf-timer-done" : "caf-timer-time" }, done ? t.doneText || "Done!" : `${mm}:${ss}`),
656
+ createElement("div", { className: "caf-bar-track" }, createElement("div", { className: "caf-bar-fill caf-timer-fill", style: { width: `${left / total * 100}%` } })),
657
+ createElement(
658
+ "div",
659
+ { className: "caf-timer-controls" },
660
+ createElement(
661
+ "button",
662
+ { type: "button", className: "caf-btn caf-btn-primary", disabled: done, onClick: () => setRunning((r) => !r) },
663
+ running ? "Pause" : "Start"
664
+ ),
665
+ createElement("button", { type: "button", className: "caf-btn", onClick: () => {
666
+ setRunning(false);
667
+ setLeft(total);
668
+ } }, "Reset")
669
+ )
670
+ );
671
+ }
512
672
  function CustomBlock({ spec, ctx }) {
513
673
  const rendered = spec.custom(ctx);
514
674
  if (typeof rendered === "string") {
@@ -521,6 +681,8 @@ var BLOCKS = {
521
681
  output: OutputBlock,
522
682
  text: TextBlock,
523
683
  list: ListBlock,
684
+ chart: ChartBlock,
685
+ timer: TimerBlock,
524
686
  custom: CustomBlock
525
687
  };
526
688
  var BLOCK_NAMES = Object.keys(BLOCKS);
@@ -644,8 +806,91 @@ function RecordsLayout({ spec, ctx }) {
644
806
  );
645
807
  }
646
808
 
809
+ // src/steps.js
810
+ function stepErrors(step, data) {
811
+ return (step.fields || []).some((f) => validateField(f, data[f.key]) !== null);
812
+ }
813
+ function ResultScreen({ step, ctx }) {
814
+ const items = step.result(ctx.data) || [];
815
+ return createElement(
816
+ "div",
817
+ { className: "caf-block caf-output" },
818
+ step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
819
+ createElement(
820
+ "div",
821
+ { className: "caf-output-grid" },
822
+ items.map(
823
+ (item, i) => createElement(
824
+ "div",
825
+ { key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
826
+ createElement("span", { className: "caf-stat-label" }, item.label),
827
+ createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
828
+ item.hint ? createElement("small", { className: "caf-hint" }, item.hint) : null
829
+ )
830
+ )
831
+ ),
832
+ createElement(
833
+ "div",
834
+ { className: "caf-step-nav" },
835
+ createElement("button", { type: "button", className: "caf-btn", onClick: () => ctx.setStep(0) }, "Start over")
836
+ )
837
+ );
838
+ }
839
+ function StepsLayout({ spec, ctx }) {
840
+ const steps = spec.steps;
841
+ const i = Math.min(Math.max(ctx.view.step || 0, 0), steps.length - 1);
842
+ const step = steps[i];
843
+ const isResult = typeof step.result === "function";
844
+ const isLast = i === steps.length - 1;
845
+ return createElement(
846
+ "div",
847
+ { className: "caf-panel caf-panel-single" },
848
+ createElement(
849
+ "div",
850
+ { className: "caf-steps-progress" },
851
+ createElement("span", { className: "caf-hint" }, isResult ? "Result" : `Step ${i + 1} of ${steps.length}`),
852
+ createElement(
853
+ "div",
854
+ { className: "caf-bar-track" },
855
+ createElement("div", { className: "caf-bar-fill caf-steps-fill", style: { width: `${(i + 1) / steps.length * 100}%` } })
856
+ )
857
+ ),
858
+ isResult ? createElement(ResultScreen, { step, ctx }) : createElement(
859
+ "div",
860
+ { className: "caf-block caf-fields" },
861
+ step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
862
+ step.text ? createElement("p", { className: "caf-step-text" }, typeof step.text === "function" ? step.text(ctx.data) : step.text) : null,
863
+ (step.fields || []).map(
864
+ (field) => createElement(Field, {
865
+ key: field.key,
866
+ field,
867
+ value: ctx.data[field.key],
868
+ onChange: (v) => ctx.update((d) => {
869
+ d[field.key] = v;
870
+ })
871
+ })
872
+ ),
873
+ createElement(
874
+ "div",
875
+ { className: "caf-step-nav" },
876
+ i > 0 ? createElement("button", { type: "button", className: "caf-btn", onClick: () => ctx.setStep(i - 1) }, "\u2039 Back") : createElement("span"),
877
+ !isLast ? createElement(
878
+ "button",
879
+ {
880
+ type: "button",
881
+ className: "caf-btn caf-btn-primary",
882
+ disabled: stepErrors(step, ctx.data),
883
+ onClick: () => ctx.setStep(i + 1)
884
+ },
885
+ "Next \u203A"
886
+ ) : null
887
+ )
888
+ )
889
+ );
890
+ }
891
+
647
892
  // src/app.js
648
- var LAYOUTS = ["panel", "records"];
893
+ var LAYOUTS = ["panel", "records", "steps"];
649
894
  var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
650
895
  function validate(spec) {
651
896
  const layout = spec.layout || "panel";
@@ -654,6 +899,38 @@ function validate(spec) {
654
899
  `claude-artifact-framework: unknown layout "${layout}". Valid layouts: ${LAYOUTS.join(", ")}.`
655
900
  );
656
901
  }
902
+ if (layout === "steps") {
903
+ const steps = spec.steps;
904
+ if (!Array.isArray(steps) || steps.length === 0) {
905
+ throw new Error('claude-artifact-framework: layout "steps" needs `steps: [...]` with at least one step.');
906
+ }
907
+ const STEP_KEYS = ["title", "text", "fields", "result"];
908
+ const seen = /* @__PURE__ */ new Set();
909
+ steps.forEach((st, i) => {
910
+ if (!st || typeof st !== "object") {
911
+ throw new Error(`claude-artifact-framework: steps[${i}] must be an object.`);
912
+ }
913
+ const unknown = Object.keys(st).filter((k) => !STEP_KEYS.includes(k));
914
+ if (unknown.length) {
915
+ throw new Error(
916
+ `claude-artifact-framework: steps[${i}] has unknown keys (${unknown.join(", ")}). Valid keys: ${STEP_KEYS.join(", ")}.`
917
+ );
918
+ }
919
+ if (!st.fields && !st.text && !st.result) {
920
+ throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
921
+ }
922
+ for (const f of st.fields || []) {
923
+ if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in steps[${i}] needs a \`key\`.`);
924
+ if (seen.has(f.key)) {
925
+ throw new Error(
926
+ `claude-artifact-framework: field key "${f.key}" appears in more than one step \u2014 keys share one data pool and must be unique.`
927
+ );
928
+ }
929
+ seen.add(f.key);
930
+ }
931
+ });
932
+ return layout;
933
+ }
657
934
  if (layout === "records") {
658
935
  const r = spec.records;
659
936
  if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
@@ -700,7 +977,7 @@ function defaultsFrom(spec) {
700
977
  if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
701
978
  data.records = [];
702
979
  }
703
- for (const block of spec.blocks || []) {
980
+ for (const block of [...spec.blocks || [], ...spec.steps || []]) {
704
981
  for (const field of block.fields || []) {
705
982
  if (field && field.key !== void 0 && !(field.key in data)) {
706
983
  data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
@@ -753,7 +1030,7 @@ function Panel({ spec, ctx }) {
753
1030
  )
754
1031
  );
755
1032
  }
756
- var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout };
1033
+ var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout, steps: StepsLayout };
757
1034
  function createApp(spec = {}) {
758
1035
  const layout = validate(spec);
759
1036
  const state = createAppState(defaultsFrom(spec));
@@ -776,7 +1053,9 @@ function createApp(spec = {}) {
776
1053
  view: state.getView(),
777
1054
  update: state.update,
778
1055
  go: state.go,
779
- back: state.back
1056
+ back: state.back,
1057
+ setStep: (n) => state.setView({ step: n }),
1058
+ colors: (n) => seriesColors(spec.theme && spec.theme.seed, n)
780
1059
  };
781
1060
  const Layout = LAYOUT_COMPONENTS[layout];
782
1061
  return createElement(
@@ -864,6 +1143,37 @@ var BASE_CSS = `
864
1143
  .caf-btn:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
865
1144
  .caf-btn-primary { background: var(--caf-accent); border-color: var(--caf-accent); color: var(--caf-accentText); }
866
1145
  .caf-btn-danger { background: transparent; border-color: var(--caf-danger); color: var(--caf-danger); }
1146
+ .caf-bars { display: flex; flex-direction: column; gap: 0.55rem; }
1147
+ .caf-bar-row { display: grid; grid-template-columns: minmax(60px, 30%) 1fr auto; align-items: center; gap: 0.6rem; }
1148
+ .caf-bar-label { font-size: 0.82rem; color: var(--caf-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1149
+ .caf-bar-track { height: 8px; border-radius: 4px; background: var(--caf-sunken); overflow: hidden; }
1150
+ .caf-bar-fill { height: 100%; border-radius: 4px; background: var(--caf-accent); }
1151
+ .caf-bar-value { font-size: 0.82rem; font-weight: 600; font-variant-numeric: tabular-nums; }
1152
+ .caf-line-svg { width: 100%; height: 110px; display: block; }
1153
+ .caf-line-grid { stroke: var(--caf-border); stroke-width: 0.3; }
1154
+ .caf-line-path { fill: none; stroke: var(--caf-accent); stroke-width: 1.1; stroke-linejoin: round; stroke-linecap: round; }
1155
+ .caf-line-dot { fill: var(--caf-accent); }
1156
+ .caf-line-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 0.6rem; }
1157
+ .caf-line-last { font-weight: 650; font-variant-numeric: tabular-nums; color: var(--caf-accent); }
1158
+ .caf-donut { display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
1159
+ .caf-donut-svg { width: 110px; height: 110px; flex: none; transform: rotate(0.001deg); }
1160
+ .caf-donut-seg { fill: none; stroke-width: 6; }
1161
+ .caf-donut-legend { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; flex: 1; }
1162
+ .caf-legend-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.84rem; }
1163
+ .caf-legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
1164
+ .caf-legend-label { color: var(--caf-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1165
+ .caf-legend-value { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
1166
+ .caf-timer { align-items: center; text-align: center; }
1167
+ .caf-timer .caf-bar-track { width: 100%; }
1168
+ .caf-timer-time { font-size: 2.6rem; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
1169
+ .caf-timer-done { color: var(--caf-accent); }
1170
+ .caf-timer-fill { transition: width 0.9s linear; }
1171
+ .caf-timer-controls { display: flex; gap: 0.6rem; }
1172
+ .caf-btn:disabled { opacity: 0.45; cursor: not-allowed; }
1173
+ .caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
1174
+ .caf-steps-fill { transition: width 0.25s ease; }
1175
+ .caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
1176
+ .caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }
867
1177
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
868
1178
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
869
1179
  .caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }