@panaversity/ksor 0.0.29 → 0.0.31

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 (30) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +24 -0
  3. package/dist/cli.mjs +11 -3
  4. package/dist/{gateway-api-8lNruq9e-CuohjtoK.mjs → gateway-api-6nC9x54K-BWFTI_6U.mjs} +1 -1
  5. package/dist/gateway.mjs +1 -1
  6. package/package.json +1 -1
  7. package/templates/scaffold/.agents/skills/format-checker/check.mjs +11 -1
  8. package/templates/scaffold/.agents/skills/make-slides/SKILL.md +160 -0
  9. package/templates/scaffold/.claude/skills/format-checker/check.mjs +11 -1
  10. package/templates/scaffold/.claude/skills/make-slides/SKILL.md +160 -0
  11. package/templates/scaffold/AGENTS.md +91 -5
  12. package/templates/scaffold/README.md +24 -1
  13. package/templates/scaffold/knowledge/what-is-a-ksor.quiz.yaml +90 -0
  14. package/templates/scaffold/knowledge/what-is-a-ksor.slides.yaml +65 -0
  15. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +17 -2
  16. package/templates/scaffold/system/site/app/global.css +113 -0
  17. package/templates/scaffold/system/site/components/deck-viewer.tsx +195 -0
  18. package/templates/scaffold/system/site/components/quiz.tsx +321 -0
  19. package/templates/scaffold/system/site/components/slides.tsx +128 -0
  20. package/templates/scaffold/system/site/components/study-aids.tsx +1 -1
  21. package/templates/scaffold/system/site/lib/attachment-rule.ts +14 -0
  22. package/templates/scaffold/system/site/lib/attachments.ts +88 -3
  23. package/templates/scaffold/system/site/lib/deck.ts +3 -12
  24. package/templates/scaffold/system/site/lib/identity.ts +55 -0
  25. package/templates/scaffold/system/site/lib/quiz-audit.ts +306 -0
  26. package/templates/scaffold/system/site/lib/quiz-round.ts +57 -0
  27. package/templates/scaffold/system/site/lib/quiz.ts +84 -0
  28. package/templates/scaffold/system/site/lib/slides-embed.ts +93 -0
  29. package/templates/scaffold/system/site/lib/slides.ts +123 -0
  30. package/templates/scaffold/system/site/source.config.ts +25 -0
@@ -506,11 +506,15 @@ 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 four optional companions named
510
+ after it, in the same folder: `<doc>.summary.md` (a short précis),
511
+ `<doc>.slides.yaml` (a presentation), `<doc>.flashcards.yaml` (a recall deck)
512
+ and `<doc>.quiz.yaml` (a multiple-choice check). The summary appears as a
513
+ second tab beside the document's own words; the presentation appears at the
514
+ TOP of the page, before the document, because a deck is the shape of the
515
+ thing and gives the detail somewhere to land; the deck and the quiz appear at
516
+ the END, because those are used after reading. None of them appears anywhere
517
+ else in the site.
514
518
 
515
519
  An attachment is **part of its document**, not a document. It has no URL of
516
520
  its own, no sidebar row, no line in `llms.txt`, and no identity an agent can
@@ -549,6 +553,86 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
549
553
  kept in the reader's own browser, so it is per-person and per-device, and it
550
554
  is not part of the record.
551
555
 
556
+ A **quiz** is the same idea with an answer to choose. Each question carries
557
+ two to six options, `answer` as the zero-based index of the correct one, a
558
+ required `explanation`, and an optional `source` naming where in the document
559
+ the answer lives:
560
+
561
+ ```yaml
562
+ quiz:
563
+ title: Expense approvals
564
+ questionsPerRound: 10 # optional; the default
565
+ questions:
566
+ - question: Who approves a purchase above the threshold?
567
+ options:
568
+ - A second approver, independent of the requester
569
+ - The requester's own manager, in every case
570
+ - The finance team, after the purchase completes
571
+ answer: 0
572
+ explanation: Why that is right, and why the others are not.
573
+ source: Approvals — thresholds
574
+ ```
575
+
576
+ The reader sees whether they were right **immediately**, then the
577
+ explanation — a quiz here teaches through the mistake, and it does not
578
+ certify anybody, so there is no pass mark. Answers stay in the reader's
579
+ browser and are sent nowhere. A bank larger than `questionsPerRound` is drawn
580
+ from at random and offers another round; a smaller one is simply asked in
581
+ full.
582
+
583
+ **`pnpm check` and `pnpm build` both refuse a quiz a reader could pass
584
+ without reading**, and this is worth knowing before you write one, because
585
+ it is easy to trip by accident:
586
+
587
+ | Refusal | Means |
588
+ | -------------------------- | ----------------------------------------------------- |
589
+ | `ksor-quiz-answer-bias` | more than 60% of answers sit at one option position |
590
+ | `ksor-quiz-length-bias` | picking the longest (or shortest) option usually wins |
591
+ | `ksor-quiz-answer-run` | four or more questions in a row share an answer |
592
+ | `ksor-quiz-contradiction` | an explanation calls the marked answer wrong |
593
+ | `ksor-quiz-duplicate-stem` | two questions open with the same 60 characters |
594
+
595
+ So: move the correct answer around, and keep the options close in length.
596
+ The ratio rules do not apply below five questions, where spreading answers
597
+ would mean the checker choosing them for you. These checks come from the
598
+ predecessor, where the same mistakes shipped and were found by readers — one
599
+ quiz had every correct answer in the same position across 451 questions.
600
+
601
+ A **presentation** is slides the record owns. Ask your coding agent for one
602
+ rather than writing it by hand — `make slides for knowledge/<path>.md` runs
603
+ the `make-slides` skill, which reads the document, writes the deck, checks
604
+ every claim back against it, and tells you what it left out:
605
+
606
+ ```yaml
607
+ slides:
608
+ title: Expense approvals
609
+ description: The 15-minute version, for a room.
610
+ deck:
611
+ - heading: When two copies disagree, one wins
612
+ bullets:
613
+ - Two approvers above the threshold, always
614
+ - The threshold is per invoice, including tax
615
+ note: Spoken, never shown. Pause here — people remember this one wrong.
616
+ ```
617
+
618
+ `heading` is required and should be a claim rather than a label; `lead` is
619
+ one sentence for a single-point slide; `bullets` caps at six because a slide
620
+ someone reads aloud is a slide nobody listens to; `note` is what the
621
+ presenter SAYS and never appears on the slide, including in fullscreen.
622
+
623
+ The slides render in the page — no third party, no request to anyone, and
624
+ every slide is in the shipped HTML, so a reader without JavaScript still gets
625
+ the whole deck. Because they live in the record they are reviewed in the same
626
+ pull request as the document, versioned with it, and withdrawn with it.
627
+
628
+ **A deck you keep elsewhere** can be pointed at instead — `slides.url:` and
629
+ no `deck:`. The embed url is derived for Google Slides, Canva and SlideShare;
630
+ anything else needs an explicit `embed:` or renders as a link. The url must
631
+ be `https` (a browser blocks a mixed-content frame silently), and the frame
632
+ loads on CLICK, so nothing is requested from the host until a reader asks.
633
+ Declaring both `deck:` and `slides.url:` is refused — two presentations with
634
+ nothing to say which one governs.
635
+
552
636
  - Copy load-bearing values (numbers, thresholds, dates) exactly from their
553
637
  source, and name the source in `provenance`.
554
638
 
@@ -571,6 +655,8 @@ CI — and a first deploy without it serves an empty record. Full walkthrough:
571
655
  write `instance.md` together.
572
656
  - `.agents/skills/add-sources/` — turn source material (documents, pages,
573
657
  notes) into governed knowledge.
658
+ - `.agents/skills/make-slides/` — generate a presentation from one document
659
+ and attach it, so it renders on that document's page.
574
660
  - `.agents/skills/format-checker/` — the rules above, as a program;
575
661
  `pnpm check` runs it and its errors explain how to fix themselves.
576
662
 
@@ -23,6 +23,29 @@ versions that bundle corepack. The first `pnpm install` also fetches the
23
23
  `ksor` tool (pinned in `package.json`) and writes it into your lockfile —
24
24
  commit the updated lockfile.
25
25
 
26
+ ### Presenting a document
27
+
28
+ Ask your coding agent for slides and it writes them, from the document, into
29
+ the record:
30
+
31
+ ```
32
+ make slides for knowledge/expenses/approvals.md
33
+ ```
34
+
35
+ The `make-slides` skill reads the document whole, writes the deck into
36
+ `knowledge/expenses/approvals.slides.yaml`, checks every claim and every
37
+ number back against the document, and tells you what it left out because the
38
+ document did not support it — which is usually how you find out a document has
39
+ a gap. The deck then renders at the top of that document's page: click through
40
+ it inline, or **Present** for fullscreen. Presenter notes stay off the screen.
41
+
42
+ The slides live in the record, so they are reviewed in the same pull request
43
+ as the document, versioned with it, and withdrawn when it is withdrawn. There
44
+ is no third party and no link to rot. If you already keep a deck in Google
45
+ Slides, Canva or SlideShare you can point at it instead — `slides.url:` rather
46
+ than `deck:` — and the page will offer it as a link with a frame the reader
47
+ loads on click, so nothing is requested from the host until somebody asks.
48
+
26
49
  ### Serving to agents
27
50
 
28
51
  The record's other surface is an MCP server for AI agents — the same
@@ -128,7 +151,7 @@ different coding agent's way of finding the same working contract.
128
151
  | `instance.md` | what this record is authoritative for; its `name:` is the identity every surface publishes (read at server/build start — restart `pnpm dev` after renaming). This prose IS the agent surface's system prompt — `ksor serve` wires it into the MCP server's instructions. |
129
152
  | `AGENTS.md` | the working contract every coding agent reads first — the rules for writing knowledge here. |
130
153
  | `CLAUDE.md` | one line, pointing at `AGENTS.md`. Claude Code looks for this filename, not that one. |
131
- | `.agents/skills/` | the agent kit: `intake-interview` (define the record with you), `add-sources` (turn source material into governed documents), `format-checker` (the rules, as a program). |
154
+ | `.agents/skills/` | the agent kit: `intake-interview` (define the record with you), `add-sources` (turn source material into governed documents), `make-slides` (generate a presentation from a document and attach it), `format-checker` (the rules, as a program). |
132
155
  | `.claude/skills/` | byte-identical copies of the kit — Claude Code discovers skills only here. The checker enforces the mirror, so the two cannot drift. |
133
156
  | `.gemini/settings.json` | points Gemini CLI at `AGENTS.md`; Gemini does not read that filename on its own. |
134
157
  | `.github/workflows/validate.yml` | your CI: runs the same checker on every pull request and push to main. |
@@ -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
@@ -0,0 +1,65 @@
1
+ # The presentation that teaches what-is-a-ksor.md.
2
+ #
3
+ # These slides live IN the record: reviewed in a pull request, versioned with
4
+ # the document, withdrawn when it is withdrawn, and rendered by the site — so
5
+ # there is no third party, no dead link, and nothing to keep in step by hand.
6
+ # Your coding agent writes them from the document; `.agents/skills/make-slides/`
7
+ # is the procedure.
8
+ #
9
+ # A slide may only say what the document says. A deck is a way of presenting
10
+ # the record, never a second source.
11
+ slides:
12
+ title: Introducing the record
13
+ description: The 15-minute version, for a room.
14
+ deck:
15
+ # The opening slide does NOT repeat the page title. It sits directly under
16
+ # it, so restating it wastes the one slide everyone actually looks at.
17
+ - heading: Which copy should an agent trust?
18
+ lead: That question is what this record exists to answer, and nothing in an ordinary assistant can.
19
+ note: Open with the question, not the definition. The definition lands after they feel the problem.
20
+
21
+ - heading: The problem it solves
22
+ bullets:
23
+ - Knowledge is scattered across wikis, decks, PDFs, prompts and memory
24
+ - No authoritative answer to the question an agent has to ask
25
+ - Which of these copies should I trust?
26
+ note: Ask the room where their real answer lives today. Wait for the disagreement — it always comes.
27
+
28
+ - heading: When two copies disagree, one wins
29
+ bullets:
30
+ - A traditional system of record settles the state of a business
31
+ - The ledger is authoritative; the spreadsheet is not
32
+ - Recency is not authority — a fresh edit to an ungoverned copy is still ungoverned
33
+ note: This is the whole idea. If they take one slide away, it is this one.
34
+
35
+ - heading: AI never had one
36
+ bullets:
37
+ - An assistant answers from everything it has ever read
38
+ - Nothing in that separates a checked claim from an unchecked one
39
+ - Which is exactly why it cannot tell you which of its sentences were verified
40
+ note: Not a criticism of the models. It is a missing layer, and that layer is what we are building.
41
+
42
+ - heading: What this record settles
43
+ bullets:
44
+ - Which policies apply, and which thresholds are approved
45
+ - What a term means inside this organization
46
+ - What to do when the answer is not known
47
+ note: Point at the third one. It is the one people do not expect.
48
+
49
+ - heading: Abstention is a feature
50
+ lead: '"Not in this corpus" is a correct answer — never an error, and never a licence to fall back on model knowledge.'
51
+ note: A record that answers everything has stopped being a record of anything in particular.
52
+
53
+ - heading: One source, two surfaces
54
+ bullets:
55
+ - The website is for people
56
+ - The MCP server is for agents
57
+ - Both render the same build — they can never disagree
58
+ note: Open llms.txt beside the page here. Seeing one source render twice lands better than describing it.
59
+
60
+ - heading: Where the edges are
61
+ bullets:
62
+ - Provenance proves who said what, and when
63
+ - It does not prove the source was right — that is a separate judgment
64
+ - Governance is a ladder, not a gate; level 0 works on day one
65
+ note: Say the second bullet out loud. Overselling provenance is how these systems lose trust.
@@ -17,8 +17,10 @@ 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";
21
+ import { Slides } from "@/components/slides";
20
22
  import { StudyAids } from "@/components/study-aids";
21
- import { deckFor, summaryFor } from "@/lib/attachments";
23
+ import { deckFor, quizFor, slidesFor, summaryFor } from "@/lib/attachments";
22
24
  import { readingMinutes } from "@/lib/reading-time";
23
25
 
24
26
  export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
@@ -32,6 +34,8 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
32
34
  const summary = summaryFor(page.path);
33
35
  const Summary = summary?.body ?? null;
34
36
  const deck = deckFor(page.path);
37
+ const quiz = quizFor(page.path);
38
+ const presentation = slidesFor(page.path);
35
39
  // Counted at BUILD time from the document's own markdown, so the figure is in
36
40
  // the shipped HTML for a reader with a failed bundle, a crawler and an agent
37
41
  // alike. The predecessor measured the rendered DOM after paint, which put it
@@ -122,6 +126,12 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
122
126
  {showGovernance ? (
123
127
  <GovernanceMeta governance={governance} replaces={replaces} markdownUrl={markdownUrl} />
124
128
  ) : null}
129
+ {/* BEFORE the document, not after it. The deck is the shape of the
130
+ thing — five minutes of slides gives the detail somewhere to land —
131
+ so it belongs where a reader meets it first, which is also where the
132
+ predecessor puts its own. The recall aids stay at the end, because
133
+ those are used AFTER reading. */}
134
+ {presentation === null ? null : <Slides slides={presentation} />}
125
135
  {/* grow-0, against the shell's own `flex-1`: the article is a flex column
126
136
  stretched to the viewport, so the body inflated from ~150px of text to
127
137
  402px and pushed Sources and everything after it to the bottom of the
@@ -157,7 +167,12 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
157
167
  region, so the quiz that will sit beside the deck is a child here and
158
168
  not a new argument about where it goes. Renders nothing at all when
159
169
  the document has no study aids. */}
160
- <StudyAids>{deck === null ? null : <Flashcards deck={deck} />}</StudyAids>
170
+ {/* Recall first, then the check on it — and each renders only if the
171
+ document carries one, so a page with just a quiz shows just a quiz. */}
172
+ <StudyAids>
173
+ {deck === null ? null : <Flashcards deck={deck} />}
174
+ {quiz === null ? null : <Quiz quiz={quiz} />}
175
+ </StudyAids>
161
176
  {/* A folder's index page lists what the folder holds. Without it the
162
177
  page ended at its own sentence and the documents below it were
163
178
  reachable only from the sidebar (research/site-design.md F5). Empty
@@ -274,6 +274,119 @@ html > body[data-scroll-locked] {
274
274
  --ksor-caution: #e0906e;
275
275
  }
276
276
 
277
+ /* The slide STAGE.
278
+
279
+ A slide is a projected surface, and reading as one at a glance is most of
280
+ what makes a deck legible on a page full of prose. The first version painted
281
+ it `--muted` and it read as an empty placeholder rather than as a slide —
282
+ pale grey on a pale page, with nothing saying "this is a different medium".
283
+
284
+ So the stage is dark in BOTH themes, deliberately. That is not a palette
285
+ inconsistency: the page is a document and the stage is a projection, and the
286
+ whole job of this block is to look like the second thing while sitting
287
+ inside the first. Its ink is warm-neutral rather than pure white, which is
288
+ what stops a dark panel from glaring on a light page.
289
+
290
+ Light and dark differ only in how far the stage sits from its surround. */
291
+ :root {
292
+ --ksor-stage: oklch(0.21 0.018 258);
293
+ --ksor-stage-ink: oklch(0.96 0.005 90);
294
+ --ksor-stage-dim: oklch(0.74 0.012 258);
295
+ --ksor-stage-rule: oklch(0.32 0.02 258);
296
+ }
297
+
298
+ /* In dark the stage must RISE off the page, not sink into it. A stage darker
299
+ than its surround measured L 4.4 against the page's 3.3 — near-identical,
300
+ so the slide lost its edges entirely and read as text floating on the page
301
+ (found live). On a dark ground, elevation reads as lighter. */
302
+ .dark {
303
+ --ksor-stage: oklch(0.26 0.019 258);
304
+ --ksor-stage-ink: oklch(0.96 0.005 90);
305
+ --ksor-stage-dim: oklch(0.76 0.012 258);
306
+ --ksor-stage-rule: oklch(0.38 0.02 258);
307
+ }
308
+
309
+ .ksor-stage {
310
+ background-color: var(--ksor-stage);
311
+ color: var(--ksor-stage-ink);
312
+ }
313
+
314
+ .ksor-stage-dim {
315
+ color: var(--ksor-stage-dim);
316
+ }
317
+
318
+ .ksor-stage-rule {
319
+ background-color: var(--ksor-stage-rule);
320
+ }
321
+
322
+ /* Right and wrong, for the quiz — the ONE place this record spends green and
323
+ red, and it earns them: a reader checking an answer is asking a true/false
324
+ question, and the accent cannot say "this is correct" while also saying
325
+ "this is what you picked". Both are needed at once when you answer wrongly.
326
+
327
+ Colour is never the only channel: the correct option also carries a check,
328
+ the wrong one a cross, and the line above states the verdict in words — so
329
+ the pair works for a reader who cannot distinguish them. Declared as
330
+ light/dark pairs on the root for the reason `--ksor-caution` above records.
331
+
332
+ Deliberately NOT `--ksor-caution`, which means "the record withdrew this".
333
+ A wrong answer is not a governance state. */
334
+ :root {
335
+ --ksor-correct: #1a7f4b;
336
+ --ksor-incorrect: #b3261e;
337
+ }
338
+
339
+ .dark {
340
+ --ksor-correct: #6edaa0;
341
+ --ksor-incorrect: #f2897f;
342
+ }
343
+
344
+ /* The three states an answered question shows at once. Written as CLASSES,
345
+ the way `.ksor-caution` below is, rather than as Tailwind arbitrary values:
346
+ `border-[color:var(--x)]` emits a rule whose `var()` did not resolve on the
347
+ element in a real build (found live 2026-08-23 — the class was present, the
348
+ rule was in the stylesheet, the token computed correctly on the element, and
349
+ the border still painted the default hairline). A plain class has none of
350
+ that ambiguity, and this file is already where this record's semantic colour
351
+ lives. */
352
+ .ksor-answer-correct {
353
+ border-color: var(--ksor-correct);
354
+ background-color: color-mix(in oklab, var(--ksor-correct) 10%, transparent);
355
+ }
356
+
357
+ .ksor-answer-wrong {
358
+ border-color: var(--ksor-incorrect);
359
+ background-color: color-mix(in oklab, var(--ksor-incorrect) 10%, transparent);
360
+ }
361
+
362
+ /* "This is the one you picked" — kept in the accent, and kept SEPARATE from
363
+ right/wrong, because after a wrong answer a reader needs all three facts. */
364
+ .ksor-answer-mine {
365
+ outline: 2px solid var(--color-fd-primary);
366
+ outline-offset: 1px;
367
+ }
368
+
369
+ .ksor-answer-correct-text {
370
+ color: var(--ksor-correct);
371
+ }
372
+
373
+ .ksor-answer-wrong-text {
374
+ color: var(--ksor-incorrect);
375
+ }
376
+
377
+ .dark .ksor-answer-correct,
378
+ .dark .ksor-answer-wrong {
379
+ background-color: color-mix(in oklab, currentColor 0%, transparent);
380
+ }
381
+
382
+ .dark .ksor-answer-correct {
383
+ background-color: color-mix(in oklab, var(--ksor-correct) 14%, transparent);
384
+ }
385
+
386
+ .dark .ksor-answer-wrong {
387
+ background-color: color-mix(in oklab, var(--ksor-incorrect) 14%, transparent);
388
+ }
389
+
277
390
  .ksor-caution {
278
391
  border-color: color-mix(in oklab, var(--ksor-caution) 32%, transparent);
279
392
  border-left-color: var(--ksor-caution);