tessera-learn 0.0.1 → 0.0.2

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.
@@ -2,6 +2,9 @@
2
2
  import { getContext, onMount } from 'svelte';
3
3
  import { useQuestion } from '../runtime/hooks.svelte.js';
4
4
  import { slugFromQuestion } from './util.js';
5
+ import LockedBanner from './LockedBanner.svelte';
6
+ import ResultIcon from './ResultIcon.svelte';
7
+ import RetryButton from './RetryButton.svelte';
5
8
 
6
9
  let {
7
10
  id,
@@ -18,11 +21,9 @@
18
21
  const standalone = !quiz;
19
22
 
20
23
  let inputValue = $state('');
21
- let saRetryCount = $state(0);
22
- let saCanRetry = $derived(saRetryCount < maxRetries);
23
24
 
24
- const inputId = `fitb-${Math.random().toString(36).slice(2, 9)}`;
25
- const defaultId = `fitb-${slugFromQuestion(question)}`;
25
+ const componentId = $props.id();
26
+ const inputId = `fitb-${componentId}`;
26
27
 
27
28
  function checkAnswer(userAnswer) {
28
29
  if (!userAnswer || typeof userAnswer !== 'string') return false;
@@ -35,8 +36,9 @@
35
36
  }
36
37
 
37
38
  const handle = useQuestion({
38
- id: id ?? defaultId,
39
- weight,
39
+ get id() { return id ?? `fitb-${slugFromQuestion(question)}`; },
40
+ get weight() { return weight; },
41
+ get maxRetries() { return maxRetries; },
40
42
  response: () => ({
41
43
  type: 'fill-in',
42
44
  response: inputValue,
@@ -73,11 +75,6 @@
73
75
  }
74
76
  }
75
77
 
76
- function handleRetry() {
77
- saRetryCount++;
78
- inputValue = '';
79
- handle.reset();
80
- }
81
78
  </script>
82
79
 
83
80
  {#if standalone}
@@ -100,7 +97,7 @@
100
97
  />
101
98
  {#if !handle.submitted}
102
99
  <button
103
- class="tessera-fitb-check-btn"
100
+ class="tessera-btn-primary tessera-fitb-check-btn"
104
101
  disabled={!inputValue.trim()}
105
102
  onclick={() => { handle.submit(); }}
106
103
  >
@@ -114,9 +111,7 @@
114
111
  <div class="tessera-fitb-review">
115
112
  {#if isCorrect}
116
113
  <div class="tessera-fitb-result correct">
117
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
118
- <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
119
- </svg>
114
+ <ResultIcon kind="correct" />
120
115
  Correct
121
116
  </div>
122
117
  {#if correctFeedback}
@@ -124,9 +119,7 @@
124
119
  {/if}
125
120
  {:else}
126
121
  <div class="tessera-fitb-result incorrect">
127
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
128
- <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
129
- </svg>
122
+ <ResultIcon kind="incorrect" />
130
123
  Incorrect
131
124
  </div>
132
125
  <p class="tessera-fitb-correct-answer">
@@ -136,8 +129,8 @@
136
129
  <p class="tessera-fitb-feedback incorrect">{incorrectFeedback}</p>
137
130
  {/if}
138
131
  {/if}
139
- {#if saCanRetry}
140
- <button class="tessera-standalone-retry" onclick={handleRetry}>Try again</button>
132
+ {#if handle.canRetry}
133
+ <RetryButton onclick={() => handle.retry()} />
141
134
  {/if}
142
135
  </div>
143
136
  {/if}
@@ -147,12 +140,7 @@
147
140
  {#snippet renderQuestion()}
148
141
  <div class="tessera-fitb">
149
142
  {#if isLocked}
150
- <div class="tessera-quiz-locked-banner">
151
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
152
- <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
153
- </svg>
154
- You already got this one right — click Next to continue.
155
- </div>
143
+ <LockedBanner />
156
144
  {/if}
157
145
  <label class="tessera-fitb-question" for={inputId}>{question}</label>
158
146
 
@@ -177,9 +165,7 @@
177
165
  <div class="tessera-fitb-review">
178
166
  {#if isCorrect}
179
167
  <div class="tessera-fitb-result correct">
180
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
181
- <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
182
- </svg>
168
+ <ResultIcon kind="correct" />
183
169
  Correct
184
170
  </div>
185
171
  {#if correctFeedback}
@@ -187,9 +173,7 @@
187
173
  {/if}
188
174
  {:else}
189
175
  <div class="tessera-fitb-result incorrect">
190
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
191
- <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
192
- </svg>
176
+ <ResultIcon kind="incorrect" />
193
177
  Incorrect
194
178
  </div>
195
179
  <p class="tessera-fitb-correct-answer">
@@ -237,7 +221,7 @@
237
221
  .tessera-fitb-input:focus {
238
222
  outline: none;
239
223
  border-color: var(--tessera-primary);
240
- box-shadow: var(--tessera-focus-ring, 0 0 0 3px rgba(37, 99, 235, 0.4));
224
+ box-shadow: var(--tessera-focus-ring);
241
225
  }
242
226
 
243
227
  .tessera-fitb-input:disabled {
@@ -288,12 +272,12 @@
288
272
 
289
273
  .tessera-fitb-feedback.correct {
290
274
  color: var(--tessera-success);
291
- background: color-mix(in srgb, var(--tessera-success) 8%, transparent);
275
+ background: var(--tessera-success-bg);
292
276
  }
293
277
 
294
278
  .tessera-fitb-feedback.incorrect {
295
279
  color: var(--tessera-error);
296
- background: color-mix(in srgb, var(--tessera-error) 8%, transparent);
280
+ background: var(--tessera-error-bg);
297
281
  }
298
282
 
299
283
  .tessera-fitb-check-btn {
@@ -301,40 +285,6 @@
301
285
  padding: 0.5rem 1rem;
302
286
  font-size: 0.875rem;
303
287
  font-weight: 600;
304
- color: #fff;
305
- background: var(--tessera-primary);
306
- border: none;
307
- border-radius: 6px;
308
- cursor: pointer;
309
- min-height: 44px;
310
- transition: background 0.2s, opacity 0.2s;
311
- }
312
-
313
- .tessera-fitb-check-btn:hover:not(:disabled) {
314
- background: var(--tessera-primary-dark);
315
- }
316
-
317
- .tessera-fitb-check-btn:disabled {
318
- opacity: 0.4;
319
- cursor: not-allowed;
320
- }
321
-
322
- .tessera-standalone-retry {
323
- display: inline-block;
324
- margin-top: var(--tessera-spacing-md);
325
- padding: 0;
326
- font-size: 0.875rem;
327
- font-weight: 600;
328
- color: var(--tessera-primary);
329
- background: none;
330
- border: none;
331
- cursor: pointer;
332
- text-decoration: underline;
333
- text-underline-offset: 2px;
334
- }
335
-
336
- .tessera-standalone-retry:hover {
337
- color: var(--tessera-primary-dark);
338
288
  }
339
289
 
340
290
  @media (max-width: 640px) {
@@ -0,0 +1,30 @@
1
+ <script>
2
+ let { message = 'You already got this one right — click Next to continue.' } = $props();
3
+ </script>
4
+
5
+ <div class="tessera-quiz-locked-banner">
6
+ <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
7
+ <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
8
+ </svg>
9
+ {message}
10
+ </div>
11
+
12
+ <style>
13
+ .tessera-quiz-locked-banner {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: var(--tessera-spacing-sm);
17
+ padding: var(--tessera-spacing-md);
18
+ margin-bottom: var(--tessera-spacing-md);
19
+ background: var(--tessera-success-bg);
20
+ border: 1px solid var(--tessera-success-border);
21
+ border-radius: 6px;
22
+ color: var(--tessera-success);
23
+ font-size: 0.9375rem;
24
+ font-weight: 500;
25
+ }
26
+
27
+ .tessera-quiz-locked-banner svg {
28
+ flex-shrink: 0;
29
+ }
30
+ </style>
@@ -2,7 +2,10 @@
2
2
  import { getContext, onMount } from 'svelte';
3
3
  import { SvelteMap } from 'svelte/reactivity';
4
4
  import { useQuestion } from '../runtime/hooks.svelte.js';
5
- import { slugFromQuestion } from './util.js';
5
+ import { slugFromQuestion, shuffle } from './util.js';
6
+ import LockedBanner from './LockedBanner.svelte';
7
+ import ResultIcon from './ResultIcon.svelte';
8
+ import RetryButton from './RetryButton.svelte';
6
9
 
7
10
  let {
8
11
  id,
@@ -19,31 +22,19 @@
19
22
 
20
23
  let shuffledRight = $state([]);
21
24
  let matches = $state(new SvelteMap());
25
+ // Reverse index (right.originalIndex → left index) for O(1) right-column lookups.
26
+ let rightToLeft = $state(new SvelteMap());
22
27
  let selectedLeft = $state(null);
23
28
  let selectedRight = $state(null);
24
29
 
25
- let saRetryCount = $state(0);
26
- let saCanRetry = $derived(saRetryCount < maxRetries);
27
- let saAllMatched = $derived(matches.size === pairs.length);
28
-
29
- const defaultId = `matching-${slugFromQuestion(question)}`;
30
30
 
31
31
  const pairColors = [
32
32
  '#2563eb', '#9333ea', '#0891b2', '#c2410c', '#4f46e5',
33
33
  '#0d9488', '#b91c1c', '#7c3aed', '#0369a1', '#a16207',
34
34
  ];
35
35
 
36
- function shuffleArray(arr) {
37
- const shuffled = [...arr];
38
- for (let i = shuffled.length - 1; i > 0; i--) {
39
- const j = Math.floor(Math.random() * (i + 1));
40
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
41
- }
42
- return shuffled;
43
- }
44
-
45
36
  function initShuffle() {
46
- shuffledRight = shuffleArray(pairs.map((p, i) => ({ text: p.right, originalIndex: i })));
37
+ shuffledRight = shuffle(pairs.map((p, i) => ({ text: p.right, originalIndex: i })));
47
38
  }
48
39
 
49
40
  if (standalone) {
@@ -66,14 +57,16 @@
66
57
 
67
58
  function resetState() {
68
59
  matches = new SvelteMap();
60
+ rightToLeft = new SvelteMap();
69
61
  selectedLeft = null;
70
62
  selectedRight = null;
71
63
  initShuffle();
72
64
  }
73
65
 
74
66
  const handle = useQuestion({
75
- id: id ?? defaultId,
76
- weight,
67
+ get id() { return id ?? `matching-${slugFromQuestion(question)}`; },
68
+ get weight() { return weight; },
69
+ get maxRetries() { return maxRetries; },
77
70
  response: () => ({
78
71
  type: 'matching',
79
72
  response: [...matches.entries()].map(([l, r]) => [String(l), String(r)]),
@@ -87,13 +80,6 @@
87
80
  let isLocked = $derived(standalone ? false : quiz.isLockedCorrect(myIndex));
88
81
  let quizLocked = $derived(standalone ? handle.submitted : quiz.isAnswerLocked(myIndex));
89
82
 
90
- // Auto-submit in standalone mode when all pairs matched
91
- $effect(() => {
92
- if (standalone && saAllMatched && !handle.submitted) {
93
- handle.submit();
94
- }
95
- });
96
-
97
83
  function handleLeftClick(leftIndex) {
98
84
  if (standalone) {
99
85
  if (handle.submitted) return;
@@ -133,17 +119,26 @@
133
119
  }
134
120
 
135
121
  function createMatch(leftIndex, rightOriginalIndex) {
136
- for (const [l, r] of matches) {
137
- if (l === leftIndex || r === rightOriginalIndex) {
138
- matches.delete(l);
139
- }
122
+ // Free any prior partner on either side so both maps stay consistent.
123
+ const priorRightForLeft = matches.get(leftIndex);
124
+ if (priorRightForLeft !== undefined) {
125
+ rightToLeft.delete(priorRightForLeft);
126
+ }
127
+ const priorLeftForRight = rightToLeft.get(rightOriginalIndex);
128
+ if (priorLeftForRight !== undefined) {
129
+ matches.delete(priorLeftForRight);
140
130
  }
141
131
 
142
132
  matches.set(leftIndex, rightOriginalIndex);
133
+ rightToLeft.set(rightOriginalIndex, leftIndex);
143
134
  selectedLeft = null;
144
135
  selectedRight = null;
145
136
 
146
- if (!standalone) {
137
+ if (standalone) {
138
+ if (matches.size === pairs.length && !handle.submitted) {
139
+ handle.submit();
140
+ }
141
+ } else {
147
142
  quiz.setAnswer(myIndex, new Map(matches));
148
143
  }
149
144
  }
@@ -154,34 +149,30 @@
154
149
  } else {
155
150
  if (quizLocked) return;
156
151
  }
152
+ const right = matches.get(leftIndex);
157
153
  matches.delete(leftIndex);
154
+ if (right !== undefined) rightToLeft.delete(right);
158
155
  if (!standalone) {
159
156
  quiz.setAnswer(myIndex, new Map(matches));
160
157
  }
161
158
  }
162
159
 
163
- function handleRetry() {
164
- saRetryCount++;
165
- handle.reset();
166
- }
167
-
168
160
  function getMatchColor(leftIndex) {
169
161
  if (!matches.has(leftIndex)) return null;
170
162
  return pairColors[leftIndex % pairColors.length];
171
163
  }
172
164
 
173
165
  function getRightMatchColor(rightOriginalIndex) {
174
- for (const [l, r] of matches) {
175
- if (r === rightOriginalIndex) return pairColors[l % pairColors.length];
176
- }
177
- return null;
166
+ const l = rightToLeft.get(rightOriginalIndex);
167
+ return l === undefined ? null : pairColors[l % pairColors.length];
178
168
  }
179
169
 
180
170
  function isRightMatched(rightOriginalIndex) {
181
- for (const [, r] of matches) {
182
- if (r === rightOriginalIndex) return true;
183
- }
184
- return false;
171
+ return rightToLeft.has(rightOriginalIndex);
172
+ }
173
+
174
+ function getLeftForRight(rightOriginalIndex) {
175
+ return rightToLeft.get(rightOriginalIndex);
185
176
  }
186
177
 
187
178
  function isMatchCorrect(leftIndex) {
@@ -246,7 +237,7 @@
246
237
  aria-label="{item.text}{matched ? ' (matched)' : ''}"
247
238
  >
248
239
  {#if matched}
249
- {@const leftIdx = [...matches.entries()].find(([, r]) => r === item.originalIndex)?.[0]}
240
+ {@const leftIdx = getLeftForRight(item.originalIndex)}
250
241
  <span class="tessera-matching-badge" style="background: {color}">
251
242
  {leftIdx !== undefined ? leftIdx + 1 : ''}
252
243
  </span>
@@ -262,9 +253,7 @@
262
253
  <div class="tessera-matching-review">
263
254
  {#if isCorrect}
264
255
  <div class="tessera-matching-result correct">
265
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
266
- <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
267
- </svg>
256
+ <ResultIcon kind="correct" />
268
257
  All pairs matched correctly!
269
258
  </div>
270
259
  {#if correctFeedback}
@@ -272,9 +261,7 @@
272
261
  {/if}
273
262
  {:else}
274
263
  <div class="tessera-matching-result incorrect">
275
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
276
- <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
277
- </svg>
264
+ <ResultIcon kind="incorrect" />
278
265
  Some pairs are incorrect
279
266
  </div>
280
267
  <div class="tessera-matching-correct-pairs">
@@ -287,8 +274,8 @@
287
274
  <p class="tessera-matching-feedback incorrect">{incorrectFeedback}</p>
288
275
  {/if}
289
276
  {/if}
290
- {#if standalone && saCanRetry}
291
- <button class="tessera-standalone-retry" onclick={handleRetry}>Try again</button>
277
+ {#if standalone && handle.canRetry}
278
+ <RetryButton onclick={() => handle.retry()} />
292
279
  {/if}
293
280
  </div>
294
281
  {/if}
@@ -303,12 +290,7 @@
303
290
  {#snippet renderQuestion()}
304
291
  <div class="tessera-matching" aria-label={question}>
305
292
  {#if isLocked}
306
- <div class="tessera-quiz-locked-banner">
307
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
308
- <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
309
- </svg>
310
- You already got this one right — click Next to continue.
311
- </div>
293
+ <LockedBanner />
312
294
  {/if}
313
295
  {@render matchingContent()}
314
296
  </div>
@@ -382,12 +364,12 @@
382
364
 
383
365
  .tessera-matching-item.correct {
384
366
  border-color: var(--tessera-success) !important;
385
- background: color-mix(in srgb, var(--tessera-success) 8%, transparent) !important;
367
+ background: var(--tessera-success-bg) !important;
386
368
  }
387
369
 
388
370
  .tessera-matching-item.incorrect {
389
371
  border-color: var(--tessera-error) !important;
390
- background: color-mix(in srgb, var(--tessera-error) 8%, transparent) !important;
372
+ background: var(--tessera-error-bg) !important;
391
373
  }
392
374
 
393
375
  .tessera-matching-item:disabled {
@@ -474,30 +456,12 @@
474
456
 
475
457
  .tessera-matching-feedback.correct {
476
458
  color: var(--tessera-success);
477
- background: color-mix(in srgb, var(--tessera-success) 8%, transparent);
459
+ background: var(--tessera-success-bg);
478
460
  }
479
461
 
480
462
  .tessera-matching-feedback.incorrect {
481
463
  color: var(--tessera-error);
482
- background: color-mix(in srgb, var(--tessera-error) 8%, transparent);
483
- }
484
-
485
- .tessera-standalone-retry {
486
- display: inline-block;
487
- margin-top: var(--tessera-spacing-md);
488
- padding: 0;
489
- font-size: 0.875rem;
490
- font-weight: 600;
491
- color: var(--tessera-primary);
492
- background: none;
493
- border: none;
494
- cursor: pointer;
495
- text-decoration: underline;
496
- text-underline-offset: 2px;
497
- }
498
-
499
- .tessera-standalone-retry:hover {
500
- color: var(--tessera-primary-dark);
464
+ background: var(--tessera-error-bg);
501
465
  }
502
466
 
503
467
  @media (max-width: 640px) {
@@ -2,6 +2,8 @@
2
2
  import { getContext, onMount } from 'svelte';
3
3
  import { useQuestion } from '../runtime/hooks.svelte.js';
4
4
  import { slugFromQuestion } from './util.js';
5
+ import LockedBanner from './LockedBanner.svelte';
6
+ import RetryButton from './RetryButton.svelte';
5
7
 
6
8
  let {
7
9
  id,
@@ -19,16 +21,14 @@
19
21
  const standalone = !quiz;
20
22
 
21
23
  let selectedOption = $state(null);
22
- let saRetryCount = $state(0);
23
- let saCanRetry = $derived(saRetryCount < maxRetries);
24
24
 
25
- // Unique IDs for accessibility
26
- const groupId = `mc-${Math.random().toString(36).slice(2, 9)}`;
27
- const defaultId = `mc-${slugFromQuestion(question)}`;
25
+ const componentId = $props.id();
26
+ const groupId = `mc-${componentId}`;
28
27
 
29
28
  const handle = useQuestion({
30
- id: id ?? defaultId,
31
- weight,
29
+ get id() { return id ?? `mc-${slugFromQuestion(question)}`; },
30
+ get weight() { return weight; },
31
+ get maxRetries() { return maxRetries; },
32
32
  response: () => ({
33
33
  type: 'choice',
34
34
  response: selectedOption !== null ? [String(selectedOption)] : [],
@@ -55,12 +55,6 @@
55
55
  }
56
56
  }
57
57
 
58
- function handleRetry() {
59
- saRetryCount++;
60
- selectedOption = null;
61
- handle.reset();
62
- }
63
-
64
58
  function isCorrectOption(optIndex) {
65
59
  return optIndex === correct;
66
60
  }
@@ -129,8 +123,8 @@
129
123
  {:else if selectedOption !== correct && incorrectFeedback && !optionFeedback[selectedOption]}
130
124
  <div class="tessera-mc-overall-feedback incorrect">{incorrectFeedback}</div>
131
125
  {/if}
132
- {#if saCanRetry}
133
- <button class="tessera-standalone-retry" onclick={handleRetry}>Try again</button>
126
+ {#if handle.canRetry}
127
+ <RetryButton onclick={() => handle.retry()} />
134
128
  {/if}
135
129
  {/if}
136
130
  </div>
@@ -139,12 +133,7 @@
139
133
  {#snippet renderQuestion()}
140
134
  <div class="tessera-mc" role="radiogroup" aria-labelledby="{groupId}-label">
141
135
  {#if isLocked}
142
- <div class="tessera-quiz-locked-banner">
143
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16" aria-hidden="true">
144
- <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
145
- </svg>
146
- You already got this one right — click Next to continue.
147
- </div>
136
+ <LockedBanner />
148
137
  {/if}
149
138
  <p class="tessera-mc-question" id="{groupId}-label">{question}</p>
150
139
 
@@ -237,12 +226,12 @@
237
226
 
238
227
  .tessera-mc-option.correct {
239
228
  border-color: var(--tessera-success);
240
- background: color-mix(in srgb, var(--tessera-success) 8%, transparent);
229
+ background: var(--tessera-success-bg);
241
230
  }
242
231
 
243
232
  .tessera-mc-option.incorrect {
244
233
  border-color: var(--tessera-error);
245
- background: color-mix(in srgb, var(--tessera-error) 8%, transparent);
234
+ background: var(--tessera-error-bg);
246
235
  }
247
236
 
248
237
  .tessera-mc-option input[type="radio"] {
@@ -323,33 +312,15 @@
323
312
  }
324
313
 
325
314
  .tessera-mc-overall-feedback.correct {
326
- background: color-mix(in srgb, var(--tessera-success) 8%, transparent);
315
+ background: var(--tessera-success-bg);
327
316
  color: var(--tessera-success);
328
317
  }
329
318
 
330
319
  .tessera-mc-overall-feedback.incorrect {
331
- background: color-mix(in srgb, var(--tessera-error) 8%, transparent);
320
+ background: var(--tessera-error-bg);
332
321
  color: var(--tessera-error);
333
322
  }
334
323
 
335
- .tessera-standalone-retry {
336
- display: inline-block;
337
- margin-top: var(--tessera-spacing-md);
338
- padding: 0;
339
- font-size: 0.875rem;
340
- font-weight: 600;
341
- color: var(--tessera-primary);
342
- background: none;
343
- border: none;
344
- cursor: pointer;
345
- text-decoration: underline;
346
- text-underline-offset: 2px;
347
- }
348
-
349
- .tessera-standalone-retry:hover {
350
- color: var(--tessera-primary-dark);
351
- }
352
-
353
324
  .tessera-mc-option:has(input:focus-visible) {
354
325
  outline: 2px solid var(--tessera-primary);
355
326
  outline-offset: 2px;