@bitbaum/chatkit 0.9.0 → 0.11.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.
Files changed (55) hide show
  1. package/README.md +63 -7
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/react/AnswerActions.d.ts +39 -0
  7. package/dist/react/AnswerActions.d.ts.map +1 -0
  8. package/dist/react/AnswerActions.js +50 -0
  9. package/dist/react/AnswerActions.js.map +1 -0
  10. package/dist/react/AttachMenu.d.ts +11 -2
  11. package/dist/react/AttachMenu.d.ts.map +1 -1
  12. package/dist/react/AttachMenu.js +21 -24
  13. package/dist/react/AttachMenu.js.map +1 -1
  14. package/dist/react/Markdown.d.ts +20 -2
  15. package/dist/react/Markdown.d.ts.map +1 -1
  16. package/dist/react/Markdown.js +49 -12
  17. package/dist/react/Markdown.js.map +1 -1
  18. package/dist/react/RichBlock.d.ts +6 -0
  19. package/dist/react/RichBlock.d.ts.map +1 -0
  20. package/dist/react/RichBlock.js +28 -0
  21. package/dist/react/RichBlock.js.map +1 -0
  22. package/dist/react/Sheet.d.ts +20 -0
  23. package/dist/react/Sheet.d.ts.map +1 -0
  24. package/dist/react/Sheet.js +37 -0
  25. package/dist/react/Sheet.js.map +1 -0
  26. package/dist/react/Thread.d.ts +19 -2
  27. package/dist/react/Thread.d.ts.map +1 -1
  28. package/dist/react/Thread.js +10 -8
  29. package/dist/react/Thread.js.map +1 -1
  30. package/dist/react/icons.d.ts +3 -0
  31. package/dist/react/icons.d.ts.map +1 -1
  32. package/dist/react/icons.js +3 -0
  33. package/dist/react/icons.js.map +1 -1
  34. package/dist/react/index.d.ts +7 -1
  35. package/dist/react/index.d.ts.map +1 -1
  36. package/dist/react/index.js +5 -1
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/react/use-speech.d.ts +25 -0
  39. package/dist/react/use-speech.d.ts.map +1 -0
  40. package/dist/react/use-speech.js +66 -0
  41. package/dist/react/use-speech.js.map +1 -0
  42. package/dist/replies.d.ts +6 -3
  43. package/dist/replies.d.ts.map +1 -1
  44. package/dist/replies.js +8 -5
  45. package/dist/replies.js.map +1 -1
  46. package/dist/rich.d.ts +43 -0
  47. package/dist/rich.d.ts.map +1 -0
  48. package/dist/rich.js +100 -0
  49. package/dist/rich.js.map +1 -0
  50. package/dist/speech.d.ts +17 -0
  51. package/dist/speech.d.ts.map +1 -0
  52. package/dist/speech.js +82 -0
  53. package/dist/speech.js.map +1 -0
  54. package/package.json +1 -1
  55. package/styles.css +313 -0
package/dist/rich.js ADDED
@@ -0,0 +1,100 @@
1
+ const record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2
+ const str = (v, max = 2000) => typeof v === "string" && !!v.trim() && v.length <= max;
3
+ const list = (v, max) => Array.isArray(v) && v.length > 0 && v.length <= max;
4
+ /** Web images or same-origin assets only; no embedded markup or protocol-relative URLs. */
5
+ export function safeImageSrc(value) {
6
+ if (!str(value, 4096) || /[\s\\]/.test(value) || [...value].some((c) => c.charCodeAt(0) < 32))
7
+ return null;
8
+ if (/^\/(?!\/)/.test(value))
9
+ return value;
10
+ try {
11
+ const url = new URL(value);
12
+ return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password
13
+ ? value
14
+ : null;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ /** Invalid or oversized blocks are rejected without crashing the answer. */
21
+ export function parseRichBlock(raw) {
22
+ if (raw.length > 40000)
23
+ return null;
24
+ let b;
25
+ try {
26
+ b = JSON.parse(raw);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ if (!record(b) || !str(b.title, 200))
32
+ return null;
33
+ switch (b.type) {
34
+ case "callout":
35
+ return str(b.body) ? { type: b.type, title: b.title, body: b.body } : null;
36
+ case "gallery":
37
+ if (!list(b.images, 6) ||
38
+ !b.images.every((i) => record(i) &&
39
+ safeImageSrc(i.src) &&
40
+ str(i.alt, 300) &&
41
+ (i.caption === undefined || str(i.caption, 500))))
42
+ return null;
43
+ return b;
44
+ case "diagram": {
45
+ if (!list(b.nodes, 8) ||
46
+ !b.nodes.every((n) => record(n) &&
47
+ str(n.id, 50) &&
48
+ str(n.label, 120) &&
49
+ (n.detail === undefined || str(n.detail, 300))))
50
+ return null;
51
+ const ids = new Set(b.nodes.map((n) => n.id));
52
+ if (ids.size !== b.nodes.length ||
53
+ !Array.isArray(b.edges) ||
54
+ b.edges.length > 16 ||
55
+ !b.edges.every((e) => record(e) &&
56
+ ids.has(e.from) &&
57
+ ids.has(e.to) &&
58
+ (e.label === undefined || str(e.label, 100))))
59
+ return null;
60
+ return b;
61
+ }
62
+ case "choices":
63
+ if (!str(b.submit, 80) ||
64
+ !list(b.options, 6) ||
65
+ !b.options.every((o) => record(o) && str(o.label, 200) && str(o.reply, 500)))
66
+ return null;
67
+ return b;
68
+ default:
69
+ return null;
70
+ }
71
+ }
72
+ export const RICH_ANSWERS_INSTRUCTION = [
73
+ "## Visual answers",
74
+ "Lead with the answer. Use headings, short paragraphs, markdown tables for comparisons, and numbered steps when they help. Keep simple answers simple.",
75
+ "When useful, insert a fenced chat_ui block containing ONE JSON object at the point it belongs. These are the supported shapes:",
76
+ '{"type":"gallery","title":"Reference photos","images":[{"src":"/provided-image.jpg","alt":"Describe the image","caption":"What to notice"}]}',
77
+ '{"type":"diagram","title":"How it connects","nodes":[{"id":"a","label":"Input","detail":"Optional explanation"},{"id":"b","label":"Result"}],"edges":[{"from":"a","to":"b","label":"leads to"}]}',
78
+ '{"type":"callout","title":"Recommendation","body":"The useful takeaway."}',
79
+ '{"type":"choices","title":"What would you like next?","options":[{"label":"Explain the first option","reply":"Explain the first option"},{"label":"Compare the alternatives","reply":"Compare the alternatives"}],"submit":"Continue"}',
80
+ "Use at most 6 images, 8 diagram nodes, 16 edges, or 6 choices per block. Translate titles, labels and submit into the reader's language. A choice sends its reply as a normal message; it never performs an action directly. Never invent image URLs, numbers, sources, tool results or product capabilities. Use only image URLs provided by the person, context or tools; otherwise use a diagram or plain text. Always explain the takeaway in prose too. No HTML, scripts, raw SVG, or decorative images. Preserve domain-specific evidence and authority rules. Put suggested replies last, outside chat_ui blocks.",
81
+ ].join("\n");
82
+ /** Copy, share and voice keep the content of a visual, never its wire JSON. */
83
+ export function richAnswerText(text) {
84
+ return text.replace(/```chat_ui\s*([\s\S]*?)(?:```|$)/g, (_, raw) => {
85
+ const b = parseRichBlock(raw);
86
+ if (!b)
87
+ return "";
88
+ switch (b.type) {
89
+ case "callout":
90
+ return `${b.title}\n${b.body}`;
91
+ case "gallery":
92
+ return `${b.title}\n${b.images.map((i) => `${i.alt}${i.caption ? ` — ${i.caption}` : ""}\n${i.src}`).join("\n")}`;
93
+ case "choices":
94
+ return `${b.title}\n${b.options.map((o) => `- ${o.label}`).join("\n")}`;
95
+ case "diagram":
96
+ return `${b.title}\n${b.nodes.map((n) => `${n.label}${n.detail ? `: ${n.detail}` : ""}`).join("\n")}\n${b.edges.map((e) => `${b.nodes.find((n) => n.id === e.from).label} → ${b.nodes.find((n) => n.id === e.to).label}${e.label ? ` (${e.label})` : ""}`).join("\n")}`;
97
+ }
98
+ });
99
+ }
100
+ //# sourceMappingURL=rich.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich.js","sourceRoot":"","sources":["../src/rich.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export declare function plainTextForSpeech(markdown: string): string;
2
+ /**
3
+ * Which voice language to ask for. The fleet's operators write mixed German
4
+ * and English; a German sentence is detectable cheaply enough by its most
5
+ * common function words to pick the voice, and the page's language otherwise.
6
+ */
7
+ export declare function guessSpeechLang(text: string, fallback?: string): string;
8
+ /** Who reads the words. The browser's own voice by default (`useSpeech`); an
9
+ * app with a studio voice hands in its own with the same two methods. */
10
+ export type Speaker = {
11
+ /** Resolves when the text has been read, or cut off. Never rejects. */
12
+ speak(text: string): Promise<void>;
13
+ cancel(): void;
14
+ };
15
+ /** The browser's own voice: free, offline, no key. `null` where there is none. */
16
+ export declare function createSynthSpeaker(): Speaker | null;
17
+ //# sourceMappingURL=speech.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.d.ts","sourceRoot":"","sources":["../src/speech.ts"],"names":[],"mappings":"AAWA,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B3D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAO,GAAG,MAAM,CAWrE;AAED;0EAC0E;AAC1E,MAAM,MAAM,OAAO,GAAG;IACpB,uEAAuE;IACvE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,kFAAkF;AAClF,wBAAgB,kBAAkB,IAAI,OAAO,GAAG,IAAI,CAuBnD"}
package/dist/speech.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * An answer as it should be READ ALOUD.
3
+ *
4
+ * Markdown read literally is noise — "hash hash Summary, asterisk asterisk",
5
+ * a citation "F eight" after every claim — so the text is flattened first.
6
+ * From loki's `speech-text`, where the play button under every answer first
7
+ * lived. Pure, so it is tested without a voice.
8
+ */
9
+ import { richAnswerText } from "./rich.js";
10
+ import { extractReplies } from "./replies.js";
11
+ export function plainTextForSpeech(markdown) {
12
+ // Suggested replies are buttons, never words: a quick_replies block is
13
+ // dropped outright rather than announced as "(code)".
14
+ const prose = richAnswerText(extractReplies(markdown).text);
15
+ return (prose
16
+ // Fenced code is not prose; say that it is there rather than reading it.
17
+ .replace(/```[\s\S]*?```/g, " (code) ")
18
+ .replace(/`([^`]*)`/g, "$1")
19
+ // Citations like [F8] or [D1] are for the eye.
20
+ .replace(/\[(?:[A-Z]{1,2}\d{1,3}(?:,\s*[A-Z]{1,2}\d{1,3})*)\]/g, "")
21
+ // Links and images: keep the words, drop the URL.
22
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, "")
23
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
24
+ .replace(/https?:\/\/\S+/g, "")
25
+ // Headings, emphasis, quotes, bullets, tasks, rules, tables.
26
+ .replace(/^\s{0,3}#{1,6}\s+/gm, "")
27
+ .replace(/^\s*>\s?/gm, "")
28
+ .replace(/^\s*[-*+]\s+(?:\[[ xX]\]\s+)?/gm, "")
29
+ .replace(/^\s*\d+[.)]\s+/gm, "")
30
+ .replace(/^\s*[-*_]{3,}\s*$/gm, "")
31
+ .replace(/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/gm, "")
32
+ .replace(/\|/g, " ")
33
+ .replace(/(\*\*|__)(.*?)\1/g, "$2")
34
+ .replace(/(\*|_)(.*?)\1/g, "$2")
35
+ .replace(/~~(.*?)~~/g, "$1")
36
+ .replace(/[ \t]+/g, " ")
37
+ .replace(/\s*\n\s*\n\s*/g, "\n")
38
+ .trim());
39
+ }
40
+ /**
41
+ * Which voice language to ask for. The fleet's operators write mixed German
42
+ * and English; a German sentence is detectable cheaply enough by its most
43
+ * common function words to pick the voice, and the page's language otherwise.
44
+ */
45
+ export function guessSpeechLang(text, fallback = "en") {
46
+ const sample = text.toLowerCase().slice(0, 600);
47
+ const german = (sample.match(/\b(und|nicht|das|ist|ich|mit|für|auf|eine?|der|die|sind|wird|noch|auch)\b/g) ?? []).length;
48
+ const english = (sample.match(/\b(and|not|the|is|with|for|on|a|an|are|will|also|this|that)\b/g) ?? []).length;
49
+ if (german > english && german >= 3)
50
+ return "de";
51
+ if (english >= 3)
52
+ return "en";
53
+ return fallback;
54
+ }
55
+ /** The browser's own voice: free, offline, no key. `null` where there is none. */
56
+ export function createSynthSpeaker() {
57
+ if (typeof window === "undefined" || !("speechSynthesis" in window))
58
+ return null;
59
+ let current = null;
60
+ return {
61
+ speak(text) {
62
+ return new Promise((resolve) => {
63
+ window.speechSynthesis.cancel();
64
+ const u = new SpeechSynthesisUtterance(text);
65
+ u.lang = guessSpeechLang(text, document.documentElement.lang || "en");
66
+ u.onend = () => {
67
+ if (current === u)
68
+ current = null;
69
+ resolve();
70
+ };
71
+ u.onerror = u.onend;
72
+ current = u;
73
+ window.speechSynthesis.speak(u);
74
+ });
75
+ },
76
+ cancel() {
77
+ window.speechSynthesis.cancel();
78
+ current = null;
79
+ },
80
+ };
81
+ }
82
+ //# sourceMappingURL=speech.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.js","sourceRoot":"","sources":["../src/speech.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,uEAAuE;IACvE,sDAAsD;IACtD,MAAM,KAAK,GAAG,cAAc,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5D,OAAO,CACL,KAAK;QACH,yEAAyE;SACxE,OAAO,CAAC,iBAAiB,EAAE,UAAU,CAAC;SACtC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC;QAC5B,+CAA+C;SAC9C,OAAO,CAAC,sDAAsD,EAAE,EAAE,CAAC;QACpE,kDAAkD;SACjD,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC;SACpC,OAAO,CAAC,wBAAwB,EAAE,IAAI,CAAC;SACvC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;QAC/B,6DAA6D;SAC5D,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC;SAClC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;SACzB,OAAO,CAAC,iCAAiC,EAAE,EAAE,CAAC;SAC9C,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC;SAClC,OAAO,CAAC,qDAAqD,EAAE,EAAE,CAAC;SAClE,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,mBAAmB,EAAE,IAAI,CAAC;SAClC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC;SAC3B,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC;SAC/B,IAAI,EAAE,CACV,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAQ,GAAG,IAAI;IAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,CACb,MAAM,CAAC,KAAK,CAAC,4EAA4E,CAAC,IAAI,EAAE,CACjG,CAAC,MAAM,CAAC;IACT,MAAM,OAAO,GAAG,CACd,MAAM,CAAC,KAAK,CAAC,gEAAgE,CAAC,IAAI,EAAE,CACrF,CAAC,MAAM,CAAC;IACT,IAAI,MAAM,GAAG,OAAO,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO,QAAQ,CAAC;AAClB,CAAC;AAUD,kFAAkF;AAClF,MAAM,UAAU,kBAAkB;IAChC,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,CAAC,CAAC,iBAAiB,IAAI,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACjF,IAAI,OAAO,GAAoC,IAAI,CAAC;IACpD,OAAO;QACL,KAAK,CAAC,IAAI;YACR,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;gBACnC,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,CAAC;gBAChC,MAAM,CAAC,GAAG,IAAI,wBAAwB,CAAC,IAAI,CAAC,CAAC;gBAC7C,CAAC,CAAC,IAAI,GAAG,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,eAAe,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;gBACtE,CAAC,CAAC,KAAK,GAAG,GAAG,EAAE;oBACb,IAAI,OAAO,KAAK,CAAC;wBAAE,OAAO,GAAG,IAAI,CAAC;oBAClC,OAAO,EAAE,CAAC;gBACZ,CAAC,CAAC;gBACF,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC;gBACpB,OAAO,GAAG,CAAC,CAAC;gBACZ,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAClC,CAAC,CAAC,CAAC;QACL,CAAC;QACD,MAAM;YACJ,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,CAAC;YAChC,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/chatkit",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "One chat for every product: a composer with a microphone that always works, a thread that does not fight the reader, messages with copy and retry, and LLM markdown \u2014 the fleet's chat standard as a package.",
5
5
  "license": "MIT",
6
6
  "author": "Cato",
package/styles.css CHANGED
@@ -122,6 +122,15 @@
122
122
  }
123
123
  .ck-input::placeholder {
124
124
  color: var(--ck-muted);
125
+ /* One line, always: an empty box is one row high on every phone, and a
126
+ long placeholder is cut at the edge rather than wrapped into a second
127
+ line that the box then clips in half (320px, 2026-10-21). */
128
+ white-space: nowrap;
129
+ overflow: hidden;
130
+ text-overflow: ellipsis;
131
+ }
132
+ .ck-input:placeholder-shown {
133
+ overflow: hidden;
125
134
  }
126
135
  .ck-actions {
127
136
  display: flex;
@@ -1057,6 +1066,13 @@ a.ck-source:hover {
1057
1066
  .ck-reply:hover {
1058
1067
  border-color: var(--ck-accent);
1059
1068
  }
1069
+ /* The first reply is the recommended next step: the same pill, drawn as the
1070
+ lead — a firmer line and the reading colour — so the eye lands on it. */
1071
+ .ck-reply-lead {
1072
+ border-color: var(--ck-line-2);
1073
+ background: var(--ck-raised);
1074
+ font-weight: 500;
1075
+ }
1060
1076
  .ck-reply:disabled {
1061
1077
  opacity: 0.5;
1062
1078
  cursor: default;
@@ -1194,6 +1210,110 @@ a.ck-source:hover {
1194
1210
  cursor: not-allowed;
1195
1211
  opacity: 0.45;
1196
1212
  }
1213
+ /* An app's own rows under Camera · Photos · Files in the attach sheet. */
1214
+ .ck-sheet-extra {
1215
+ display: flex;
1216
+ flex-direction: column;
1217
+ gap: 0.125rem;
1218
+ margin-top: 0.75rem;
1219
+ padding-top: 0.75rem;
1220
+ border-top: 1px solid var(--ck-line);
1221
+ }
1222
+
1223
+ /* while an answer is read aloud: Stop, a quiet wave, the time so far */
1224
+ .ck-player {
1225
+ align-items: center;
1226
+ }
1227
+ .ck-action-stop {
1228
+ color: var(--ck-fg);
1229
+ }
1230
+ .ck-player-wave {
1231
+ display: inline-flex;
1232
+ align-items: center;
1233
+ gap: 2px;
1234
+ height: 1rem;
1235
+ }
1236
+ .ck-player-wave > span {
1237
+ display: block;
1238
+ width: 2px;
1239
+ height: 0.375rem;
1240
+ border-radius: 1px;
1241
+ background: var(--ck-fg-2);
1242
+ animation: ck-wave 1s ease-in-out infinite;
1243
+ }
1244
+ .ck-player-wave > span:nth-child(2n) {
1245
+ animation-delay: 0.15s;
1246
+ }
1247
+ .ck-player-wave > span:nth-child(3n) {
1248
+ animation-delay: 0.3s;
1249
+ }
1250
+ @keyframes ck-wave {
1251
+ 50% {
1252
+ height: 1rem;
1253
+ }
1254
+ }
1255
+ .ck-player-time {
1256
+ font-size: 0.8125rem;
1257
+ font-variant-numeric: tabular-nums;
1258
+ color: var(--ck-fg-2);
1259
+ }
1260
+ @media (prefers-reduced-motion: reduce) {
1261
+ .ck-player-wave > span {
1262
+ animation: none;
1263
+ height: 0.75rem;
1264
+ }
1265
+ }
1266
+
1267
+ /* the sources sheet on a touch screen: one row per record */
1268
+ .ck-sources-open {
1269
+ cursor: pointer;
1270
+ font: inherit;
1271
+ }
1272
+ .ck-sources-open > svg {
1273
+ font-size: 1rem;
1274
+ }
1275
+ .ck-source-list {
1276
+ display: flex;
1277
+ flex-direction: column;
1278
+ margin: 0;
1279
+ padding: 0;
1280
+ list-style: none;
1281
+ }
1282
+ .ck-source-row {
1283
+ display: flex;
1284
+ align-items: flex-start;
1285
+ gap: 0.75rem;
1286
+ min-height: var(--ck-target);
1287
+ padding: 0.625rem 0.25rem;
1288
+ border-top: 1px solid var(--ck-line);
1289
+ color: var(--ck-fg);
1290
+ text-decoration: none;
1291
+ }
1292
+ .ck-source-list > li:first-child .ck-source-row {
1293
+ border-top: 0;
1294
+ }
1295
+ .ck-source-row .ck-source-id {
1296
+ margin-top: 0.125rem;
1297
+ }
1298
+ .ck-source-body {
1299
+ display: flex;
1300
+ min-width: 0;
1301
+ flex-direction: column;
1302
+ gap: 0.125rem;
1303
+ }
1304
+ .ck-source-title {
1305
+ font-weight: 500;
1306
+ overflow-wrap: anywhere;
1307
+ }
1308
+ .ck-source-detail {
1309
+ font-size: 0.8125rem;
1310
+ color: var(--ck-fg-2);
1311
+ overflow-wrap: anywhere;
1312
+ }
1313
+ a.ck-source-row:hover .ck-source-title {
1314
+ text-decoration: underline;
1315
+ }
1316
+
1197
1317
  @keyframes ck-rise {
1198
1318
  from {
1199
1319
  transform: translateY(1.5rem);
@@ -1205,3 +1325,196 @@ a.ck-source:hover {
1205
1325
  animation: none;
1206
1326
  }
1207
1327
  }
1328
+
1329
+ /* Visual answers share the app's palette, with a quiet editorial hierarchy. */
1330
+ .ck-rich {
1331
+ box-sizing: border-box;
1332
+ min-width: 0;
1333
+ margin: 1.5rem 0;
1334
+ color: var(--ck-fg);
1335
+ overflow-wrap: anywhere;
1336
+ }
1337
+ .ck-rich * {
1338
+ box-sizing: border-box;
1339
+ min-width: 0;
1340
+ }
1341
+ .ck-rich-title {
1342
+ display: block;
1343
+ font-size: 1.05em;
1344
+ font-weight: 650;
1345
+ line-height: 1.4;
1346
+ margin-bottom: 1rem;
1347
+ }
1348
+ .ck-gallery-grid {
1349
+ display: grid;
1350
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1351
+ gap: 0.75rem;
1352
+ }
1353
+ .ck-gallery-grid figure {
1354
+ margin: 0;
1355
+ }
1356
+ .ck-photo {
1357
+ display: flex;
1358
+ flex-direction: column;
1359
+ width: 100%;
1360
+ min-height: var(--ck-target);
1361
+ padding: 0;
1362
+ overflow: hidden;
1363
+ border: 1px solid var(--ck-line);
1364
+ border-radius: var(--ck-radius);
1365
+ background: var(--ck-raised);
1366
+ color: var(--ck-fg);
1367
+ cursor: zoom-in;
1368
+ font: inherit;
1369
+ text-align: start;
1370
+ }
1371
+ .ck-photo img {
1372
+ display: block;
1373
+ width: 100%;
1374
+ aspect-ratio: 1;
1375
+ object-fit: cover;
1376
+ }
1377
+ .ck-photo-label {
1378
+ padding: 0.5rem 0.75rem;
1379
+ font-size: 0.875em;
1380
+ }
1381
+ .ck-gallery-grid figcaption {
1382
+ padding: 0.5rem 0;
1383
+ font-size: 0.875em;
1384
+ color: var(--ck-fg-2);
1385
+ }
1386
+ .ck-photo-full {
1387
+ display: block;
1388
+ max-width: 100%;
1389
+ max-height: 70dvh;
1390
+ object-fit: contain;
1391
+ margin: auto;
1392
+ }
1393
+ .ck-callout {
1394
+ padding: 1.25rem;
1395
+ border-inline-start: 3px solid var(--ck-fg);
1396
+ background: var(--ck-raised);
1397
+ border-radius: 0 var(--ck-radius-sm) var(--ck-radius-sm) 0;
1398
+ }
1399
+ .ck-callout .ck-rich-title {
1400
+ margin-bottom: 0.5rem;
1401
+ }
1402
+ .ck-rich p {
1403
+ margin: 0.25rem 0 0;
1404
+ }
1405
+ .ck-diagram {
1406
+ padding: 1.25rem;
1407
+ border: 1px solid var(--ck-line);
1408
+ border-radius: var(--ck-radius);
1409
+ background: var(--ck-raised);
1410
+ }
1411
+ .ck-md .ck-diagram-nodes {
1412
+ display: grid;
1413
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
1414
+ gap: 0.75rem;
1415
+ padding: 0;
1416
+ list-style: none;
1417
+ margin: 0;
1418
+ }
1419
+ .ck-diagram-nodes li {
1420
+ display: flex;
1421
+ align-items: start;
1422
+ gap: 0.75rem;
1423
+ padding: 1rem;
1424
+ background: var(--ck-bg);
1425
+ border: 1px solid var(--ck-line);
1426
+ border-radius: var(--ck-radius-sm);
1427
+ }
1428
+ .ck-diagram-number {
1429
+ display: grid;
1430
+ place-items: center;
1431
+ flex: 0 0 1.75rem;
1432
+ height: 1.75rem;
1433
+ border-radius: 50%;
1434
+ background: var(--ck-fg);
1435
+ color: var(--ck-bg);
1436
+ font-size: 0.875em;
1437
+ }
1438
+ .ck-diagram-nodes p {
1439
+ font-size: 0.875em;
1440
+ color: var(--ck-fg-2);
1441
+ }
1442
+ .ck-md .ck-diagram-edges {
1443
+ padding: 0.75rem 0 0;
1444
+ margin: 0;
1445
+ list-style: none;
1446
+ }
1447
+ .ck-diagram-edges li {
1448
+ display: flex;
1449
+ align-items: center;
1450
+ gap: 0.5rem;
1451
+ flex-wrap: wrap;
1452
+ border-top: 1px solid var(--ck-line);
1453
+ padding: 0.75rem 0;
1454
+ }
1455
+ .ck-diagram-arrow {
1456
+ color: var(--ck-fg-3);
1457
+ }
1458
+ .ck-diagram-edges small {
1459
+ flex-basis: 100%;
1460
+ color: var(--ck-fg-2);
1461
+ }
1462
+ .ck-choice fieldset {
1463
+ border: 0;
1464
+ margin: 0;
1465
+ padding: 0;
1466
+ }
1467
+ .ck-choice legend {
1468
+ padding: 0;
1469
+ }
1470
+ .ck-choice-option {
1471
+ display: flex;
1472
+ align-items: center;
1473
+ gap: 0.75rem;
1474
+ min-height: var(--ck-target);
1475
+ padding: 0.75rem;
1476
+ border: 1px solid var(--ck-line);
1477
+ border-radius: var(--ck-radius-sm);
1478
+ margin-bottom: 0.5rem;
1479
+ cursor: pointer;
1480
+ }
1481
+ .ck-choice-option:has(:checked) {
1482
+ border-color: var(--ck-fg);
1483
+ background: var(--ck-raised);
1484
+ }
1485
+ .ck-choice-option input {
1486
+ flex: 0 0 auto;
1487
+ width: 1.25rem;
1488
+ height: 1.25rem;
1489
+ accent-color: var(--ck-fg);
1490
+ }
1491
+ .ck-choice-submit {
1492
+ display: flex;
1493
+ justify-content: center;
1494
+ align-items: center;
1495
+ gap: 1rem;
1496
+ width: 100%;
1497
+ min-height: var(--ck-target);
1498
+ border: 0;
1499
+ border-radius: var(--ck-radius);
1500
+ background: var(--ck-fg);
1501
+ color: var(--ck-bg);
1502
+ padding: 0.75rem 1rem;
1503
+ font: inherit;
1504
+ font-weight: 600;
1505
+ cursor: pointer;
1506
+ }
1507
+ .ck-choice-submit:disabled {
1508
+ opacity: 0.45;
1509
+ cursor: default;
1510
+ }
1511
+ .ck-rich button:focus-visible,
1512
+ .ck-rich input:focus-visible {
1513
+ outline: 2px solid var(--ck-fg);
1514
+ outline-offset: 3px;
1515
+ }
1516
+ @media (max-width: 480px) {
1517
+ .ck-gallery-grid {
1518
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1519
+ }
1520
+ }