@panaversity/ksor 0.0.29 → 0.0.30

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/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @panaversity/ksor
2
2
 
3
+ ## 0.0.30
4
+
5
+ ### Patch Changes
6
+
7
+ - fbf149b: Quizzes, as governed attachments of a document.
8
+
9
+ A document in `knowledge/` may now carry `<doc>.quiz.yaml` beside its summary
10
+ and its flashcard deck. It renders at the end of the document's page, under the
11
+ deck: choose an option, see immediately whether you were right, and read the
12
+ explanation before moving on. There is no pass mark — a quiz here checks
13
+ understanding of the record, it does not certify anybody — and answers stay in
14
+ the reader's own browser.
15
+
16
+ A quiz is **part of its document, not a document**: no URL, no sidebar row, no
17
+ `llms.txt` line, no markdown twin, no search entry, and no stable id. That last
18
+ one settles a question worth being explicit about: because `ksor ingest` creates
19
+ no node for a quiz, **the answer key cannot reach the MCP surface at all**.
20
+ There is nothing for an agent to search and nothing for it to read — not by a
21
+ filter that could be forgotten, but because the row does not exist. Governance
22
+ inherits from the parent exactly as the summary and the deck already do.
23
+
24
+ **`pnpm check` and `pnpm build` refuse a quiz a reader could pass without
25
+ reading it**, naming the questions to fix:
26
+
27
+ - `ksor-quiz-answer-bias` — more than 60% of answers at one option position
28
+ - `ksor-quiz-length-bias` — picking the longest or shortest option usually wins
29
+ - `ksor-quiz-answer-run` — four or more questions in a row share an answer
30
+ - `ksor-quiz-contradiction` — an explanation calls the marked answer wrong
31
+ - `ksor-quiz-duplicate-stem` — two questions open with the same 60 characters
32
+
33
+ These are carried from the predecessor, where the same mistakes shipped and were
34
+ found by readers rather than by the project — one quiz put every correct answer
35
+ in the same position across 451 questions. There they lived in a script that was
36
+ run once; here they are part of loading the file, so a quiz that fails them
37
+ cannot be published. The ratio rules do not apply below five questions, where
38
+ enforcing a spread would mean choosing an author's answers for them.
39
+
40
+ `ksor init` ships a quiz on the seed document, so a first `pnpm dev` shows the
41
+ shape. Its own first draft was refused for putting four of five answers at
42
+ option B — the check catching exactly what it was carried for.
43
+
3
44
  ## 0.0.29
4
45
 
5
46
  ### Patch Changes
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as resolveCommand, r as verbs, t as exitCodes } from "./src-pl4aOpVs.mjs";
3
- import { A as runProbe$1, C as keyRingFromEnv, D as prewarmPool, E as pooledEndpointFor, F as withPgRetry$1, I as withProbeDeadline$1, L as z$1, M as storedTextSearchConfig, N as tlsPosture, O as readHandler, P as visibleTiers$1, S as embedQueryVlit, T as parseInstanceText$1, _ as buildShippedProvider$1, a as MAX_OUTLINE_LIMIT, b as contentPool$1, c as MissingProviderKeyError$1, d as READ_OUTPUT, f as SEARCH_OUTPUT, g as assertSchemaCompatible, h as assertGovernanceServable$1, i as GovernanceGateError$1, j as searchHandler, k as recordIsUndescribed, l as OUTLINE_OUTPUT, m as TextSearchConfigMismatch, n as EmbeddingSpaceMismatch$1, p as SchemaVersionError, r as FLOOR, s as McpServer$1, t as ContentStoreError$1, u as READ_ONLY, v as checkEmbeddingSpace$1, w as outlineHandler, x as contentPoolMin, y as composeInstructions } from "./gateway-api-8lNruq9e-CuohjtoK.mjs";
3
+ import { A as runProbe$1, C as keyRingFromEnv, D as prewarmPool, E as pooledEndpointFor, F as withPgRetry$1, I as withProbeDeadline$1, L as z$1, M as storedTextSearchConfig, N as tlsPosture, O as readHandler, P as visibleTiers$1, S as embedQueryVlit, T as parseInstanceText$1, _ as buildShippedProvider$1, a as MAX_OUTLINE_LIMIT, b as contentPool$1, c as MissingProviderKeyError$1, d as READ_OUTPUT, f as SEARCH_OUTPUT, g as assertSchemaCompatible, h as assertGovernanceServable$1, i as GovernanceGateError$1, j as searchHandler, k as recordIsUndescribed, l as OUTLINE_OUTPUT, m as TextSearchConfigMismatch, n as EmbeddingSpaceMismatch$1, p as SchemaVersionError, r as FLOOR, s as McpServer$1, t as ContentStoreError$1, u as READ_ONLY, v as checkEmbeddingSpace$1, w as outlineHandler, x as contentPoolMin, y as composeInstructions } from "./gateway-api-CbFkHZiU-HvlJRjRB.mjs";
4
4
  import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { InMemoryTransport, LATEST_PROTOCOL_VERSION, createMcpHandler } from "@modelcontextprotocol/server";
@@ -16,7 +16,7 @@ import { bodyLimit } from "hono/body-limit";
16
16
  import { execFileSync, spawnSync } from "node:child_process";
17
17
  import { parseArgs } from "node:util";
18
18
  import { readFile, readdir, stat } from "node:fs/promises";
19
- //#region ../content-gateway/dist/main-C-OMrGGt.mjs
19
+ //#region ../content-gateway/dist/main-CS5a_8OW.mjs
20
20
  /**
21
21
  * The default registration — and the ORIGINAL of the file `ksor init` emits.
22
22
  *
@@ -1583,7 +1583,7 @@ async function withPgRetry(op, options = {}) {
1583
1583
  throw lastError;
1584
1584
  }
1585
1585
  //#endregion
1586
- //#region ../content/dist/commands-DU1nqwZa.mjs
1586
+ //#region ../content/dist/commands-CLUS9CX8.mjs
1587
1587
  /**
1588
1588
  * EVAL-LOCKED constants, quarried verbatim from the oracle
1589
1589
  * (sor-agentfactory @ b554f91, config.py) — changing any of these is a
@@ -4208,6 +4208,10 @@ const ATTACHMENT_SUFFIXES = [
4208
4208
  {
4209
4209
  suffix: ".flashcards.yaml",
4210
4210
  kind: "deck"
4211
+ },
4212
+ {
4213
+ suffix: ".quiz.yaml",
4214
+ kind: "quiz"
4211
4215
  }
4212
4216
  ];
4213
4217
  /**
@@ -5,7 +5,7 @@ import { z, z as z$1 } from "zod";
5
5
  import pg from "pg";
6
6
  import path from "node:path";
7
7
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
8
- //#region ../content-gateway/dist/gateway-api-8lNruq9e.mjs
8
+ //#region ../content-gateway/dist/gateway-api-CbFkHZiU.mjs
9
9
  /**
10
10
  * The record's system prompt: instance.md's body, preserved beneath a framework
11
11
  * floor. Lifted out of server.ts so the public gateway surface can re-export
package/dist/gateway.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { L as z$1, O as readHandler, a as MAX_OUTLINE_LIMIT, d as READ_OUTPUT, f as SEARCH_OUTPUT, j as searchHandler, k as recordIsUndescribed, l as OUTLINE_OUTPUT, o as MAX_SEARCH_K, r as FLOOR, s as McpServer$1, u as READ_ONLY, w as outlineHandler, y as composeInstructions } from "./gateway-api-8lNruq9e-CuohjtoK.mjs";
1
+ import { L as z$1, O as readHandler, a as MAX_OUTLINE_LIMIT, d as READ_OUTPUT, f as SEARCH_OUTPUT, j as searchHandler, k as recordIsUndescribed, l as OUTLINE_OUTPUT, o as MAX_SEARCH_K, r as FLOOR, s as McpServer$1, u as READ_ONLY, w as outlineHandler, y as composeInstructions } from "./gateway-api-CbFkHZiU-HvlJRjRB.mjs";
2
2
  export { FLOOR, MAX_OUTLINE_LIMIT, MAX_SEARCH_K, McpServer$1 as McpServer, OUTLINE_OUTPUT, READ_ONLY, READ_OUTPUT, SEARCH_OUTPUT, composeInstructions, outlineHandler, readHandler, recordIsUndescribed, searchHandler, z$1 as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "Knowledge System of Record — compile governed markdown into a static site for people and an MCP server for AI agents, with citations and measured abstention.",
5
5
  "keywords": [
6
6
  "abstention",
@@ -41,7 +41,7 @@ const ASSET_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".web
41
41
  // rules below. This mirrors packages/content/src/lib/attachment-rule.ts, which
42
42
  // this dependency-free file cannot import; ATTACHMENT_CASES there is the table
43
43
  // both are held to.
44
- const ATTACHMENT_SUFFIXES = [".summary.md", ".summary.mdx", ".flashcards.yaml"];
44
+ const ATTACHMENT_SUFFIXES = [".summary.md", ".summary.mdx", ".flashcards.yaml", ".quiz.yaml"];
45
45
  // One character off a real attachment, refused BY NAME: `.yml` reaches the
46
46
  // site bundler's `Unknown file type` throw, which names the path and nothing
47
47
  // about the rule.
@@ -49,6 +49,8 @@ const ATTACHMENT_NEAR_MISSES = [
49
49
  [".flashcards.yml", ".flashcards.yaml"],
50
50
  [".flashcards.json", ".flashcards.yaml"],
51
51
  [".summary.markdown", ".summary.md"],
52
+ [".quiz.yml", ".quiz.yaml"],
53
+ [".quiz.json", ".quiz.yaml"],
52
54
  ];
53
55
 
54
56
  /** The attachment suffix this name carries, or null. A dotfile has no stem. */
@@ -41,7 +41,7 @@ const ASSET_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".web
41
41
  // rules below. This mirrors packages/content/src/lib/attachment-rule.ts, which
42
42
  // this dependency-free file cannot import; ATTACHMENT_CASES there is the table
43
43
  // both are held to.
44
- const ATTACHMENT_SUFFIXES = [".summary.md", ".summary.mdx", ".flashcards.yaml"];
44
+ const ATTACHMENT_SUFFIXES = [".summary.md", ".summary.mdx", ".flashcards.yaml", ".quiz.yaml"];
45
45
  // One character off a real attachment, refused BY NAME: `.yml` reaches the
46
46
  // site bundler's `Unknown file type` throw, which names the path and nothing
47
47
  // about the rule.
@@ -49,6 +49,8 @@ const ATTACHMENT_NEAR_MISSES = [
49
49
  [".flashcards.yml", ".flashcards.yaml"],
50
50
  [".flashcards.json", ".flashcards.yaml"],
51
51
  [".summary.markdown", ".summary.md"],
52
+ [".quiz.yml", ".quiz.yaml"],
53
+ [".quiz.json", ".quiz.yaml"],
52
54
  ];
53
55
 
54
56
  /** The attachment suffix this name carries, or null. A dotfile has no stem. */
@@ -506,11 +506,12 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
506
506
  refused.
507
507
  - Images and assets live in `knowledge/` beside the document that uses them,
508
508
  referenced by relative links. A relative link must never leave `knowledge/`.
509
- - **Study attachments.** A document may carry two optional companions named
510
- after it, in the same folder: `<doc>.summary.md` (a short précis) and
511
- `<doc>.flashcards.yaml` (a recall deck). The summary appears as a second tab
512
- beside the document's own words; the deck appears at the END of that
513
- document's page. Both appear nowhere else in the site.
509
+ - **Study attachments.** A document may carry three optional companions named
510
+ after it, in the same folder: `<doc>.summary.md` (a short précis),
511
+ `<doc>.flashcards.yaml` (a recall deck) and `<doc>.quiz.yaml` (a
512
+ multiple-choice check). The summary appears as a second tab beside the
513
+ document's own words; the deck and the quiz appear at the END of that
514
+ document's page. None of them appears anywhere else in the site.
514
515
 
515
516
  An attachment is **part of its document**, not a document. It has no URL of
516
517
  its own, no sidebar row, no line in `llms.txt`, and no identity an agent can
@@ -549,6 +550,51 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
549
550
  kept in the reader's own browser, so it is per-person and per-device, and it
550
551
  is not part of the record.
551
552
 
553
+ A **quiz** is the same idea with an answer to choose. Each question carries
554
+ two to six options, `answer` as the zero-based index of the correct one, a
555
+ required `explanation`, and an optional `source` naming where in the document
556
+ the answer lives:
557
+
558
+ ```yaml
559
+ quiz:
560
+ title: Expense approvals
561
+ questionsPerRound: 10 # optional; the default
562
+ questions:
563
+ - question: Who approves a purchase above the threshold?
564
+ options:
565
+ - A second approver, independent of the requester
566
+ - The requester's own manager, in every case
567
+ - The finance team, after the purchase completes
568
+ answer: 0
569
+ explanation: Why that is right, and why the others are not.
570
+ source: Approvals — thresholds
571
+ ```
572
+
573
+ The reader sees whether they were right **immediately**, then the
574
+ explanation — a quiz here teaches through the mistake, and it does not
575
+ certify anybody, so there is no pass mark. Answers stay in the reader's
576
+ browser and are sent nowhere. A bank larger than `questionsPerRound` is drawn
577
+ from at random and offers another round; a smaller one is simply asked in
578
+ full.
579
+
580
+ **`pnpm check` and `pnpm build` both refuse a quiz a reader could pass
581
+ without reading**, and this is worth knowing before you write one, because
582
+ it is easy to trip by accident:
583
+
584
+ | Refusal | Means |
585
+ | -------------------------- | ----------------------------------------------------- |
586
+ | `ksor-quiz-answer-bias` | more than 60% of answers sit at one option position |
587
+ | `ksor-quiz-length-bias` | picking the longest (or shortest) option usually wins |
588
+ | `ksor-quiz-answer-run` | four or more questions in a row share an answer |
589
+ | `ksor-quiz-contradiction` | an explanation calls the marked answer wrong |
590
+ | `ksor-quiz-duplicate-stem` | two questions open with the same 60 characters |
591
+
592
+ So: move the correct answer around, and keep the options close in length.
593
+ The ratio rules do not apply below five questions, where spreading answers
594
+ would mean the checker choosing them for you. These checks come from the
595
+ predecessor, where the same mistakes shipped and were found by readers — one
596
+ quiz had every correct answer in the same position across 451 questions.
597
+
552
598
  - Copy load-bearing values (numbers, thresholds, dates) exactly from their
553
599
  source, and name the source in `provenance`.
554
600
 
@@ -0,0 +1,90 @@
1
+ # A quiz for what-is-a-ksor.md.
2
+ #
3
+ # Every question and every answer states only what its parent document states —
4
+ # a quiz is a way of checking the record, never a second source. Ask your coding
5
+ # agent to write one from a document, and to check each answer back against it.
6
+ #
7
+ # Two habits worth copying. The options are kept close in LENGTH, because if the
8
+ # correct answer is reliably the longest one a reader passes by looking rather
9
+ # than by reading. And the correct answer moves POSITION between questions: a
10
+ # quiz whose answer is usually B is a quiz you can pass without the document.
11
+ # `pnpm check` and `pnpm build` both refuse a quiz where either is true — the
12
+ # first draft of this one was refused for putting four of five answers at B.
13
+ quiz:
14
+ title: Check yourself
15
+ description: Five questions on what this record is, and what it is not.
16
+ questions:
17
+ - question: A spreadsheet and the ledger disagree about a number. Which one is authoritative?
18
+ options:
19
+ - Whichever of the two was most recently edited
20
+ - The spreadsheet, being closer to the daily work
21
+ - The ledger, because it is the governing copy
22
+ - Neither, until a person reconciles the two
23
+ answer: 2
24
+ explanation: >
25
+ A system of record is the copy that governs, so the ledger wins by
26
+ definition rather than by being newer or closer to the work. Recency is
27
+ not authority: an edit made this morning in a spreadsheet nobody governs
28
+ is still ungoverned. Reconciliation is something a record makes possible,
29
+ not a precondition for having an answer at all.
30
+ source: The opening definition
31
+
32
+ - question: What does a KSoR settle that a traditional system of record does not?
33
+ options:
34
+ - The state of a business, such as its balances
35
+ - Which vendor an organization has chosen to use
36
+ - How much storage the organization is paying for
37
+ - What the organization knows and how it operates
38
+ answer: 3
39
+ explanation: >
40
+ A traditional system of record settles the state of a business — what is
41
+ owed, what is owned, what was transacted. A KSoR settles the layer above
42
+ it: which policies apply, which thresholds are approved, and what a term
43
+ means inside this organization. Vendor choice and storage cost are
44
+ consequences of running one, never what it is authoritative for.
45
+ source: What this record settles
46
+
47
+ - question: Why can an ordinary assistant not tell you which of its sentences were checked?
48
+ options:
49
+ - It answers from everything it has ever read
50
+ - It was not given enough context to work with
51
+ - Its answers are checked, but only in summary
52
+ - It lacks permission to reveal its own sources
53
+ answer: 0
54
+ explanation: >
55
+ It answers from everything it has ever read, and nothing in that process
56
+ separates a checked claim from an unchecked one. This is not a context
57
+ problem and not a permissions problem: more context cannot create a
58
+ distinction that was never recorded in the first place. The record closes
59
+ the gap by governing what gets read.
60
+ source: Why an assistant cannot answer that question
61
+
62
+ - question: What problem does a KSoR exist to solve?
63
+ options:
64
+ - Storage costs across a growing organization
65
+ - Scatter, with no authoritative copy to trust
66
+ - The speed at which an assistant can reply
67
+ - The difficulty of writing documentation well
68
+ answer: 1
69
+ explanation: >
70
+ The problem is scatter: knowledge spread across wikis, decks, PDFs,
71
+ prompts and somebody's memory, with no authoritative answer to which of
72
+ them an agent should trust. Speed and cost are not what the record
73
+ addresses, and writing well is valuable but does not by itself tell an
74
+ agent which of two documents governs.
75
+ source: The problem it solves
76
+
77
+ - question: Abstention — answering "not in this corpus" — is best described as what?
78
+ options:
79
+ - An error state the record should minimise
80
+ - A temporary gap, pending the next ingest
81
+ - A setting an operator turns on when ready
82
+ - A correct answer the record is meant to give
83
+ answer: 3
84
+ explanation: >
85
+ Abstention is a correct answer, never an error and never a licence to
86
+ fall back on general model knowledge. A record that answers everything
87
+ has stopped being a record of anything in particular. It is not a gap
88
+ waiting to be filled, and while the gate that enforces it is configured
89
+ deliberately, the honesty it expresses is the product itself.
90
+ source: What this record settles, and what it does not
@@ -17,8 +17,9 @@ import { showGovernance } from "@/lib/shared";
17
17
  import { RecordToc, TocItems } from "@/components/record-toc";
18
18
  import { RecordViews } from "@/components/record-views";
19
19
  import { Flashcards } from "@/components/flashcards";
20
+ import { Quiz } from "@/components/quiz";
20
21
  import { StudyAids } from "@/components/study-aids";
21
- import { deckFor, summaryFor } from "@/lib/attachments";
22
+ import { deckFor, quizFor, summaryFor } from "@/lib/attachments";
22
23
  import { readingMinutes } from "@/lib/reading-time";
23
24
 
24
25
  export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
@@ -32,6 +33,7 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
32
33
  const summary = summaryFor(page.path);
33
34
  const Summary = summary?.body ?? null;
34
35
  const deck = deckFor(page.path);
36
+ const quiz = quizFor(page.path);
35
37
  // Counted at BUILD time from the document's own markdown, so the figure is in
36
38
  // the shipped HTML for a reader with a failed bundle, a crawler and an agent
37
39
  // alike. The predecessor measured the rendered DOM after paint, which put it
@@ -157,7 +159,12 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
157
159
  region, so the quiz that will sit beside the deck is a child here and
158
160
  not a new argument about where it goes. Renders nothing at all when
159
161
  the document has no study aids. */}
160
- <StudyAids>{deck === null ? null : <Flashcards deck={deck} />}</StudyAids>
162
+ {/* Recall first, then the check on it — and each renders only if the
163
+ document carries one, so a page with just a quiz shows just a quiz. */}
164
+ <StudyAids>
165
+ {deck === null ? null : <Flashcards deck={deck} />}
166
+ {quiz === null ? null : <Quiz quiz={quiz} />}
167
+ </StudyAids>
161
168
  {/* A folder's index page lists what the folder holds. Without it the
162
169
  page ended at its own sentence and the documents below it were
163
170
  reachable only from the sidebar (research/site-design.md F5). Empty
@@ -274,6 +274,74 @@ html > body[data-scroll-locked] {
274
274
  --ksor-caution: #e0906e;
275
275
  }
276
276
 
277
+ /* Right and wrong, for the quiz — the ONE place this record spends green and
278
+ red, and it earns them: a reader checking an answer is asking a true/false
279
+ question, and the accent cannot say "this is correct" while also saying
280
+ "this is what you picked". Both are needed at once when you answer wrongly.
281
+
282
+ Colour is never the only channel: the correct option also carries a check,
283
+ the wrong one a cross, and the line above states the verdict in words — so
284
+ the pair works for a reader who cannot distinguish them. Declared as
285
+ light/dark pairs on the root for the reason `--ksor-caution` above records.
286
+
287
+ Deliberately NOT `--ksor-caution`, which means "the record withdrew this".
288
+ A wrong answer is not a governance state. */
289
+ :root {
290
+ --ksor-correct: #1a7f4b;
291
+ --ksor-incorrect: #b3261e;
292
+ }
293
+
294
+ .dark {
295
+ --ksor-correct: #6edaa0;
296
+ --ksor-incorrect: #f2897f;
297
+ }
298
+
299
+ /* The three states an answered question shows at once. Written as CLASSES,
300
+ the way `.ksor-caution` below is, rather than as Tailwind arbitrary values:
301
+ `border-[color:var(--x)]` emits a rule whose `var()` did not resolve on the
302
+ element in a real build (found live 2026-08-23 — the class was present, the
303
+ rule was in the stylesheet, the token computed correctly on the element, and
304
+ the border still painted the default hairline). A plain class has none of
305
+ that ambiguity, and this file is already where this record's semantic colour
306
+ lives. */
307
+ .ksor-answer-correct {
308
+ border-color: var(--ksor-correct);
309
+ background-color: color-mix(in oklab, var(--ksor-correct) 10%, transparent);
310
+ }
311
+
312
+ .ksor-answer-wrong {
313
+ border-color: var(--ksor-incorrect);
314
+ background-color: color-mix(in oklab, var(--ksor-incorrect) 10%, transparent);
315
+ }
316
+
317
+ /* "This is the one you picked" — kept in the accent, and kept SEPARATE from
318
+ right/wrong, because after a wrong answer a reader needs all three facts. */
319
+ .ksor-answer-mine {
320
+ outline: 2px solid var(--color-fd-primary);
321
+ outline-offset: 1px;
322
+ }
323
+
324
+ .ksor-answer-correct-text {
325
+ color: var(--ksor-correct);
326
+ }
327
+
328
+ .ksor-answer-wrong-text {
329
+ color: var(--ksor-incorrect);
330
+ }
331
+
332
+ .dark .ksor-answer-correct,
333
+ .dark .ksor-answer-wrong {
334
+ background-color: color-mix(in oklab, currentColor 0%, transparent);
335
+ }
336
+
337
+ .dark .ksor-answer-correct {
338
+ background-color: color-mix(in oklab, var(--ksor-correct) 14%, transparent);
339
+ }
340
+
341
+ .dark .ksor-answer-wrong {
342
+ background-color: color-mix(in oklab, var(--ksor-incorrect) 14%, transparent);
343
+ }
344
+
277
345
  .ksor-caution {
278
346
  border-color: color-mix(in oklab, var(--ksor-caution) 32%, transparent);
279
347
  border-left-color: var(--ksor-caution);