@docyrus/animaguru 0.19.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/dist/animaguru.js +366 -261
- package/package.json +1 -1
- package/skills/animaguru/SKILL.md +9 -1
- package/skills/animaguru/references/sharing.md +3 -0
- package/skills/animaguru-branching-story/SKILL.md +72 -0
- package/skills/animaguru-general-video/SKILL.md +1 -0
- package/skills/animaguru-general-video/references/branching-story.md +13 -0
- package/skills/animaguru-quiz/SKILL.md +80 -0
- package/skills/animaguru-video/SKILL.md +9 -1
- package/templates/video/AGENTS.md +2 -0
- package/templates/video/README.md +8 -0
- package/templates/video/WORKSPACE-AGENTS.md +3 -1
- package/templates/video/scripts/build.mjs +4 -1
- package/templates/video/scripts/export-mp4.mjs +2 -0
- package/templates/video/scripts/html-export.mjs +12 -4
- package/templates/video/scripts/project-context.mjs +20 -3
- package/templates/video/scripts/quiz.d.mts +31 -0
- package/templates/video/scripts/quiz.mjs +126 -0
- package/templates/video/scripts/review.mjs +9 -1
- package/templates/video/scripts/story.d.mts +36 -0
- package/templates/video/scripts/story.mjs +139 -0
- package/templates/video/scripts/workspace-plugin.mjs +9 -4
- package/templates/video/shared/locales/en/player.json +29 -1
- package/templates/video/src/kit/index.ts +1 -0
- package/templates/video/src/kit/story.ts +19 -0
- package/templates/video/src/main.ts +6 -1
- package/templates/video/src/player-types.ts +6 -0
- package/templates/video/src/player.ts +213 -17
- package/templates/video/src/quiz.ts +315 -0
- package/templates/video/src/story.ts +112 -0
- package/templates/video/src/style.css +106 -3
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { validateStory, type StoryGraph, type StorySegment } from "../scripts/story.mjs";
|
|
2
|
+
import type { Branding } from "./player-types";
|
|
3
|
+
|
|
4
|
+
export type { StoryGraph, StorySegment };
|
|
5
|
+
export type StoryCopy = { theEnd: string; playAgain: string; back: string; choose: string };
|
|
6
|
+
/** What the panel over the picture shows: nothing, the segment's choices, or its ending. */
|
|
7
|
+
export type StoryView =
|
|
8
|
+
| { kind: "none" }
|
|
9
|
+
| { kind: "choices"; segment: StorySegment; picked: number | null; remaining: number | null }
|
|
10
|
+
| { kind: "ending"; segment: StorySegment; canGoBack: boolean };
|
|
11
|
+
|
|
12
|
+
/** The story's segments by id and by time; the graph is checked once, against the film's duration. */
|
|
13
|
+
export class StoryMap {
|
|
14
|
+
private readonly byId: Map<string, StorySegment>;
|
|
15
|
+
private readonly ordered: StorySegment[];
|
|
16
|
+
constructor(readonly graph: StoryGraph, duration: number) {
|
|
17
|
+
validateStory(graph, { duration });
|
|
18
|
+
this.byId = new Map(graph.segments.map((segment) => [segment.id, segment]));
|
|
19
|
+
this.ordered = [...graph.segments].sort((a, b) => a.start - b.start);
|
|
20
|
+
}
|
|
21
|
+
get first() { return this.get(this.graph.start); }
|
|
22
|
+
get(id: string) {
|
|
23
|
+
const segment = this.byId.get(id);
|
|
24
|
+
if (!segment) throw new Error(`The story has no segment ${id}.`);
|
|
25
|
+
return segment;
|
|
26
|
+
}
|
|
27
|
+
/** The segment playing at a time of the film's timeline; a boundary belongs to the segment that starts there. */
|
|
28
|
+
at(time: number) {
|
|
29
|
+
let found = this.ordered[0];
|
|
30
|
+
for (const segment of this.ordered) if (segment.start <= time + 1e-6) found = segment;
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
/** When a segment's choices appear. */
|
|
34
|
+
choiceAt(segment: StorySegment) { return segment.choiceAt ?? segment.end; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The choice and ending panels over the picture. They are HTML buttons, so a viewer can tab to them, a screen reader
|
|
39
|
+
* reads them and a touch picks them; 1–4 on the keyboard choose too. The panels are rebuilt only when what they show
|
|
40
|
+
* changes, so focus and hover survive every frame's update.
|
|
41
|
+
*/
|
|
42
|
+
export class StoryOverlay {
|
|
43
|
+
readonly element = document.createElement("div");
|
|
44
|
+
private readonly live = document.createElement("p");
|
|
45
|
+
private shown = "";
|
|
46
|
+
private timer: HTMLElement | null = null;
|
|
47
|
+
|
|
48
|
+
constructor(host: HTMLElement, graph: StoryGraph, private readonly copy: StoryCopy, branding: Branding | undefined,
|
|
49
|
+
private readonly actions: { choose(index: number): void; restart(): void; back(): void }) {
|
|
50
|
+
this.element.className = "story-layer";
|
|
51
|
+
const theme = graph.theme ?? {};
|
|
52
|
+
this.element.style.setProperty("--story-accent", theme.accent ?? branding?.colors.primary ?? "#2c6bd0");
|
|
53
|
+
this.element.style.setProperty("--story-surface", theme.surface ?? "#fffdfa");
|
|
54
|
+
this.element.style.setProperty("--story-text", theme.text ?? branding?.colors.text ?? "#16232a");
|
|
55
|
+
if (branding) this.element.style.setProperty("--story-font", branding.fonts.heading.family);
|
|
56
|
+
this.live.className = "story-live"; this.live.setAttribute("role", "status");
|
|
57
|
+
host.append(this.element, this.live);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
render(view: StoryView) {
|
|
61
|
+
const key = view.kind === "none" ? "none" : `${view.kind}:${view.segment.id}:${view.kind === "choices" ? view.picked : view.canGoBack}`;
|
|
62
|
+
if (key !== this.shown) { this.shown = key; this.build(view); }
|
|
63
|
+
if (view.kind === "choices" && this.timer) this.timer.style.transform = `scaleX(${Math.max(0, view.remaining ?? 0)})`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private build(view: StoryView) {
|
|
67
|
+
this.element.replaceChildren(); this.timer = null;
|
|
68
|
+
this.element.dataset.view = view.kind;
|
|
69
|
+
if (view.kind === "none") return;
|
|
70
|
+
const panel = document.createElement("div");
|
|
71
|
+
panel.className = `story-panel story-panel-${view.kind}`;
|
|
72
|
+
const button = (label: string, action: () => void, className = "") => {
|
|
73
|
+
const element = document.createElement("button");
|
|
74
|
+
element.type = "button"; element.className = className; element.textContent = label;
|
|
75
|
+
// A click on a choice is not a click on the picture, which would pause the film.
|
|
76
|
+
element.addEventListener("click", (event) => { event.stopPropagation(); action(); });
|
|
77
|
+
return element;
|
|
78
|
+
};
|
|
79
|
+
if (view.kind === "choices") {
|
|
80
|
+
const { segment, picked } = view;
|
|
81
|
+
panel.setAttribute("role", "group");
|
|
82
|
+
const prompt = segment.prompt ?? this.copy.choose;
|
|
83
|
+
panel.setAttribute("aria-label", prompt);
|
|
84
|
+
if (segment.prompt) {
|
|
85
|
+
const heading = document.createElement("p"); heading.className = "story-prompt"; heading.textContent = segment.prompt;
|
|
86
|
+
panel.append(heading);
|
|
87
|
+
}
|
|
88
|
+
if (segment.timeout !== undefined && picked === null) {
|
|
89
|
+
const track = document.createElement("div"); track.className = "story-timer"; track.setAttribute("aria-hidden", "true");
|
|
90
|
+
this.timer = document.createElement("span"); track.append(this.timer); panel.append(track);
|
|
91
|
+
}
|
|
92
|
+
const choices = document.createElement("div"); choices.className = "story-choices";
|
|
93
|
+
segment.choices!.forEach((choice, index) => {
|
|
94
|
+
const element = button(choice.label, () => this.actions.choose(index), picked === index ? "picked" : "");
|
|
95
|
+
element.dataset.key = String(index + 1);
|
|
96
|
+
if (picked !== null) { element.disabled = picked !== index; element.setAttribute("aria-pressed", String(picked === index)); }
|
|
97
|
+
choices.append(element);
|
|
98
|
+
});
|
|
99
|
+
panel.append(choices);
|
|
100
|
+
this.live.textContent = picked === null ? `${prompt} ${segment.choices!.map((choice, index) => `${index + 1}: ${choice.label}.`).join(" ")}` : segment.choices![picked].label;
|
|
101
|
+
} else {
|
|
102
|
+
const eyebrow = document.createElement("p"); eyebrow.className = "story-eyebrow"; eyebrow.textContent = this.copy.theEnd;
|
|
103
|
+
const title = document.createElement("h2"); title.className = "story-title"; title.textContent = view.segment.ending!;
|
|
104
|
+
const actions = document.createElement("div"); actions.className = "story-choices";
|
|
105
|
+
actions.append(button(this.copy.playAgain, this.actions.restart, "picked"));
|
|
106
|
+
if (view.canGoBack) actions.append(button(this.copy.back, this.actions.back));
|
|
107
|
+
panel.append(eyebrow, title, actions);
|
|
108
|
+
this.live.textContent = `${this.copy.theEnd}: ${view.segment.ending}`;
|
|
109
|
+
}
|
|
110
|
+
this.element.append(panel);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -15,7 +15,18 @@ button:focus-visible, a:focus-visible, input:focus-visible, summary:focus-visibl
|
|
|
15
15
|
main { max-width: 1380px; margin: auto; padding: 45px 48px 15px; }
|
|
16
16
|
.eyebrow { font-size: 10px; font-weight: 700; letter-spacing: 1.9px; margin: 0 0 12px; color: #667660; }
|
|
17
17
|
.specs { font: 10px monospace; color: #6a7668; white-space: nowrap; }
|
|
18
|
-
#theater { border-radius: 17px; overflow: hidden; border: 1px solid #d9ddcf; background: #f4f1e9; box-shadow: 0 16px 60px #1726240d; }
|
|
18
|
+
#theater { container: theater / inline-size; border-radius: 17px; overflow: hidden; border: 1px solid #d9ddcf; background: #f4f1e9; box-shadow: 0 16px 60px #1726240d; }
|
|
19
|
+
/* The page's player fits the window: its width follows the height left for the picture below the page header and above
|
|
20
|
+
the controls, so the picture and the controls show without scrolling and the timeline follows below. The player
|
|
21
|
+
measures that header and those controls (--player-above, --player-controls). The small viewport height keeps the
|
|
22
|
+
size steady while a phone's toolbars come and go. The picture keeps 240px of height where the window has it: a header
|
|
23
|
+
that leaves less scrolls away instead, as on a phone on its side. The player keeps a width its controls fit, and a
|
|
24
|
+
tall film narrower than that shows with bars at its sides, as on a phone. */
|
|
25
|
+
.fit-window { --player-room: calc(100vh - var(--player-controls) - 16px); }
|
|
26
|
+
@supports (height: 100svh) { .fit-window { --player-room: calc(100svh - var(--player-controls) - 16px); } }
|
|
27
|
+
.fit-window { --player-fit: max(var(--player-room) - var(--player-above), min(var(--player-room), 240px)); }
|
|
28
|
+
.fit-window #theater:not(.expanded):not(:fullscreen), .fit-window .player-note { min-width: min(100%, 360px); max-width: calc(var(--player-fit) * var(--film-aspect) + 2px); margin-inline: auto; }
|
|
29
|
+
.fit-window #theater:not(.expanded):not(:fullscreen) #canvas-host { max-height: var(--player-fit); }
|
|
19
30
|
#canvas-host { width: 100%; position: relative; aspect-ratio: 16 / 9; }
|
|
20
31
|
#canvas-host canvas { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
21
32
|
#loading { position: absolute; inset: 45% 0 auto; text-align: center; color: #788073; }
|
|
@@ -33,7 +44,7 @@ main { max-width: 1380px; margin: auto; padding: 45px 48px 15px; }
|
|
|
33
44
|
#download-menu button { display: flex; align-items: center; justify-content: space-between; gap: 14px; width: 100%; padding: 12px 10px; text-align: left; font-size: 12px; }
|
|
34
45
|
#download-menu small { color: #77836f; font-size: 10px; }
|
|
35
46
|
#captions[aria-pressed="true"] { background: #e7ecdc; font-weight: 700; }
|
|
36
|
-
.player-note { display: flex; justify-content: space-between; gap: 15px; color: #7b8174; font-size: 10px; margin: 14px 2px 0; }
|
|
47
|
+
.player-note { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px 15px; color: #7b8174; font-size: 10px; margin: 14px 2px 0; }
|
|
37
48
|
.preview-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; }
|
|
38
49
|
#status { color: #9a391c; font-size: 13px; margin: 10px 0; }
|
|
39
50
|
.resource-timeline { min-width: 0; margin: 32px 0; border: 1px solid #daddcf; border-radius: 12px; overflow: hidden; background: #fbf9f3; }
|
|
@@ -108,12 +119,104 @@ body:not(.capture) #canvas-host canvas { cursor: pointer; }
|
|
|
108
119
|
@media (max-width: 750px) {
|
|
109
120
|
.masthead { padding: 20px; }.brand span { display: none; }.brand strong { font-size: 20px; }.template { font-size: 9px; padding: 8px 10px; }
|
|
110
121
|
main { padding: 30px 20px 10px; }
|
|
111
|
-
.transport { flex-wrap: wrap; gap: 12px; padding: 12px; }.tools { justify-content: space-between; width: 100%; }.tools button { min-height: 36px; }
|
|
112
122
|
.player-note { flex-direction: column; gap: 6px; font-size: 9px; }
|
|
113
123
|
.timeline-toolbar { flex-wrap: wrap; padding: 13px; }.timeline-zoom { margin-left: auto; }.timeline-clock { margin-right: 4px; }
|
|
114
124
|
footer { padding: 25px 20px; }footer span:last-child { display: none; }
|
|
115
125
|
}
|
|
126
|
+
/* A short window, such as a phone on its side, keeps more of its height for the picture. */
|
|
127
|
+
@media (max-height: 560px) { .masthead { padding-block: 12px; } main { padding-top: 16px; } }
|
|
128
|
+
/* The transport wraps by the player's width, which a tall film keeps narrow on a wide screen too. */
|
|
129
|
+
@container theater (max-width: 600px) {
|
|
130
|
+
.transport { flex-wrap: wrap; gap: 12px; padding: 12px; }.tools { justify-content: space-between; width: 100%; }.tools button { min-height: 36px; }
|
|
131
|
+
}
|
|
116
132
|
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto; } }
|
|
117
133
|
|
|
118
134
|
#audio-status { margin: 0; padding: 9px 16px; font-size: 12px; color: #8b2f25; background: #fff1eb; }
|
|
119
135
|
.capture #audio-status { display: none; }
|
|
136
|
+
|
|
137
|
+
/* A branching story's choice and ending panels, over the picture. Sizes follow the player's width (cqi), so the panels
|
|
138
|
+
keep their proportions from a phone to full screen; the minimums keep text readable and buttons easy to tap. */
|
|
139
|
+
#canvas-host { container-type: inline-size; }
|
|
140
|
+
.story-layer { position: absolute; inset: 0; z-index: 2; display: flex; align-items: flex-end; justify-content: center; pointer-events: none; font-family: var(--story-font, inherit); }
|
|
141
|
+
.story-layer[data-view="choices"] { background: linear-gradient(to top, rgb(8 12 11 / .66), rgb(8 12 11 / 0) 52%); }
|
|
142
|
+
.story-layer[data-view="ending"] { align-items: center; background: rgb(8 12 11 / .62); pointer-events: auto; }
|
|
143
|
+
.story-panel { pointer-events: auto; width: min(94%, 70cqi); padding: 0 0 4.5cqi; display: grid; justify-items: center; gap: 1.5cqi; text-align: center; animation: story-in .35s ease-out both; }
|
|
144
|
+
.story-panel-ending { padding: 0; }
|
|
145
|
+
.story-prompt { margin: 0; color: #fff; font-size: clamp(15px, 2.6cqi, 42px); font-weight: 700; line-height: 1.15; text-shadow: 0 2px 14px rgb(0 0 0 / .5); }
|
|
146
|
+
.story-timer { width: min(100%, 34cqi); height: max(3px, .35cqi); border-radius: 9px; background: rgb(255 255 255 / .3); overflow: hidden; }
|
|
147
|
+
.story-timer span { display: block; height: 100%; background: var(--story-accent); transform-origin: left; }
|
|
148
|
+
.story-choices { display: flex; flex-wrap: wrap; justify-content: center; gap: max(8px, 1.2cqi); width: 100%; }
|
|
149
|
+
.story-choices button { display: flex; align-items: center; justify-content: center; gap: .7em; flex: 1 1 0; min-width: min(100%, 18cqi); max-width: 33cqi; min-height: 44px; padding: 1.1cqi 2.2cqi; border: 2px solid transparent; border-radius: 999px; background: var(--story-surface); color: var(--story-text); font: inherit; font-size: clamp(13px, 1.8cqi, 30px); font-weight: 700; line-height: 1.2; box-shadow: 0 10px 30px -12px rgb(0 0 0 / .65); transition: transform .15s, border-color .15s; }
|
|
150
|
+
.story-choices button::before { content: attr(data-key); display: grid; place-items: center; flex: none; width: 1.6em; height: 1.6em; border-radius: 50%; background: color-mix(in srgb, var(--story-accent) 16%, transparent); color: var(--story-accent); font-size: .72em; }
|
|
151
|
+
.story-panel-ending .story-choices button::before { content: none; }
|
|
152
|
+
.story-choices button:hover:enabled, .story-choices button:focus-visible { transform: translateY(-2px); border-color: var(--story-accent); }
|
|
153
|
+
/* A picked choice keeps the story's text color on a tint of the accent, readable whatever the accent is. */
|
|
154
|
+
.story-choices button.picked { border-color: var(--story-accent); background: color-mix(in srgb, var(--story-accent) 24%, var(--story-surface)); }
|
|
155
|
+
.story-choices button.picked::before { background: var(--story-accent); color: var(--story-surface); }
|
|
156
|
+
.story-eyebrow { margin: 0; color: rgb(255 255 255 / .78); font-size: clamp(11px, 1.3cqi, 20px); font-weight: 700; letter-spacing: .22em; text-transform: uppercase; }
|
|
157
|
+
.story-title { margin: 0 0 1cqi; color: #fff; font-size: clamp(22px, 4.8cqi, 78px); line-height: 1.05; text-wrap: balance; }
|
|
158
|
+
.story-live { position: absolute; width: 1px; height: 1px; margin: 0; overflow: hidden; clip-path: inset(50%); }
|
|
159
|
+
@keyframes story-in { from { opacity: 0; transform: translateY(12px); } }
|
|
160
|
+
@container (max-width: 560px) { .story-panel { width: 96%; padding-bottom: 10px; gap: 8px; } .story-choices button { max-width: none; } }
|
|
161
|
+
@media (prefers-reduced-motion: reduce) { .story-panel { animation: none; } .story-choices button { transition: none; } }
|
|
162
|
+
.capture .story-layer { display: none; }
|
|
163
|
+
/* A quiz's question and score cards, centered over the dimmed picture. Sizes follow the player's width (cqi) like the
|
|
164
|
+
story panels; a card taller than a small player scrolls inside it. Right and wrong carry an icon as well as a color. */
|
|
165
|
+
.quiz-layer { --quiz-right: #1d8a4e; --quiz-wrong: #c8412f; position: absolute; inset: 0; z-index: 3; display: grid; overflow-y: auto; overscroll-behavior: contain; padding: clamp(8px, 2.4cqi, 40px); box-sizing: border-box; background: rgb(8 12 11 / .6); font-family: var(--quiz-font, inherit); }
|
|
166
|
+
.quiz-layer[hidden] { display: none; }
|
|
167
|
+
.quiz-card { margin: auto; width: min(100%, 64cqi); box-sizing: border-box; display: grid; gap: clamp(8px, 1.25cqi, 22px); padding: clamp(14px, 2.7cqi, 48px); border-radius: clamp(14px, 1.7cqi, 30px); background: var(--quiz-surface); color: var(--quiz-text); box-shadow: 0 30px 80px -30px rgb(0 0 0 / .75); }
|
|
168
|
+
.quiz-card-in { animation: quiz-in .3s ease-out both; }
|
|
169
|
+
.quiz-eyebrow { margin: 0; color: color-mix(in srgb, var(--quiz-accent) 72%, var(--quiz-text)); font-size: clamp(11px, 1.15cqi, 19px); font-weight: 800; letter-spacing: .14em; text-transform: uppercase; }
|
|
170
|
+
.quiz-prompt { margin: 0; font-size: clamp(16px, 2.45cqi, 42px); font-weight: 800; line-height: 1.18; text-wrap: balance; }
|
|
171
|
+
.quiz-hint { margin: -.2em 0 0; font-size: clamp(12px, 1.3cqi, 21px); opacity: .72; }
|
|
172
|
+
.quiz-options { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(6px, .95cqi, 16px); }
|
|
173
|
+
.quiz-options.quiz-long, .quiz-options.quiz-odd { grid-template-columns: 1fr; }
|
|
174
|
+
.quiz-option { display: flex; align-items: center; gap: .7em; min-height: 44px; padding: clamp(8px, 1cqi, 18px) clamp(10px, 1.35cqi, 24px); border: 2px solid color-mix(in srgb, var(--quiz-text) 14%, transparent); border-radius: clamp(10px, 1.1cqi, 18px); background: color-mix(in srgb, var(--quiz-text) 3%, var(--quiz-surface)); color: inherit; font: inherit; font-size: clamp(13px, 1.6cqi, 28px); font-weight: 650; line-height: 1.25; text-align: left; cursor: pointer; transition: border-color .15s, background-color .15s, transform .15s; }
|
|
175
|
+
.quiz-option:hover:enabled, .quiz-option:focus-visible { border-color: var(--quiz-accent); transform: translateY(-1px); }
|
|
176
|
+
.quiz-option:focus-visible, .quiz-actions button:focus-visible { outline: 3px solid color-mix(in srgb, var(--quiz-accent) 50%, transparent); outline-offset: 2px; }
|
|
177
|
+
.quiz-option:disabled { cursor: default; opacity: 1; }
|
|
178
|
+
.quiz-key { flex: none; display: grid; place-items: center; width: 1.75em; height: 1.75em; border-radius: 50%; background: color-mix(in srgb, var(--quiz-accent) 18%, transparent); color: color-mix(in srgb, var(--quiz-accent) 62%, var(--quiz-text)); font-size: .78em; font-weight: 800; }
|
|
179
|
+
.quiz-multi .quiz-key { border-radius: 28%; }
|
|
180
|
+
.quiz-option-text { flex: 1; min-width: 0; }
|
|
181
|
+
.quiz-option.selected { border-color: var(--quiz-accent); background: color-mix(in srgb, var(--quiz-accent) 15%, var(--quiz-surface)); }
|
|
182
|
+
.quiz-option.selected .quiz-key { background: var(--quiz-accent); color: var(--quiz-on-accent); }
|
|
183
|
+
.quiz-option.correct { border-color: var(--quiz-right); background: color-mix(in srgb, var(--quiz-right) 13%, var(--quiz-surface)); }
|
|
184
|
+
.quiz-option.wrong { border-color: var(--quiz-wrong); background: color-mix(in srgb, var(--quiz-wrong) 11%, var(--quiz-surface)); }
|
|
185
|
+
.quiz-option.missed { border-style: dashed; }
|
|
186
|
+
.quiz-option.correct .quiz-key { background: var(--quiz-right); color: #fff; }
|
|
187
|
+
.quiz-option.wrong .quiz-key { background: var(--quiz-wrong); color: #fff; }
|
|
188
|
+
.quiz-option.dim { opacity: .5; }
|
|
189
|
+
.quiz-icon { display: grid; place-items: center; width: 100%; height: 100%; }
|
|
190
|
+
.quiz-icon svg { width: 62%; height: 62%; }
|
|
191
|
+
.quiz-feedback { margin: 0; padding: clamp(8px, 1.05cqi, 18px) clamp(10px, 1.35cqi, 22px); border-left: 4px solid var(--quiz-right); border-radius: clamp(8px, .9cqi, 14px); background: color-mix(in srgb, var(--quiz-right) 11%, var(--quiz-surface)); font-size: clamp(13px, 1.45cqi, 25px); line-height: 1.42; }
|
|
192
|
+
.quiz-feedback.wrong { border-left-color: var(--quiz-wrong); background: color-mix(in srgb, var(--quiz-wrong) 9%, var(--quiz-surface)); }
|
|
193
|
+
.quiz-answer { font-weight: 700; }
|
|
194
|
+
.quiz-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: clamp(6px, 1cqi, 14px); }
|
|
195
|
+
.quiz-actions button { min-height: 44px; padding: 0 clamp(14px, 2cqi, 34px); border-radius: 999px; font: inherit; font-size: clamp(13px, 1.45cqi, 25px); font-weight: 750; cursor: pointer; transition: transform .15s, filter .15s; }
|
|
196
|
+
.quiz-actions button:hover:enabled { transform: translateY(-1px); filter: brightness(1.06); }
|
|
197
|
+
.quiz-actions button:disabled { opacity: .45; cursor: default; }
|
|
198
|
+
.quiz-primary { border: 2px solid var(--quiz-accent); background: var(--quiz-accent); color: var(--quiz-on-accent); }
|
|
199
|
+
.quiz-secondary { border: 2px solid color-mix(in srgb, var(--quiz-text) 24%, transparent); background: transparent; color: inherit; }
|
|
200
|
+
.quiz-results { justify-items: center; text-align: center; }
|
|
201
|
+
.quiz-results .quiz-actions { justify-content: center; }
|
|
202
|
+
.quiz-score { margin: 0; color: color-mix(in srgb, var(--quiz-accent) 75%, var(--quiz-text)); font-size: clamp(38px, 7cqi, 124px); font-weight: 900; line-height: 1; }
|
|
203
|
+
.quiz-score span { font-size: .42em; opacity: .62; }
|
|
204
|
+
.quiz-verdict { margin: 0; font-size: clamp(13px, 1.6cqi, 27px); font-weight: 750; }
|
|
205
|
+
.quiz-verdict.passed { color: color-mix(in srgb, var(--quiz-right) 80%, var(--quiz-text)); }
|
|
206
|
+
.quiz-marks { display: flex; flex-wrap: wrap; justify-content: center; gap: clamp(4px, .6cqi, 10px); margin: 0; padding: 0; list-style: none; }
|
|
207
|
+
.quiz-marks li { width: clamp(22px, 2.5cqi, 42px); height: clamp(22px, 2.5cqi, 42px); border-radius: 50%; background: var(--quiz-right); color: #fff; }
|
|
208
|
+
.quiz-marks li.wrong { background: var(--quiz-wrong); }
|
|
209
|
+
.quiz-marks li.skipped { background: color-mix(in srgb, var(--quiz-text) 22%, transparent); color: var(--quiz-text); }
|
|
210
|
+
@keyframes quiz-in { from { opacity: 0; transform: translateY(10px) scale(.985); } }
|
|
211
|
+
@container (max-width: 560px) {
|
|
212
|
+
.quiz-layer { padding: 6px; }
|
|
213
|
+
.quiz-card { width: 100%; padding: 12px; gap: 8px; border-radius: 12px; }
|
|
214
|
+
.quiz-options, .quiz-options.quiz-odd { grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
215
|
+
/* A small player keeps the answer in the feedback's words and the next step in reach. */
|
|
216
|
+
.quiz-card.answered .quiz-options { display: none; }
|
|
217
|
+
.quiz-actions { position: sticky; bottom: -12px; margin: 0 -12px -12px; padding: 8px 12px 12px; background: var(--quiz-surface); border-radius: 0 0 12px 12px; }
|
|
218
|
+
.quiz-option { min-height: 40px; padding: 6px 8px; font-size: 12.5px; }
|
|
219
|
+
.quiz-feedback { font-size: 12.5px; padding: 7px 9px; }
|
|
220
|
+
.quiz-actions button { min-height: 40px; padding: 0 14px; font-size: 13px; }
|
|
221
|
+
}
|
|
222
|
+
@media (prefers-reduced-motion: reduce) { .quiz-card-in { animation: none; } .quiz-option, .quiz-actions button { transition: none; } }
|