Spaces:
Sleeping
Sleeping
Deploy Epic Errands UI refresh
Browse files- .gitattributes +22 -0
- README.md +2 -2
- epic_errands_space/quest_generation.py +231 -59
- epic_errands_space/render.py +37 -22
- frontend/assets/demo-placeholders/placeholder-boy-720.png +3 -0
- frontend/assets/demo-placeholders/placeholder-female-parent-720.png +3 -0
- frontend/assets/demo-placeholders/placeholder-girl-720.png +3 -0
- frontend/assets/demo-placeholders/placeholder-male-parent-720.png +3 -0
- frontend/assets/generated-hero-manifest.json +0 -0
- frontend/assets/generated-v2/clean-room-classroom-c5cb506427.png +3 -0
- frontend/assets/generated-v2/clean-room-classroom-cf6f43d6ce.wav +3 -0
- frontend/assets/generated-v2/clean-room-comic-88b770a8dc.wav +3 -0
- frontend/assets/generated-v2/clean-room-comic-9bce2b2b21.png +3 -0
- frontend/assets/generated-v2/clean-room-questbook-01493f395c.wav +3 -0
- frontend/assets/generated-v2/clean-room-questbook-d35e6ee10c.png +3 -0
- frontend/assets/generated-v2/project-outline-classroom-3783f10bae.png +3 -0
- frontend/assets/generated-v2/project-outline-classroom-70a886be5a.wav +3 -0
- frontend/assets/generated-v2/project-outline-comic-4f9eaa443a.png +3 -0
- frontend/assets/generated-v2/project-outline-comic-ef3f180846.wav +3 -0
- frontend/assets/generated-v2/project-outline-questbook-bddf03af1a.wav +3 -0
- frontend/assets/generated-v2/project-outline-questbook-e1d5ced2e8.png +3 -0
- frontend/assets/generated-v2/read-20-classroom-7c13f43782.png +3 -0
- frontend/assets/generated-v2/read-20-classroom-cca67b8288.wav +3 -0
- frontend/assets/generated-v2/read-20-comic-7ec8c91bf8.wav +3 -0
- frontend/assets/generated-v2/read-20-comic-b5ae2d561a.png +3 -0
- frontend/assets/generated-v2/read-20-questbook-39aac8ba25.png +3 -0
- frontend/assets/generated-v2/read-20-questbook-5805518930.wav +3 -0
- frontend/index.html +27 -4
- frontend/scripts/app.js +315 -387
- frontend/styles/app.css +189 -801
- frontend/styles/components.css +350 -174
- frontend/styles/tokens.css +323 -52
.gitattributes
CHANGED
|
@@ -47,3 +47,25 @@ frontend/assets/generated-audio-3-read-for-20-minutes-5e944b16.wav filter=lfs di
|
|
| 47 |
frontend/assets/generated-hero-1-clean-up-my-room-before-dinner-07b300bc.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
frontend/assets/generated-hero-2-finish-my-class-project-outline-4a8a06bc.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
frontend/assets/generated-hero-3-read-for-20-minutes-3781199a.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
frontend/assets/generated-hero-1-clean-up-my-room-before-dinner-07b300bc.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
frontend/assets/generated-hero-2-finish-my-class-project-outline-4a8a06bc.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
frontend/assets/generated-hero-3-read-for-20-minutes-3781199a.png filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
frontend/assets/demo-placeholders/placeholder-boy-720.png filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
frontend/assets/demo-placeholders/placeholder-female-parent-720.png filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
frontend/assets/demo-placeholders/placeholder-girl-720.png filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
frontend/assets/demo-placeholders/placeholder-male-parent-720.png filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
frontend/assets/generated-v2/clean-room-classroom-c5cb506427.png filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
frontend/assets/generated-v2/clean-room-classroom-cf6f43d6ce.wav filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
frontend/assets/generated-v2/clean-room-comic-88b770a8dc.wav filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
frontend/assets/generated-v2/clean-room-comic-9bce2b2b21.png filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
frontend/assets/generated-v2/clean-room-questbook-01493f395c.wav filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
frontend/assets/generated-v2/clean-room-questbook-d35e6ee10c.png filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
frontend/assets/generated-v2/project-outline-classroom-3783f10bae.png filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
frontend/assets/generated-v2/project-outline-classroom-70a886be5a.wav filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
frontend/assets/generated-v2/project-outline-comic-4f9eaa443a.png filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
frontend/assets/generated-v2/project-outline-comic-ef3f180846.wav filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
frontend/assets/generated-v2/project-outline-questbook-bddf03af1a.wav filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
frontend/assets/generated-v2/project-outline-questbook-e1d5ced2e8.png filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
frontend/assets/generated-v2/read-20-classroom-7c13f43782.png filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
frontend/assets/generated-v2/read-20-classroom-cca67b8288.wav filter=lfs diff=lfs merge=lfs -text
|
| 68 |
+
frontend/assets/generated-v2/read-20-comic-7ec8c91bf8.wav filter=lfs diff=lfs merge=lfs -text
|
| 69 |
+
frontend/assets/generated-v2/read-20-comic-b5ae2d561a.png filter=lfs diff=lfs merge=lfs -text
|
| 70 |
+
frontend/assets/generated-v2/read-20-questbook-39aac8ba25.png filter=lfs diff=lfs merge=lfs -text
|
| 71 |
+
frontend/assets/generated-v2/read-20-questbook-5805518930.wav filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -21,8 +21,8 @@ The prototype includes:
|
|
| 21 |
|
| 22 |
- Home, Parent Goals, Kid Goals, and disabled DIY Lab navigation.
|
| 23 |
- Quest, Classroom, and Comic app theme switching.
|
| 24 |
-
-
|
| 25 |
-
only
|
| 26 |
- Theme-based parent/kid titles such as Quest Keeper and Young Adventurer.
|
| 27 |
- Kid completion state separated from parent approval/reward state.
|
| 28 |
- Parent approve-only reward flow.
|
|
|
|
| 21 |
|
| 22 |
- Home, Parent Goals, Kid Goals, and disabled DIY Lab navigation.
|
| 23 |
- Quest, Classroom, and Comic app theme switching.
|
| 24 |
+
- Questbook is the default visible theme; the canonical internal style id is
|
| 25 |
+
`questbook`, with legacy `magical` accepted only as a compatibility alias.
|
| 26 |
- Theme-based parent/kid titles such as Quest Keeper and Young Adventurer.
|
| 27 |
- Kid completion state separated from parent approval/reward state.
|
| 28 |
- Parent approve-only reward flow.
|
epic_errands_space/quest_generation.py
CHANGED
|
@@ -27,8 +27,9 @@ VOXCPM2_MODEL_ID = "openbmb/VoxCPM2"
|
|
| 27 |
NEMOTRON_SPEECH_MODEL_ID = "nvidia/nemotron-speech-streaming-en-0.6b"
|
| 28 |
|
| 29 |
THEME_ALIASES = {
|
| 30 |
-
"quest": "
|
| 31 |
-
"
|
|
|
|
| 32 |
"class": "classroom",
|
| 33 |
"classroom": "classroom",
|
| 34 |
"comic": "comic",
|
|
@@ -66,7 +67,7 @@ def default_goal_payload() -> list[dict[str, str]]:
|
|
| 66 |
return [{"id": goal.id, "text": goal.text} for goal in DEFAULT_GOALS]
|
| 67 |
|
| 68 |
|
| 69 |
-
def _theme(value: Any, fallback: str = "
|
| 70 |
normalized = str(value or fallback).strip().lower()
|
| 71 |
return THEME_ALIASES.get(normalized, fallback)
|
| 72 |
|
|
@@ -83,12 +84,12 @@ def normalize_goals(raw_goals: Any) -> list[dict[str, str]]:
|
|
| 83 |
goals.append({
|
| 84 |
"id": str(item.get("id") or f"goal-{index + 1}"),
|
| 85 |
"text": text,
|
| 86 |
-
"theme": _theme(item.get("theme") or item.get("style_id"), "
|
| 87 |
})
|
| 88 |
if len(goals) == 3:
|
| 89 |
break
|
| 90 |
|
| 91 |
-
fallback_themes = ["
|
| 92 |
for fallback_index, fallback in enumerate(default_goal_payload()):
|
| 93 |
if len(goals) == 3:
|
| 94 |
break
|
|
@@ -144,7 +145,7 @@ def _step_key(goal_text: str) -> str:
|
|
| 144 |
return "default"
|
| 145 |
|
| 146 |
|
| 147 |
-
def _quest_title(goal_text: str, tone: str, theme: str = "
|
| 148 |
lowered = goal_text.lower()
|
| 149 |
if theme == "classroom":
|
| 150 |
noun = "Quest"
|
|
@@ -155,7 +156,7 @@ def _quest_title(goal_text: str, tone: str, theme: str = "magical") -> str:
|
|
| 155 |
tone_word = {
|
| 156 |
"cozy": "Cozy",
|
| 157 |
"brave": "Brave",
|
| 158 |
-
"
|
| 159 |
"silly": "Silly",
|
| 160 |
"epic": "Epic",
|
| 161 |
}.get(tone, "Cozy")
|
|
@@ -178,9 +179,9 @@ def _why_it_helps(goal_text: str) -> str:
|
|
| 178 |
|
| 179 |
|
| 180 |
def _prompt_for_goal(goal: dict[str, str], tone: str) -> str:
|
| 181 |
-
theme = _theme(goal.get("theme"), "
|
| 182 |
visual_direction = {
|
| 183 |
-
"
|
| 184 |
"premium storybook quest card, warm paper, gentle fantasy props, "
|
| 185 |
"earned crest, no monsters or threats"
|
| 186 |
),
|
|
@@ -202,6 +203,117 @@ def _prompt_for_goal(goal: dict[str, str], tone: str) -> str:
|
|
| 202 |
)
|
| 203 |
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
def _fallback_image_result(goal: dict[str, str], index: int, reason: str) -> dict[str, Any]:
|
| 206 |
return {
|
| 207 |
"goal_id": goal["id"],
|
|
@@ -210,6 +322,7 @@ def _fallback_image_result(goal: dict[str, str], index: int, reason: str) -> dic
|
|
| 210 |
"hero_src": _asset_to_data_uri(FALLBACK_HERO),
|
| 211 |
"origin": "deterministic_fallback",
|
| 212 |
"prompt_summary": f"Fallback classroom adventure image for goal: {goal['text']}",
|
|
|
|
| 213 |
"runtime_metadata": _runtime_metadata(
|
| 214 |
fallback_used=True,
|
| 215 |
fallback_reason=reason,
|
|
@@ -218,21 +331,19 @@ def _fallback_image_result(goal: dict[str, str], index: int, reason: str) -> dic
|
|
| 218 |
|
| 219 |
|
| 220 |
def _fallback_card_text(goal: dict[str, str], tone: str, reason: str) -> dict[str, Any]:
|
| 221 |
-
steps = [label for label, _icon in STEP_LIBRARY[_step_key(goal["text"])]][:3]
|
| 222 |
return {
|
| 223 |
"goal_id": goal["id"],
|
| 224 |
"ordinary_goal": goal["text"],
|
| 225 |
-
"theme": _theme(goal.get("theme"), "
|
| 226 |
"model_key": "deterministic_fallback",
|
| 227 |
"model_id": "none",
|
| 228 |
"schema_valid": True,
|
| 229 |
"raw_text": "",
|
|
|
|
| 230 |
"card_text": {
|
| 231 |
-
"title": _quest_title(goal["text"], tone, _theme(goal.get("theme"), "
|
| 232 |
"narration": f"Your goal card is ready: {goal['text']}. Take it one small step at a time.",
|
| 233 |
-
"steps": steps,
|
| 234 |
"reward_label": "Quest Hero",
|
| 235 |
-
"completion_check": "Done when the ordinary goal is truly finished.",
|
| 236 |
},
|
| 237 |
"runtime_metadata": _runtime_metadata(fallback_used=True, fallback_reason=reason),
|
| 238 |
}
|
|
@@ -242,10 +353,11 @@ def _fallback_audio_result(goal: dict[str, str], index: int, reason: str) -> dic
|
|
| 242 |
return {
|
| 243 |
"goal_id": goal["id"],
|
| 244 |
"ordinary_goal": goal["text"],
|
| 245 |
-
"theme": _theme(goal.get("theme"), "
|
| 246 |
"audio_asset_name": "",
|
| 247 |
"audio_src": "",
|
| 248 |
"spoken_text": "",
|
|
|
|
| 249 |
"runtime_metadata": _runtime_metadata(
|
| 250 |
model_backend="modal_http",
|
| 251 |
model_id=VOXCPM2_MODEL_ID,
|
|
@@ -310,7 +422,11 @@ def _cached_images_for(goals: list[dict[str, str]]) -> list[dict[str, Any]] | No
|
|
| 310 |
return results
|
| 311 |
|
| 312 |
|
| 313 |
-
def _call_modal(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
base_url = os.environ.get("EPIC_MODAL_IMAGE_URL", "").strip() or os.environ.get("APP_MODAL_BASE_URL", "").strip()
|
| 315 |
token = os.environ.get("APP_MODAL_AUTH_TOKEN", "").strip()
|
| 316 |
timeout = float(os.environ.get("APP_MODAL_TIMEOUT_SECONDS", "180"))
|
|
@@ -322,8 +438,9 @@ def _call_modal(goals: list[dict[str, str]], tone: str) -> tuple[list[dict[str,
|
|
| 322 |
{
|
| 323 |
"goal_id": goal["id"],
|
| 324 |
"ordinary_goal": goal["text"],
|
| 325 |
-
"theme": _theme(goal.get("theme"), "
|
| 326 |
-
"
|
|
|
|
| 327 |
"seed": 26000 + index,
|
| 328 |
}
|
| 329 |
for index, goal in enumerate(goals)
|
|
@@ -380,6 +497,7 @@ def _call_modal(goals: list[dict[str, str]], tone: str) -> tuple[list[dict[str,
|
|
| 380 |
"hero_src": f"data:image/png;base64,{base64.b64encode(image_bytes).decode('ascii')}",
|
| 381 |
"origin": "modal_flux_generated",
|
| 382 |
"prompt_summary": output.get("prompt_summary", _prompt_for_goal(goal, tone)),
|
|
|
|
| 383 |
"runtime_metadata": metadata,
|
| 384 |
"seed": output.get("seed", 26000 + index),
|
| 385 |
})
|
|
@@ -418,7 +536,8 @@ def _call_text_model(goals: list[dict[str, str]], model_key: str) -> tuple[list[
|
|
| 418 |
{
|
| 419 |
"goal_id": goal["id"],
|
| 420 |
"ordinary_goal": goal["text"],
|
| 421 |
-
"theme": _theme(goal.get("theme"), "
|
|
|
|
| 422 |
}
|
| 423 |
for goal in goals
|
| 424 |
],
|
|
@@ -458,8 +577,10 @@ def _call_voxcpm2(goals: list[dict[str, str]], card_texts: dict[str, dict[str, A
|
|
| 458 |
{
|
| 459 |
"goal_id": goal["id"],
|
| 460 |
"ordinary_goal": goal["text"],
|
| 461 |
-
"theme": _theme(goal.get("theme"), "
|
| 462 |
-
"
|
|
|
|
|
|
|
| 463 |
}
|
| 464 |
for goal in goals
|
| 465 |
],
|
|
@@ -497,11 +618,12 @@ def _call_voxcpm2(goals: list[dict[str, str]], card_texts: dict[str, dict[str, A
|
|
| 497 |
saved.append({
|
| 498 |
"goal_id": goal["id"],
|
| 499 |
"ordinary_goal": goal["text"],
|
| 500 |
-
"theme": _theme(goal.get("theme"), "
|
| 501 |
"audio_asset_name": asset_name,
|
| 502 |
"audio_src": f"data:audio/wav;base64,{base64.b64encode(audio_bytes).decode('ascii')}",
|
| 503 |
"audio_wav_base64": audio_b64,
|
| 504 |
"spoken_text": output.get("spoken_text", ""),
|
|
|
|
| 505 |
"sample_rate": output.get("sample_rate"),
|
| 506 |
"runtime_metadata": metadata,
|
| 507 |
})
|
|
@@ -557,19 +679,11 @@ def _quest_for_goal(
|
|
| 557 |
audio_results: dict[str, dict[str, Any]] | None = None,
|
| 558 |
asr_results: dict[str, dict[str, Any]] | None = None,
|
| 559 |
) -> dict[str, Any]:
|
| 560 |
-
steps = [
|
| 561 |
-
|
| 562 |
-
for step_index, (label, icon) in enumerate(STEP_LIBRARY[_step_key(goal["text"])])
|
| 563 |
-
]
|
| 564 |
-
theme = _theme(goal.get("theme"), "magical")
|
| 565 |
primary_text = (text_results or {}).get(goal["id"], {})
|
| 566 |
secondary_text = (secondary_text_results or {}).get(goal["id"], {})
|
| 567 |
card_text = primary_text.get("card_text") or _fallback_card_text(goal, tone, "Primary text model did not return card_text.")["card_text"]
|
| 568 |
-
if isinstance(card_text.get("steps"), list) and card_text["steps"]:
|
| 569 |
-
steps = [
|
| 570 |
-
{"id": f"{goal['id']}-step-{step_index + 1}", "label": str(label), "icon": "flag"}
|
| 571 |
-
for step_index, label in enumerate(card_text["steps"][:4])
|
| 572 |
-
]
|
| 573 |
audio = (audio_results or {}).get(goal["id"], {})
|
| 574 |
asr = (asr_results or {}).get(goal["id"], {})
|
| 575 |
return {
|
|
@@ -584,13 +698,22 @@ def _quest_for_goal(
|
|
| 584 |
"hero_alt": f"Storybook classroom adventure for {goal['text']}",
|
| 585 |
"reward": {
|
| 586 |
"label": str(card_text.get("reward_label") or (["Idea Builder", "Lantern Reader", "Tidy Hero"][index] if index < 3 else "Quest Hero")),
|
| 587 |
-
"description":
|
| 588 |
},
|
| 589 |
"steps": steps,
|
| 590 |
"narration": str(card_text.get("narration") or ""),
|
| 591 |
"why_it_helps": _why_it_helps(goal["text"]),
|
| 592 |
"image_origin": image_result.get("origin", "unknown"),
|
| 593 |
"prompt_summary": image_result.get("prompt_summary", ""),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
"runtime_metadata": image_result.get("runtime_metadata", _runtime_metadata()),
|
| 595 |
"text_models": {
|
| 596 |
"primary": primary_text,
|
|
@@ -605,32 +728,57 @@ def _quest_for_goal(
|
|
| 605 |
}
|
| 606 |
|
| 607 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
def build_quest_pack(payload: dict[str, Any] | None = None, *, use_cache: bool = True) -> dict[str, Any]:
|
| 609 |
payload = payload or {}
|
| 610 |
tone = str(payload.get("tone") or "cozy").strip().lower() or "cozy"
|
| 611 |
goals = normalize_goals(payload.get("goals"))
|
| 612 |
force_refresh = bool(payload.get("force_refresh"))
|
| 613 |
|
| 614 |
-
images: list[dict[str, Any]] | None = None
|
| 615 |
-
metadata = _runtime_metadata()
|
| 616 |
-
blocker = ""
|
| 617 |
-
if use_cache and not force_refresh:
|
| 618 |
-
images = _cached_images_for(goals)
|
| 619 |
-
if images:
|
| 620 |
-
metadata = dict(images[0].get("runtime_metadata", metadata))
|
| 621 |
-
metadata["fallback_used"] = any(
|
| 622 |
-
bool(item.get("runtime_metadata", {}).get("fallback_used", True))
|
| 623 |
-
for item in images
|
| 624 |
-
)
|
| 625 |
-
|
| 626 |
-
if images is None:
|
| 627 |
-
try:
|
| 628 |
-
images, metadata = _call_modal(goals, tone)
|
| 629 |
-
except (OSError, RuntimeError, ValueError, urllib.error.URLError, TimeoutError) as exc:
|
| 630 |
-
blocker = f"{type(exc).__name__}: {exc}"
|
| 631 |
-
images = [_fallback_image_result(goal, index, blocker) for index, goal in enumerate(goals)]
|
| 632 |
-
metadata = _runtime_metadata(fallback_used=True, fallback_reason=blocker)
|
| 633 |
-
|
| 634 |
text_blocker = ""
|
| 635 |
minicpm_results = [_fallback_card_text(goal, tone, "Live text generation is disabled.") for goal in goals]
|
| 636 |
nemotron3_results = [_fallback_card_text(goal, tone, "Live text generation is disabled.") for goal in goals]
|
|
@@ -650,16 +798,36 @@ def build_quest_pack(payload: dict[str, Any] | None = None, *, use_cache: bool =
|
|
| 650 |
|
| 651 |
minicpm_by_id = {item["goal_id"]: item for item in minicpm_results}
|
| 652 |
nemotron3_by_id = {item["goal_id"]: item for item in nemotron3_results}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
|
| 654 |
audio_blocker = ""
|
| 655 |
audio_results = [_fallback_audio_result(goal, index, "Live speech generation is disabled.") for index, goal in enumerate(goals)]
|
| 656 |
speech_metadata: dict[str, Any] = {}
|
| 657 |
if os.environ.get("EPIC_ENABLE_LIVE_GENERATION") == "1":
|
| 658 |
-
primary_card_texts = {
|
| 659 |
-
goal_id: result.get("card_text") or {}
|
| 660 |
-
for goal_id, result in minicpm_by_id.items()
|
| 661 |
-
if isinstance(result, dict)
|
| 662 |
-
}
|
| 663 |
try:
|
| 664 |
audio_results, speech_metadata["voxcpm2"] = _call_voxcpm2(goals, primary_card_texts)
|
| 665 |
except (OSError, RuntimeError, ValueError, urllib.error.URLError, TimeoutError) as exc:
|
|
@@ -719,9 +887,13 @@ def build_quest_pack(payload: dict[str, Any] | None = None, *, use_cache: bool =
|
|
| 719 |
and not asr_results
|
| 720 |
)
|
| 721 |
|
|
|
|
|
|
|
|
|
|
| 722 |
return {
|
| 723 |
"goals": goals,
|
| 724 |
"quests": quests,
|
|
|
|
| 725 |
"runtime_metadata": metadata,
|
| 726 |
"model_runs": {
|
| 727 |
"image": metadata,
|
|
@@ -735,7 +907,7 @@ def build_quest_pack(payload: dict[str, Any] | None = None, *, use_cache: bool =
|
|
| 735 |
or any_asr_fallback
|
| 736 |
),
|
| 737 |
"blocker": "; ".join(part for part in [blocker, text_blocker, audio_blocker, asr_blocker] if part),
|
| 738 |
-
"generated_at":
|
| 739 |
}
|
| 740 |
|
| 741 |
|
|
|
|
| 27 |
NEMOTRON_SPEECH_MODEL_ID = "nvidia/nemotron-speech-streaming-en-0.6b"
|
| 28 |
|
| 29 |
THEME_ALIASES = {
|
| 30 |
+
"quest": "questbook",
|
| 31 |
+
"questbook": "questbook",
|
| 32 |
+
"magical": "questbook",
|
| 33 |
"class": "classroom",
|
| 34 |
"classroom": "classroom",
|
| 35 |
"comic": "comic",
|
|
|
|
| 67 |
return [{"id": goal.id, "text": goal.text} for goal in DEFAULT_GOALS]
|
| 68 |
|
| 69 |
|
| 70 |
+
def _theme(value: Any, fallback: str = "questbook") -> str:
|
| 71 |
normalized = str(value or fallback).strip().lower()
|
| 72 |
return THEME_ALIASES.get(normalized, fallback)
|
| 73 |
|
|
|
|
| 84 |
goals.append({
|
| 85 |
"id": str(item.get("id") or f"goal-{index + 1}"),
|
| 86 |
"text": text,
|
| 87 |
+
"theme": _theme(item.get("theme") or item.get("style_id"), "questbook"),
|
| 88 |
})
|
| 89 |
if len(goals) == 3:
|
| 90 |
break
|
| 91 |
|
| 92 |
+
fallback_themes = ["questbook", "classroom", "comic"]
|
| 93 |
for fallback_index, fallback in enumerate(default_goal_payload()):
|
| 94 |
if len(goals) == 3:
|
| 95 |
break
|
|
|
|
| 145 |
return "default"
|
| 146 |
|
| 147 |
|
| 148 |
+
def _quest_title(goal_text: str, tone: str, theme: str = "questbook") -> str:
|
| 149 |
lowered = goal_text.lower()
|
| 150 |
if theme == "classroom":
|
| 151 |
noun = "Quest"
|
|
|
|
| 156 |
tone_word = {
|
| 157 |
"cozy": "Cozy",
|
| 158 |
"brave": "Brave",
|
| 159 |
+
"questbook": "Quest",
|
| 160 |
"silly": "Silly",
|
| 161 |
"epic": "Epic",
|
| 162 |
}.get(tone, "Cozy")
|
|
|
|
| 179 |
|
| 180 |
|
| 181 |
def _prompt_for_goal(goal: dict[str, str], tone: str) -> str:
|
| 182 |
+
theme = _theme(goal.get("theme"), "questbook")
|
| 183 |
visual_direction = {
|
| 184 |
+
"questbook": (
|
| 185 |
"premium storybook quest card, warm paper, gentle fantasy props, "
|
| 186 |
"earned crest, no monsters or threats"
|
| 187 |
),
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
|
| 206 |
+
def _theme_label(theme: str) -> str:
|
| 207 |
+
return {
|
| 208 |
+
"questbook": "Questbook",
|
| 209 |
+
"classroom": "Classroom",
|
| 210 |
+
"comic": "Comic",
|
| 211 |
+
}.get(_theme(theme), theme.title())
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _text_prompt_for_goal(goal: dict[str, str]) -> str:
|
| 215 |
+
theme = _theme_label(_theme(goal.get("theme"), "questbook"))
|
| 216 |
+
return (
|
| 217 |
+
"Create a kid-safe Epic Errands goal card for an 8-year-old. "
|
| 218 |
+
f"Theme: {theme}. Plain goal: \"{goal['text']}\". "
|
| 219 |
+
"Return compact JSON only with keys title, narration, and reward_label. "
|
| 220 |
+
"Do not include task lists, verification fields, snake_case, UI copy, or "
|
| 221 |
+
"image text. Keep the ordinary goal visible, positive, parent-readable, and "
|
| 222 |
+
"achievable. No shame, threats, monsters, weapons, private details, readable "
|
| 223 |
+
"image text, or user photo references."
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _card_prompt_block(card_text: dict[str, Any] | None) -> str:
|
| 228 |
+
if not isinstance(card_text, dict) or not card_text:
|
| 229 |
+
return "Generated Epic Errand card text is unavailable; use the plain goal only."
|
| 230 |
+
return (
|
| 231 |
+
f"{str(card_text.get('title') or '').strip()}. "
|
| 232 |
+
f"{str(card_text.get('narration') or '').strip()}"
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def _scene_hint_for_goal(goal_text: str) -> str:
|
| 237 |
+
lowered = goal_text.lower()
|
| 238 |
+
if "room" in lowered or "clean" in lowered:
|
| 239 |
+
return (
|
| 240 |
+
"Show a child tidying a bright bedroom, placing toys in bins and books "
|
| 241 |
+
"on shelves, with a cozy dinner glow through the doorway. All papers, "
|
| 242 |
+
"walls, signs, books, covers, and surfaces are completely blank."
|
| 243 |
+
)
|
| 244 |
+
if "project" in lowered or "class" in lowered:
|
| 245 |
+
return (
|
| 246 |
+
"Show a child organizing blank papers, colored shapes, pencils, and a "
|
| 247 |
+
"calm desk, with an encouraging adult nearby. Every worksheet, poster, "
|
| 248 |
+
"paper, notebook, and board is completely blank."
|
| 249 |
+
)
|
| 250 |
+
if "read" in lowered:
|
| 251 |
+
return (
|
| 252 |
+
"Show a child enjoying a quiet reading moment in a cozy seat with warm "
|
| 253 |
+
"light and a single plain book. Use a simple background with no bookshelf, "
|
| 254 |
+
"no book spines, no posters, and no decorative labels. The book cover, "
|
| 255 |
+
"book pages, and all background surfaces are completely blank."
|
| 256 |
+
)
|
| 257 |
+
return "Show a child-safe everyday accomplishment scene tied to the plain goal."
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def _image_prompt_for_card(goal: dict[str, str], tone: str, card_text: dict[str, Any] | None) -> str:
|
| 261 |
+
theme = _theme(goal.get("theme"), "questbook")
|
| 262 |
+
visual_direction = {
|
| 263 |
+
"questbook": (
|
| 264 |
+
"Warm fantasy story illustration, soft lantern light, gentle adventure "
|
| 265 |
+
"props, cozy handcrafted polish, full-bleed scene, no parchment border, "
|
| 266 |
+
"no title plaque, no banner."
|
| 267 |
+
),
|
| 268 |
+
"classroom": (
|
| 269 |
+
"Sunny learning illustration, tidy supplies, pencils, blank notebooks, "
|
| 270 |
+
"calm school-day polish, simple blank walls, no posters, no signs."
|
| 271 |
+
),
|
| 272 |
+
"comic": (
|
| 273 |
+
"Bright cartoon action illustration, bold shapes, clean motion lines, "
|
| 274 |
+
"cheerful everyday hero energy, full-bleed scene, no frame, no panel, "
|
| 275 |
+
"no banner, no speech bubble."
|
| 276 |
+
),
|
| 277 |
+
}[theme]
|
| 278 |
+
return (
|
| 279 |
+
"Generate an image-only hero illustration for a child goal app. "
|
| 280 |
+
f"Tone: {tone}. "
|
| 281 |
+
f"{visual_direction} {_scene_hint_for_goal(goal['text'])} "
|
| 282 |
+
"This is not a poster, title card, label, worksheet, book cover, or UI. Do not render any "
|
| 283 |
+
"words, alphabet letters, numerals, captions, labels, signs, handwriting, "
|
| 284 |
+
"book titles, speech bubbles, sound effects, logos, or UI text. Use image "
|
| 285 |
+
"composition only; all paper and book surfaces stay blank; no user photos, "
|
| 286 |
+
"no monsters or threats."
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def _audio_prompt_for_card(goal: dict[str, str], card_text: dict[str, Any] | None) -> str:
|
| 291 |
+
theme = _theme(goal.get("theme"), "questbook")
|
| 292 |
+
voice_direction = {
|
| 293 |
+
"questbook": "gentle Questbook narrator, warm, clear, encouraging, paced for a child",
|
| 294 |
+
"classroom": "warm teacher, clear, encouraging, calm classroom cadence",
|
| 295 |
+
"comic": "upbeat comic narrator, energetic but not loud or frantic, clear and encouraging",
|
| 296 |
+
}[theme]
|
| 297 |
+
return (
|
| 298 |
+
"Generate a friendly spoken version of the Epic Errand produced by the text model. "
|
| 299 |
+
f"Plain goal: \"{goal['text']}\". Theme: {_theme_label(theme)}. "
|
| 300 |
+
f"Voice direction: {voice_direction}. Speak only the title and short narration "
|
| 301 |
+
f"from this generated card text: {_card_prompt_block(card_text)} "
|
| 302 |
+
"Do not add internal field names, warnings, or unrelated commentary."
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def _spoken_text_for_card(goal: dict[str, str], card_text: dict[str, Any] | None) -> str:
|
| 307 |
+
if not isinstance(card_text, dict) or not card_text:
|
| 308 |
+
return f"Your Epic Errands card is ready. Today's goal is: {goal['text']}."
|
| 309 |
+
parts = [
|
| 310 |
+
str(card_text.get("title") or "").strip(),
|
| 311 |
+
str(card_text.get("narration") or "").strip(),
|
| 312 |
+
]
|
| 313 |
+
spoken = " ".join(part for part in parts if part)
|
| 314 |
+
return spoken or f"Your Epic Errands card is ready. Today's goal is: {goal['text']}."
|
| 315 |
+
|
| 316 |
+
|
| 317 |
def _fallback_image_result(goal: dict[str, str], index: int, reason: str) -> dict[str, Any]:
|
| 318 |
return {
|
| 319 |
"goal_id": goal["id"],
|
|
|
|
| 322 |
"hero_src": _asset_to_data_uri(FALLBACK_HERO),
|
| 323 |
"origin": "deterministic_fallback",
|
| 324 |
"prompt_summary": f"Fallback classroom adventure image for goal: {goal['text']}",
|
| 325 |
+
"resolved_image_prompt": _image_prompt_for_card(goal, "cozy", None),
|
| 326 |
"runtime_metadata": _runtime_metadata(
|
| 327 |
fallback_used=True,
|
| 328 |
fallback_reason=reason,
|
|
|
|
| 331 |
|
| 332 |
|
| 333 |
def _fallback_card_text(goal: dict[str, str], tone: str, reason: str) -> dict[str, Any]:
|
|
|
|
| 334 |
return {
|
| 335 |
"goal_id": goal["id"],
|
| 336 |
"ordinary_goal": goal["text"],
|
| 337 |
+
"theme": _theme(goal.get("theme"), "questbook"),
|
| 338 |
"model_key": "deterministic_fallback",
|
| 339 |
"model_id": "none",
|
| 340 |
"schema_valid": True,
|
| 341 |
"raw_text": "",
|
| 342 |
+
"text_prompt": _text_prompt_for_goal(goal),
|
| 343 |
"card_text": {
|
| 344 |
+
"title": _quest_title(goal["text"], tone, _theme(goal.get("theme"), "questbook")),
|
| 345 |
"narration": f"Your goal card is ready: {goal['text']}. Take it one small step at a time.",
|
|
|
|
| 346 |
"reward_label": "Quest Hero",
|
|
|
|
| 347 |
},
|
| 348 |
"runtime_metadata": _runtime_metadata(fallback_used=True, fallback_reason=reason),
|
| 349 |
}
|
|
|
|
| 353 |
return {
|
| 354 |
"goal_id": goal["id"],
|
| 355 |
"ordinary_goal": goal["text"],
|
| 356 |
+
"theme": _theme(goal.get("theme"), "questbook"),
|
| 357 |
"audio_asset_name": "",
|
| 358 |
"audio_src": "",
|
| 359 |
"spoken_text": "",
|
| 360 |
+
"resolved_audio_prompt": _audio_prompt_for_card(goal, None),
|
| 361 |
"runtime_metadata": _runtime_metadata(
|
| 362 |
model_backend="modal_http",
|
| 363 |
model_id=VOXCPM2_MODEL_ID,
|
|
|
|
| 422 |
return results
|
| 423 |
|
| 424 |
|
| 425 |
+
def _call_modal(
|
| 426 |
+
goals: list[dict[str, str]],
|
| 427 |
+
tone: str,
|
| 428 |
+
card_texts: dict[str, dict[str, Any]] | None = None,
|
| 429 |
+
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
| 430 |
base_url = os.environ.get("EPIC_MODAL_IMAGE_URL", "").strip() or os.environ.get("APP_MODAL_BASE_URL", "").strip()
|
| 431 |
token = os.environ.get("APP_MODAL_AUTH_TOKEN", "").strip()
|
| 432 |
timeout = float(os.environ.get("APP_MODAL_TIMEOUT_SECONDS", "180"))
|
|
|
|
| 438 |
{
|
| 439 |
"goal_id": goal["id"],
|
| 440 |
"ordinary_goal": goal["text"],
|
| 441 |
+
"theme": _theme(goal.get("theme"), "questbook"),
|
| 442 |
+
"card_text": (card_texts or {}).get(goal["id"], {}),
|
| 443 |
+
"prompt": _image_prompt_for_card(goal, tone, (card_texts or {}).get(goal["id"])),
|
| 444 |
"seed": 26000 + index,
|
| 445 |
}
|
| 446 |
for index, goal in enumerate(goals)
|
|
|
|
| 497 |
"hero_src": f"data:image/png;base64,{base64.b64encode(image_bytes).decode('ascii')}",
|
| 498 |
"origin": "modal_flux_generated",
|
| 499 |
"prompt_summary": output.get("prompt_summary", _prompt_for_goal(goal, tone)),
|
| 500 |
+
"resolved_image_prompt": output.get("resolved_image_prompt", output.get("prompt_summary", "")),
|
| 501 |
"runtime_metadata": metadata,
|
| 502 |
"seed": output.get("seed", 26000 + index),
|
| 503 |
})
|
|
|
|
| 536 |
{
|
| 537 |
"goal_id": goal["id"],
|
| 538 |
"ordinary_goal": goal["text"],
|
| 539 |
+
"theme": _theme(goal.get("theme"), "questbook"),
|
| 540 |
+
"text_prompt": _text_prompt_for_goal(goal),
|
| 541 |
}
|
| 542 |
for goal in goals
|
| 543 |
],
|
|
|
|
| 577 |
{
|
| 578 |
"goal_id": goal["id"],
|
| 579 |
"ordinary_goal": goal["text"],
|
| 580 |
+
"theme": _theme(goal.get("theme"), "questbook"),
|
| 581 |
+
"card_text": card_texts.get(goal["id"], {}),
|
| 582 |
+
"audio_prompt": _audio_prompt_for_card(goal, card_texts.get(goal["id"])),
|
| 583 |
+
"narration": _spoken_text_for_card(goal, card_texts.get(goal["id"])),
|
| 584 |
}
|
| 585 |
for goal in goals
|
| 586 |
],
|
|
|
|
| 618 |
saved.append({
|
| 619 |
"goal_id": goal["id"],
|
| 620 |
"ordinary_goal": goal["text"],
|
| 621 |
+
"theme": _theme(goal.get("theme"), "questbook"),
|
| 622 |
"audio_asset_name": asset_name,
|
| 623 |
"audio_src": f"data:audio/wav;base64,{base64.b64encode(audio_bytes).decode('ascii')}",
|
| 624 |
"audio_wav_base64": audio_b64,
|
| 625 |
"spoken_text": output.get("spoken_text", ""),
|
| 626 |
+
"resolved_audio_prompt": output.get("resolved_audio_prompt", output.get("audio_prompt", "")),
|
| 627 |
"sample_rate": output.get("sample_rate"),
|
| 628 |
"runtime_metadata": metadata,
|
| 629 |
})
|
|
|
|
| 679 |
audio_results: dict[str, dict[str, Any]] | None = None,
|
| 680 |
asr_results: dict[str, dict[str, Any]] | None = None,
|
| 681 |
) -> dict[str, Any]:
|
| 682 |
+
steps: list[dict[str, str]] = []
|
| 683 |
+
theme = _theme(goal.get("theme"), "questbook")
|
|
|
|
|
|
|
|
|
|
| 684 |
primary_text = (text_results or {}).get(goal["id"], {})
|
| 685 |
secondary_text = (secondary_text_results or {}).get(goal["id"], {})
|
| 686 |
card_text = primary_text.get("card_text") or _fallback_card_text(goal, tone, "Primary text model did not return card_text.")["card_text"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
audio = (audio_results or {}).get(goal["id"], {})
|
| 688 |
asr = (asr_results or {}).get(goal["id"], {})
|
| 689 |
return {
|
|
|
|
| 698 |
"hero_alt": f"Storybook classroom adventure for {goal['text']}",
|
| 699 |
"reward": {
|
| 700 |
"label": str(card_text.get("reward_label") or (["Idea Builder", "Lantern Reader", "Tidy Hero"][index] if index < 3 else "Quest Hero")),
|
| 701 |
+
"description": "Reward earned when the ordinary goal is finished.",
|
| 702 |
},
|
| 703 |
"steps": steps,
|
| 704 |
"narration": str(card_text.get("narration") or ""),
|
| 705 |
"why_it_helps": _why_it_helps(goal["text"]),
|
| 706 |
"image_origin": image_result.get("origin", "unknown"),
|
| 707 |
"prompt_summary": image_result.get("prompt_summary", ""),
|
| 708 |
+
"generation_trace": {
|
| 709 |
+
"text_prompt": primary_text.get("text_prompt", _text_prompt_for_goal(goal)),
|
| 710 |
+
"card_text": card_text,
|
| 711 |
+
"image_prompt_template": "Generated Epic Errand meaning + theme visual direction + strict no-text negatives.",
|
| 712 |
+
"resolved_image_prompt": image_result.get("resolved_image_prompt", image_result.get("prompt_summary", "")),
|
| 713 |
+
"audio_prompt_template": "Generated Epic Errand title/narration + theme voice direction + no-field-name rule.",
|
| 714 |
+
"resolved_audio_prompt": audio.get("resolved_audio_prompt", _audio_prompt_for_card(goal, card_text)),
|
| 715 |
+
"spoken_text": audio.get("spoken_text", _spoken_text_for_card(goal, card_text)),
|
| 716 |
+
},
|
| 717 |
"runtime_metadata": image_result.get("runtime_metadata", _runtime_metadata()),
|
| 718 |
"text_models": {
|
| 719 |
"primary": primary_text,
|
|
|
|
| 728 |
}
|
| 729 |
|
| 730 |
|
| 731 |
+
def _agent_trace_row(quest: dict[str, Any], run_generated_at: str) -> dict[str, Any]:
|
| 732 |
+
trace = quest.get("generation_trace", {})
|
| 733 |
+
primary_text = quest.get("text_models", {}).get("primary", {})
|
| 734 |
+
secondary_text = quest.get("text_models", {}).get("secondary", {})
|
| 735 |
+
image_runtime = quest.get("runtime_metadata", _runtime_metadata())
|
| 736 |
+
speech_runtime = quest.get("speech_runtime_metadata", _runtime_metadata())
|
| 737 |
+
asr_runtime = quest.get("asr_runtime_metadata", _runtime_metadata())
|
| 738 |
+
plain_goal = str(quest.get("ordinary_goal") or "")
|
| 739 |
+
theme = str(quest.get("theme") or "")
|
| 740 |
+
trace_hash = hashlib.sha1(f"{plain_goal}:{theme}:{run_generated_at}".encode("utf-8")).hexdigest()[:10]
|
| 741 |
+
return {
|
| 742 |
+
"trace_id": f"epic-errands-v1-{trace_hash}",
|
| 743 |
+
"generated_at": run_generated_at,
|
| 744 |
+
"goal_id": quest.get("source_goal_id", ""),
|
| 745 |
+
"plain_goal": plain_goal,
|
| 746 |
+
"theme": theme,
|
| 747 |
+
"text_model_primary": primary_text.get("model_id", image_runtime.get("model_id", "none")),
|
| 748 |
+
"text_model_secondary": secondary_text.get("model_id", ""),
|
| 749 |
+
"text_prompt": trace.get("text_prompt", ""),
|
| 750 |
+
"card_text": trace.get("card_text", {}),
|
| 751 |
+
"image_model_id": image_runtime.get("model_id", MODEL_ID),
|
| 752 |
+
"image_prompt_template": trace.get("image_prompt_template", ""),
|
| 753 |
+
"resolved_image_prompt": trace.get("resolved_image_prompt", ""),
|
| 754 |
+
"image_asset_path": quest.get("hero_asset_name", ""),
|
| 755 |
+
"audio_model_id": speech_runtime.get("model_id", VOXCPM2_MODEL_ID),
|
| 756 |
+
"audio_prompt_template": trace.get("audio_prompt_template", ""),
|
| 757 |
+
"resolved_audio_prompt": trace.get("resolved_audio_prompt", ""),
|
| 758 |
+
"spoken_text": trace.get("spoken_text", quest.get("spoken_text", "")),
|
| 759 |
+
"audio_asset_path": quest.get("audio_asset_name", ""),
|
| 760 |
+
"asr_model_id": asr_runtime.get("model_id", NEMOTRON_SPEECH_MODEL_ID),
|
| 761 |
+
"asr_transcript": quest.get("asr_transcript", ""),
|
| 762 |
+
"runtime_axes": {
|
| 763 |
+
"image": image_runtime,
|
| 764 |
+
"audio": speech_runtime,
|
| 765 |
+
"asr": asr_runtime,
|
| 766 |
+
},
|
| 767 |
+
"fallback_used": (
|
| 768 |
+
bool(image_runtime.get("fallback_used", True))
|
| 769 |
+
or bool(speech_runtime.get("fallback_used", True))
|
| 770 |
+
or bool(asr_runtime.get("fallback_used", True))
|
| 771 |
+
or bool(primary_text.get("runtime_metadata", {}).get("fallback_used", False))
|
| 772 |
+
),
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
|
| 776 |
def build_quest_pack(payload: dict[str, Any] | None = None, *, use_cache: bool = True) -> dict[str, Any]:
|
| 777 |
payload = payload or {}
|
| 778 |
tone = str(payload.get("tone") or "cozy").strip().lower() or "cozy"
|
| 779 |
goals = normalize_goals(payload.get("goals"))
|
| 780 |
force_refresh = bool(payload.get("force_refresh"))
|
| 781 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
text_blocker = ""
|
| 783 |
minicpm_results = [_fallback_card_text(goal, tone, "Live text generation is disabled.") for goal in goals]
|
| 784 |
nemotron3_results = [_fallback_card_text(goal, tone, "Live text generation is disabled.") for goal in goals]
|
|
|
|
| 798 |
|
| 799 |
minicpm_by_id = {item["goal_id"]: item for item in minicpm_results}
|
| 800 |
nemotron3_by_id = {item["goal_id"]: item for item in nemotron3_results}
|
| 801 |
+
primary_card_texts = {
|
| 802 |
+
goal_id: result.get("card_text") or {}
|
| 803 |
+
for goal_id, result in minicpm_by_id.items()
|
| 804 |
+
if isinstance(result, dict)
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
images: list[dict[str, Any]] | None = None
|
| 808 |
+
metadata = _runtime_metadata()
|
| 809 |
+
blocker = ""
|
| 810 |
+
if use_cache and not force_refresh:
|
| 811 |
+
images = _cached_images_for(goals)
|
| 812 |
+
if images:
|
| 813 |
+
metadata = dict(images[0].get("runtime_metadata", metadata))
|
| 814 |
+
metadata["fallback_used"] = any(
|
| 815 |
+
bool(item.get("runtime_metadata", {}).get("fallback_used", True))
|
| 816 |
+
for item in images
|
| 817 |
+
)
|
| 818 |
+
|
| 819 |
+
if images is None:
|
| 820 |
+
try:
|
| 821 |
+
images, metadata = _call_modal(goals, tone, primary_card_texts)
|
| 822 |
+
except (OSError, RuntimeError, ValueError, urllib.error.URLError, TimeoutError) as exc:
|
| 823 |
+
blocker = f"{type(exc).__name__}: {exc}"
|
| 824 |
+
images = [_fallback_image_result(goal, index, blocker) for index, goal in enumerate(goals)]
|
| 825 |
+
metadata = _runtime_metadata(fallback_used=True, fallback_reason=blocker)
|
| 826 |
|
| 827 |
audio_blocker = ""
|
| 828 |
audio_results = [_fallback_audio_result(goal, index, "Live speech generation is disabled.") for index, goal in enumerate(goals)]
|
| 829 |
speech_metadata: dict[str, Any] = {}
|
| 830 |
if os.environ.get("EPIC_ENABLE_LIVE_GENERATION") == "1":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
try:
|
| 832 |
audio_results, speech_metadata["voxcpm2"] = _call_voxcpm2(goals, primary_card_texts)
|
| 833 |
except (OSError, RuntimeError, ValueError, urllib.error.URLError, TimeoutError) as exc:
|
|
|
|
| 887 |
and not asr_results
|
| 888 |
)
|
| 889 |
|
| 890 |
+
generated_at = datetime.now(timezone.utc).isoformat()
|
| 891 |
+
agent_traces = [_agent_trace_row(quest, generated_at) for quest in quests]
|
| 892 |
+
|
| 893 |
return {
|
| 894 |
"goals": goals,
|
| 895 |
"quests": quests,
|
| 896 |
+
"agent_traces": agent_traces,
|
| 897 |
"runtime_metadata": metadata,
|
| 898 |
"model_runs": {
|
| 899 |
"image": metadata,
|
|
|
|
| 907 |
or any_asr_fallback
|
| 908 |
),
|
| 909 |
"blocker": "; ".join(part for part in [blocker, text_blocker, audio_blocker, asr_blocker] if part),
|
| 910 |
+
"generated_at": generated_at,
|
| 911 |
}
|
| 912 |
|
| 913 |
|
epic_errands_space/render.py
CHANGED
|
@@ -23,16 +23,16 @@ STYLE_MODES: dict[str, dict[str, str]] = {
|
|
| 23 |
"copy": "Warm school adventure with stickers, notebooks, and clear wins.",
|
| 24 |
"image": "generated-hero-1-finish-class-project-8f64eeb3.png",
|
| 25 |
},
|
| 26 |
-
"
|
| 27 |
-
"label": "
|
| 28 |
-
"mark": "
|
| 29 |
-
"goal_noun": "
|
| 30 |
-
"parent_title": "
|
| 31 |
-
"kid_title": "
|
| 32 |
-
"reward_noun": "
|
| 33 |
-
"reward": "
|
| 34 |
-
"waiting": "Waiting for
|
| 35 |
-
"copy": "
|
| 36 |
"image": "generated-hero-2-read-for-20-minutes-895e7642.png",
|
| 37 |
},
|
| 38 |
"comic": {
|
|
@@ -53,8 +53,8 @@ INITIAL_GOALS: list[dict[str, str]] = [
|
|
| 53 |
{
|
| 54 |
"id": "goal-1",
|
| 55 |
"ordinary_goal": "Finish class project",
|
| 56 |
-
"title": "
|
| 57 |
-
"style_id": "
|
| 58 |
"kid_completion_state": "completed",
|
| 59 |
"parent_reward_state": "waiting_for_approval",
|
| 60 |
},
|
|
@@ -93,7 +93,7 @@ def _initial_state() -> dict[str, Any]:
|
|
| 93 |
}
|
| 94 |
return {
|
| 95 |
"activeTab": "home",
|
| 96 |
-
"defaultStyleId": "
|
| 97 |
"selectedGoalId": "goal-1",
|
| 98 |
"parentName": "Maya",
|
| 99 |
"kidName": "Ari",
|
|
@@ -175,7 +175,7 @@ def build_app_css() -> str:
|
|
| 175 |
--reward: #11784f;
|
| 176 |
}
|
| 177 |
|
| 178 |
-
.ee-shell[data-style="
|
| 179 |
--theme: #7652c7;
|
| 180 |
--theme-2: #0f877e;
|
| 181 |
--theme-3: #efb84a;
|
|
@@ -656,9 +656,17 @@ if (root && root.dataset.ready !== "true") {
|
|
| 656 |
root.dataset.ready = "true";
|
| 657 |
|
| 658 |
const state = __STATE_JSON__;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
const stepsByStyle = {
|
| 660 |
classroom: ["Find your reading spot", "Read the next pages", "Tell the best part"],
|
| 661 |
-
|
| 662 |
comic: ["Sort the pile", "Fold the hero gear", "Launch it to the drawer"],
|
| 663 |
};
|
| 664 |
|
|
@@ -671,13 +679,19 @@ function escapeHtml(value) {
|
|
| 671 |
.replace(/'/g, "'");
|
| 672 |
}
|
| 673 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
function styleFor(goalOrStyleId) {
|
| 675 |
-
const
|
|
|
|
| 676 |
return state.styles[styleId] || state.styles.classroom;
|
| 677 |
}
|
| 678 |
|
| 679 |
function shellStyle() {
|
| 680 |
-
return state.styles[state.defaultStyleId] || state.styles.
|
| 681 |
}
|
| 682 |
|
| 683 |
function awaitingApprovalCount() {
|
|
@@ -824,7 +838,7 @@ function renderKid() {
|
|
| 824 |
${state.goals.map((goal) => {
|
| 825 |
const style = styleFor(goal);
|
| 826 |
return `<button class="ee-kid-tile" data-action="select-goal" data-goal-id="${goal.id}" aria-pressed="${state.selectedGoalId === goal.id}">
|
| 827 |
-
<img src="${state.images[goal.style_id] || ""}" alt="${escapeHtml(style.label)} generated image for ${escapeHtml(goal.ordinary_goal)}">
|
| 828 |
<span class="ee-tile-body">
|
| 829 |
<span class="ee-tile-title">${escapeHtml(goal.title)}</span>
|
| 830 |
${stateChip(goal)}
|
|
@@ -838,11 +852,12 @@ function renderKid() {
|
|
| 838 |
|
| 839 |
function renderGoalCard(goal) {
|
| 840 |
const style = styleFor(goal);
|
| 841 |
-
const
|
|
|
|
| 842 |
const waiting = goal.parent_reward_state === "waiting_for_approval";
|
| 843 |
const rewarded = goal.parent_reward_state === "approved_reward_given";
|
| 844 |
return `<article class="ee-goal-card" data-design-id="kid-goal-card">
|
| 845 |
-
<img class="ee-card-image" src="${state.images[
|
| 846 |
<div class="ee-card-body">
|
| 847 |
<div>
|
| 848 |
<p class="ee-kicker">${escapeHtml(style.kid_title)} ${escapeHtml(style.goal_noun)}</p>
|
|
@@ -865,7 +880,7 @@ function renderGoalCard(goal) {
|
|
| 865 |
function render() {
|
| 866 |
const shell = root.querySelector(".ee-shell");
|
| 867 |
const screen = state.activeTab === "parent" ? renderParent() : state.activeTab === "kid" ? renderKid() : renderHome();
|
| 868 |
-
shell.dataset.style = state.defaultStyleId;
|
| 869 |
shell.innerHTML = `${renderHeader()}<div class="ee-body">${screen}</div>`;
|
| 870 |
}
|
| 871 |
|
|
@@ -881,7 +896,7 @@ root.addEventListener("click", (event) => {
|
|
| 881 |
}
|
| 882 |
|
| 883 |
if (target.dataset.action === "style") {
|
| 884 |
-
state.defaultStyleId = target.dataset.style || state.defaultStyleId;
|
| 885 |
render();
|
| 886 |
return;
|
| 887 |
}
|
|
|
|
| 23 |
"copy": "Warm school adventure with stickers, notebooks, and clear wins.",
|
| 24 |
"image": "generated-hero-1-finish-class-project-8f64eeb3.png",
|
| 25 |
},
|
| 26 |
+
"questbook": {
|
| 27 |
+
"label": "Questbook",
|
| 28 |
+
"mark": "QB",
|
| 29 |
+
"goal_noun": "Quest",
|
| 30 |
+
"parent_title": "Quest Keeper",
|
| 31 |
+
"kid_title": "Young Adventurer",
|
| 32 |
+
"reward_noun": "crest",
|
| 33 |
+
"reward": "Orderkeeper crest",
|
| 34 |
+
"waiting": "Waiting for Quest Keeper approval.",
|
| 35 |
+
"copy": "Premium questbook fantasy with warm paper, ink lines, and earned crests.",
|
| 36 |
"image": "generated-hero-2-read-for-20-minutes-895e7642.png",
|
| 37 |
},
|
| 38 |
"comic": {
|
|
|
|
| 53 |
{
|
| 54 |
"id": "goal-1",
|
| 55 |
"ordinary_goal": "Finish class project",
|
| 56 |
+
"title": "Quest: Bright Idea Bridge",
|
| 57 |
+
"style_id": "questbook",
|
| 58 |
"kid_completion_state": "completed",
|
| 59 |
"parent_reward_state": "waiting_for_approval",
|
| 60 |
},
|
|
|
|
| 93 |
}
|
| 94 |
return {
|
| 95 |
"activeTab": "home",
|
| 96 |
+
"defaultStyleId": "questbook",
|
| 97 |
"selectedGoalId": "goal-1",
|
| 98 |
"parentName": "Maya",
|
| 99 |
"kidName": "Ari",
|
|
|
|
| 175 |
--reward: #11784f;
|
| 176 |
}
|
| 177 |
|
| 178 |
+
.ee-shell[data-style="questbook"] {
|
| 179 |
--theme: #7652c7;
|
| 180 |
--theme-2: #0f877e;
|
| 181 |
--theme-3: #efb84a;
|
|
|
|
| 656 |
root.dataset.ready = "true";
|
| 657 |
|
| 658 |
const state = __STATE_JSON__;
|
| 659 |
+
const styleAliases = {
|
| 660 |
+
quest: "questbook",
|
| 661 |
+
questbook: "questbook",
|
| 662 |
+
magical: "questbook",
|
| 663 |
+
class: "classroom",
|
| 664 |
+
classroom: "classroom",
|
| 665 |
+
comic: "comic",
|
| 666 |
+
};
|
| 667 |
const stepsByStyle = {
|
| 668 |
classroom: ["Find your reading spot", "Read the next pages", "Tell the best part"],
|
| 669 |
+
questbook: ["Gather your project ideas", "Build the first bright piece", "Check your questbook notes"],
|
| 670 |
comic: ["Sort the pile", "Fold the hero gear", "Launch it to the drawer"],
|
| 671 |
};
|
| 672 |
|
|
|
|
| 679 |
.replace(/'/g, "'");
|
| 680 |
}
|
| 681 |
|
| 682 |
+
function normalizeStyleId(value, fallback = "questbook") {
|
| 683 |
+
const normalized = String(value || fallback).trim().toLowerCase();
|
| 684 |
+
return styleAliases[normalized] || fallback;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
function styleFor(goalOrStyleId) {
|
| 688 |
+
const rawStyleId = typeof goalOrStyleId === "string" ? goalOrStyleId : goalOrStyleId.style_id;
|
| 689 |
+
const styleId = normalizeStyleId(rawStyleId);
|
| 690 |
return state.styles[styleId] || state.styles.classroom;
|
| 691 |
}
|
| 692 |
|
| 693 |
function shellStyle() {
|
| 694 |
+
return state.styles[normalizeStyleId(state.defaultStyleId)] || state.styles.questbook;
|
| 695 |
}
|
| 696 |
|
| 697 |
function awaitingApprovalCount() {
|
|
|
|
| 838 |
${state.goals.map((goal) => {
|
| 839 |
const style = styleFor(goal);
|
| 840 |
return `<button class="ee-kid-tile" data-action="select-goal" data-goal-id="${goal.id}" aria-pressed="${state.selectedGoalId === goal.id}">
|
| 841 |
+
<img src="${state.images[normalizeStyleId(goal.style_id)] || ""}" alt="${escapeHtml(style.label)} generated image for ${escapeHtml(goal.ordinary_goal)}">
|
| 842 |
<span class="ee-tile-body">
|
| 843 |
<span class="ee-tile-title">${escapeHtml(goal.title)}</span>
|
| 844 |
${stateChip(goal)}
|
|
|
|
| 852 |
|
| 853 |
function renderGoalCard(goal) {
|
| 854 |
const style = styleFor(goal);
|
| 855 |
+
const styleId = normalizeStyleId(goal.style_id);
|
| 856 |
+
const steps = stepsByStyle[styleId] || stepsByStyle.classroom;
|
| 857 |
const waiting = goal.parent_reward_state === "waiting_for_approval";
|
| 858 |
const rewarded = goal.parent_reward_state === "approved_reward_given";
|
| 859 |
return `<article class="ee-goal-card" data-design-id="kid-goal-card">
|
| 860 |
+
<img class="ee-card-image" src="${state.images[styleId] || ""}" alt="${escapeHtml(style.label)} generated goal card image for ${escapeHtml(goal.ordinary_goal)}">
|
| 861 |
<div class="ee-card-body">
|
| 862 |
<div>
|
| 863 |
<p class="ee-kicker">${escapeHtml(style.kid_title)} ${escapeHtml(style.goal_noun)}</p>
|
|
|
|
| 880 |
function render() {
|
| 881 |
const shell = root.querySelector(".ee-shell");
|
| 882 |
const screen = state.activeTab === "parent" ? renderParent() : state.activeTab === "kid" ? renderKid() : renderHome();
|
| 883 |
+
shell.dataset.style = normalizeStyleId(state.defaultStyleId);
|
| 884 |
shell.innerHTML = `${renderHeader()}<div class="ee-body">${screen}</div>`;
|
| 885 |
}
|
| 886 |
|
|
|
|
| 896 |
}
|
| 897 |
|
| 898 |
if (target.dataset.action === "style") {
|
| 899 |
+
state.defaultStyleId = normalizeStyleId(target.dataset.style || state.defaultStyleId);
|
| 900 |
render();
|
| 901 |
return;
|
| 902 |
}
|
frontend/assets/demo-placeholders/placeholder-boy-720.png
ADDED
|
Git LFS Details
|
frontend/assets/demo-placeholders/placeholder-female-parent-720.png
ADDED
|
Git LFS Details
|
frontend/assets/demo-placeholders/placeholder-girl-720.png
ADDED
|
Git LFS Details
|
frontend/assets/demo-placeholders/placeholder-male-parent-720.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-hero-manifest.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/assets/generated-v2/clean-room-classroom-c5cb506427.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/clean-room-classroom-cf6f43d6ce.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c28f904ffbeddd61d2c1c42c6feeb6a7f4ad6411df294e1798886e969ac49c8d
|
| 3 |
+
size 476204
|
frontend/assets/generated-v2/clean-room-comic-88b770a8dc.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7d480728fb53c35e3736bc5962bed033d43d1820e9ee2d84dfc49f5819c55a92
|
| 3 |
+
size 506924
|
frontend/assets/generated-v2/clean-room-comic-9bce2b2b21.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/clean-room-questbook-01493f395c.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1fd97b2e4646cc0a9b71a85e40a1934c49ec3d4dc56fef054205892a6d3380ed
|
| 3 |
+
size 645164
|
frontend/assets/generated-v2/clean-room-questbook-d35e6ee10c.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/project-outline-classroom-3783f10bae.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/project-outline-classroom-70a886be5a.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:32205a5ba298d905addc223c1c440f6e28f12e8ad6360e73cd272acf930ad189
|
| 3 |
+
size 476204
|
frontend/assets/generated-v2/project-outline-comic-4f9eaa443a.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/project-outline-comic-ef3f180846.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:72f4d436bae6059ccba06f0b396d6232773b312dceefd17abc56dd1567efa91c
|
| 3 |
+
size 491564
|
frontend/assets/generated-v2/project-outline-questbook-bddf03af1a.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f920054ef4f1dc56fb179afc39954362fca77d788b9053241d5a524a00aa2a15
|
| 3 |
+
size 629804
|
frontend/assets/generated-v2/project-outline-questbook-e1d5ced2e8.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/read-20-classroom-7c13f43782.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/read-20-classroom-cca67b8288.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:43d6cae31493484882409f4221bba1e73a17120df1c0f67bc32bb42c3a737d82
|
| 3 |
+
size 553004
|
frontend/assets/generated-v2/read-20-comic-7ec8c91bf8.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:def73d2835049162aa30cc8ebb44596cf90ccd55e2c0519fdb831432397a213c
|
| 3 |
+
size 337964
|
frontend/assets/generated-v2/read-20-comic-b5ae2d561a.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/read-20-questbook-39aac8ba25.png
ADDED
|
Git LFS Details
|
frontend/assets/generated-v2/read-20-questbook-5805518930.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:16131b8c8a6711c2e09ea00732c74be78333f9486ebb25f2eacccc98cced828d
|
| 3 |
+
size 798764
|
frontend/index.html
CHANGED
|
@@ -3,15 +3,38 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
-
<title>Epic Errands V1 -
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
-
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600;700;800&family=
|
| 10 |
-
<link href="
|
|
|
|
| 11 |
<link rel="stylesheet" href="/frontend/styles/app.css">
|
| 12 |
</head>
|
| 13 |
<body>
|
| 14 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
<script src="/frontend/scripts/app.js"></script>
|
| 16 |
</body>
|
| 17 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Epic Errands V1 - Generated Media Prototype</title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700;800&family=Nunito:wght@400;600;700;800;900&family=Bangers&family=Hanken+Grotesk:wght@400;500;600;700;800&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,500&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
| 10 |
+
<link rel="stylesheet" href="/frontend/styles/tokens.css">
|
| 11 |
+
<link rel="stylesheet" href="/frontend/styles/components.css">
|
| 12 |
<link rel="stylesheet" href="/frontend/styles/app.css">
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
+
<nav class="theme-rail" aria-label="Theme">
|
| 16 |
+
<span class="theme-rail__label">Theme</span>
|
| 17 |
+
<div class="theme-seg" role="group" aria-label="Choose Epic Errands theme">
|
| 18 |
+
<button class="theme-btn" type="button" data-app-theme="classroom" aria-pressed="false">
|
| 19 |
+
<span class="swatch" style="background:#e9382d"></span>
|
| 20 |
+
<span class="label">Classroom</span>
|
| 21 |
+
</button>
|
| 22 |
+
<button class="theme-btn" type="button" data-app-theme="comic" aria-pressed="false">
|
| 23 |
+
<span class="swatch" style="background:#f6a81d"></span>
|
| 24 |
+
<span class="label">Comic</span>
|
| 25 |
+
</button>
|
| 26 |
+
<button class="theme-btn" type="button" data-app-theme="questbook" aria-pressed="false">
|
| 27 |
+
<span class="swatch" style="background:#c8a25c"></span>
|
| 28 |
+
<span class="label">Questbook</span>
|
| 29 |
+
</button>
|
| 30 |
+
</div>
|
| 31 |
+
</nav>
|
| 32 |
+
<main class="stage" aria-label="Epic Errands V1 generated media prototype">
|
| 33 |
+
<div id="app"></div>
|
| 34 |
+
</main>
|
| 35 |
+
<script>
|
| 36 |
+
window.EPIC_ASSET_BASE = "/frontend/assets/";
|
| 37 |
+
</script>
|
| 38 |
<script src="/frontend/scripts/app.js"></script>
|
| 39 |
</body>
|
| 40 |
</html>
|
frontend/scripts/app.js
CHANGED
|
@@ -1,88 +1,165 @@
|
|
| 1 |
(function () {
|
| 2 |
const ASSET_BASE = window.EPIC_ASSET_BASE || "assets/";
|
| 3 |
const EMBEDDED_ASSETS = window.EPIC_ASSETS || {};
|
| 4 |
-
const root = document.getElementById("
|
| 5 |
|
| 6 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
classroom: {
|
| 8 |
label: "Classroom",
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
rewardNoun: "badge",
|
| 14 |
waitingCopy: "Waiting for Classroom Captain approval.",
|
| 15 |
-
copy: "Bright school adventure with stickers, notebooks, and sunny goals.",
|
| 16 |
-
},
|
| 17 |
-
magical: {
|
| 18 |
-
label: "Quest",
|
| 19 |
-
icon: "auto_stories",
|
| 20 |
-
goalNoun: "Quest",
|
| 21 |
-
parentTitle: "Quest Keeper",
|
| 22 |
-
kidTitle: "Young Adventurer",
|
| 23 |
-
rewardNoun: "crest",
|
| 24 |
-
waitingCopy: "Waiting for Quest Keeper approval.",
|
| 25 |
-
copy: "Premium questbook fantasy with warm paper, ink lines, and earned crests.",
|
| 26 |
},
|
| 27 |
comic: {
|
| 28 |
label: "Comic",
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
rewardNoun: "starburst",
|
| 34 |
waitingCopy: "Waiting for Comic Editor approval.",
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
},
|
| 37 |
};
|
| 38 |
|
| 39 |
-
const
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
};
|
| 44 |
|
| 45 |
const state = {
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
defaultStyleId: "magical",
|
| 50 |
-
parentAvatarReady: true,
|
| 51 |
-
kidAvatarReady: true,
|
| 52 |
goals: [
|
| 53 |
{
|
| 54 |
id: "goal-1",
|
| 55 |
-
|
| 56 |
-
|
| 57 |
kidCompletionState: "completed",
|
| 58 |
parentRewardState: "waiting_for_approval",
|
| 59 |
},
|
| 60 |
{
|
| 61 |
id: "goal-2",
|
| 62 |
-
|
| 63 |
-
|
| 64 |
kidCompletionState: "in_progress",
|
| 65 |
parentRewardState: "not_reviewed",
|
| 66 |
},
|
| 67 |
{
|
| 68 |
id: "goal-3",
|
| 69 |
-
|
| 70 |
-
|
| 71 |
kidCompletionState: "not_started",
|
| 72 |
parentRewardState: "not_reviewed",
|
| 73 |
},
|
| 74 |
],
|
| 75 |
};
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
function asset(name) {
|
| 78 |
if (EMBEDDED_ASSETS[name]) return EMBEDDED_ASSETS[name];
|
| 79 |
return `${ASSET_BASE}${name}`;
|
| 80 |
}
|
| 81 |
|
| 82 |
-
function icon(name) {
|
| 83 |
-
return `<span class="material-symbols-rounded" aria-hidden="true">${name}</span>`;
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
function escapeHtml(value) {
|
| 87 |
return String(value)
|
| 88 |
.replace(/&/g, "&")
|
|
@@ -92,410 +169,261 @@
|
|
| 92 |
.replace(/'/g, "'");
|
| 93 |
}
|
| 94 |
|
| 95 |
-
function
|
| 96 |
-
return
|
| 97 |
-
.trim()
|
| 98 |
-
.split(/\s+/)
|
| 99 |
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
| 100 |
-
.join(" ");
|
| 101 |
}
|
| 102 |
|
| 103 |
-
function
|
| 104 |
-
return
|
| 105 |
}
|
| 106 |
|
| 107 |
-
function
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
}
|
| 110 |
|
| 111 |
-
function
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
|
| 115 |
-
function
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
}
|
| 118 |
|
| 119 |
-
function
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
if (goal.styleId === "comic" && lower.includes("laundry")) return `${style.goalNoun}: Laundry Launch`;
|
| 124 |
-
if (lower.includes("read")) return `${style.goalNoun}: Reading Lantern`;
|
| 125 |
-
return `${style.goalNoun}: ${titleCase(goal.ordinaryGoal)}`;
|
| 126 |
}
|
| 127 |
|
| 128 |
-
function
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
if (goal.styleId === "comic") return `Pow ${style.rewardNoun}`;
|
| 132 |
-
return `Helper ${style.rewardNoun}`;
|
| 133 |
}
|
| 134 |
|
| 135 |
-
function
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
if (goal.kidCompletionState === "in_progress") return "working_on_it";
|
| 139 |
-
return "ready_to_start";
|
| 140 |
}
|
| 141 |
|
| 142 |
-
function
|
| 143 |
-
const
|
| 144 |
-
|
| 145 |
-
if (
|
| 146 |
-
|
| 147 |
-
return `<span class="chip">Ready</span>`;
|
| 148 |
}
|
| 149 |
|
| 150 |
-
function
|
| 151 |
-
const
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
"Build the first brave piece",
|
| 156 |
-
"Check the questbook notes",
|
| 157 |
-
];
|
| 158 |
-
}
|
| 159 |
-
if (goal.styleId === "comic") {
|
| 160 |
-
return ["Sort the pile", "Fold the hero gear", "Launch it to the drawer"];
|
| 161 |
-
}
|
| 162 |
-
return ["Find your reading spot", "Read the next pages", "Tell the best part"];
|
| 163 |
}
|
| 164 |
|
| 165 |
-
function
|
| 166 |
-
const
|
| 167 |
return `
|
| 168 |
-
<
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
</div>
|
| 173 |
-
|
| 174 |
-
<section class="panel" data-design-id="style-mode-picker" aria-labelledby="style-label">
|
| 175 |
-
<h2 class="section-label" id="style-label">App theme</h2>
|
| 176 |
-
<div class="style-grid">
|
| 177 |
-
${Object.entries(styles).map(([id, item]) => `
|
| 178 |
-
<button class="style-option" data-action="select-style" data-style="${id}" aria-pressed="${state.defaultStyleId === id}">
|
| 179 |
-
<span class="style-mark">${icon(item.icon)}</span>
|
| 180 |
-
<span>
|
| 181 |
-
<span class="style-name">${item.label}</span>
|
| 182 |
-
<span class="style-meta">${item.parentTitle} / ${item.rewardNoun}. ${item.copy}</span>
|
| 183 |
-
</span>
|
| 184 |
-
<span class="selected-dot" aria-hidden="true"></span>
|
| 185 |
-
</button>
|
| 186 |
-
`).join("")}
|
| 187 |
-
</div>
|
| 188 |
-
</section>
|
| 189 |
-
|
| 190 |
-
<section class="panel" data-design-id="avatar-creator" aria-labelledby="avatar-label">
|
| 191 |
-
<h2 class="section-label" id="avatar-label">Avatars</h2>
|
| 192 |
-
<div class="avatar-grid">
|
| 193 |
-
<div class="avatar-slot">
|
| 194 |
-
<div>
|
| 195 |
-
<div class="avatar-face">${icon("shield_person")}</div>
|
| 196 |
-
<p class="avatar-name">Maya</p>
|
| 197 |
-
<p class="avatar-title">${escapeHtml(style.parentTitle)}</p>
|
| 198 |
-
</div>
|
| 199 |
-
</div>
|
| 200 |
-
<div class="avatar-slot">
|
| 201 |
-
<div>
|
| 202 |
-
<div class="avatar-face">${icon("face_3")}</div>
|
| 203 |
-
<p class="avatar-name">Ari</p>
|
| 204 |
-
<p class="avatar-title">${escapeHtml(style.kidTitle)}</p>
|
| 205 |
-
</div>
|
| 206 |
-
</div>
|
| 207 |
-
</div>
|
| 208 |
-
<p class="privacy-note">${icon("lock")} Photos create session-only avatars in this prototype. Nothing is persisted.</p>
|
| 209 |
-
</section>
|
| 210 |
-
|
| 211 |
-
<section class="counter-grid" aria-label="Goal counts">
|
| 212 |
-
<div class="counter">
|
| 213 |
-
<strong>${activeGoals()}</strong>
|
| 214 |
-
<span>active goals</span>
|
| 215 |
-
</div>
|
| 216 |
-
<div class="counter">
|
| 217 |
-
<strong>${awaitingApproval()}</strong>
|
| 218 |
-
<span>awaiting approval</span>
|
| 219 |
-
</div>
|
| 220 |
-
</section>
|
| 221 |
-
</section>
|
| 222 |
`;
|
| 223 |
}
|
| 224 |
|
| 225 |
-
function
|
| 226 |
-
const
|
|
|
|
| 227 |
return `
|
| 228 |
-
<
|
| 229 |
-
<div class="
|
| 230 |
-
<
|
| 231 |
-
<
|
| 232 |
</div>
|
| 233 |
-
<
|
| 234 |
-
<
|
| 235 |
-
<
|
| 236 |
-
|
| 237 |
-
<
|
| 238 |
-
|
| 239 |
-
<
|
| 240 |
-
<input id="new-goal-input" name="ordinaryGoal" type="text" autocomplete="off" placeholder="Practice spelling words">
|
| 241 |
-
<button class="create-button" type="submit">${icon("add_task")} Add</button>
|
| 242 |
-
</div>
|
| 243 |
-
</form>
|
| 244 |
-
<div class="goal-list" data-design-id="parent-goal-list">
|
| 245 |
-
${state.goals.map((goal) => {
|
| 246 |
-
const style = goalStyle(goal);
|
| 247 |
-
const canApprove = goal.parentRewardState === "waiting_for_approval";
|
| 248 |
-
const approved = goal.parentRewardState === "approved_reward_given";
|
| 249 |
-
return `
|
| 250 |
-
<article class="goal-row" data-goal-id="${goal.id}">
|
| 251 |
-
<div class="goal-main">
|
| 252 |
-
<div class="goal-title-line">
|
| 253 |
-
<span class="goal-name">${escapeHtml(goalTitle(goal))}</span>
|
| 254 |
-
<span class="chip">${escapeHtml(style.label)}</span>
|
| 255 |
-
${stateChip(goal)}
|
| 256 |
-
</div>
|
| 257 |
-
<p class="goal-plain">Ordinary goal: ${escapeHtml(goal.ordinaryGoal)}</p>
|
| 258 |
-
<p class="goal-plain">Reward: ${escapeHtml(rewardLabel(goal))}</p>
|
| 259 |
-
</div>
|
| 260 |
-
<button class="approve-button" data-design-id="parent-approve-action" data-action="approve-goal" data-goal-id="${goal.id}" ${canApprove ? "" : "disabled"}>
|
| 261 |
-
${approved ? "Approved" : "Approve"}
|
| 262 |
-
</button>
|
| 263 |
-
</article>
|
| 264 |
-
`;
|
| 265 |
-
}).join("")}
|
| 266 |
</div>
|
| 267 |
-
</
|
| 268 |
`;
|
| 269 |
}
|
| 270 |
|
| 271 |
-
function
|
| 272 |
return `
|
| 273 |
-
<
|
| 274 |
-
<div class="
|
| 275 |
-
<
|
| 276 |
-
<
|
|
|
|
|
|
|
|
|
|
| 277 |
</div>
|
| 278 |
-
<div class="
|
| 279 |
-
${state.goals
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
</div>
|
| 289 |
-
</
|
| 290 |
`;
|
| 291 |
}
|
| 292 |
|
| 293 |
-
function
|
| 294 |
-
const
|
| 295 |
-
const
|
| 296 |
-
const
|
| 297 |
-
const
|
| 298 |
-
const
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
` : "";
|
| 303 |
return `
|
| 304 |
-
<
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
<
|
| 308 |
-
<span class="
|
| 309 |
</div>
|
| 310 |
-
<div class="
|
| 311 |
-
<
|
| 312 |
-
<
|
| 313 |
-
<
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
<
|
| 323 |
-
${icon("volume_up")} Audio in V1.1
|
| 324 |
-
</button>
|
| 325 |
-
<button class="complete-button" data-action="complete-goal" data-goal-id="${goal.id}" ${goal.kidCompletionState === "completed" ? "disabled" : ""}>
|
| 326 |
-
${goal.kidCompletionState === "completed" ? "Completed" : `Mark ${style.goalNoun} complete`}
|
| 327 |
-
</button>
|
| 328 |
</div>
|
| 329 |
-
${isWaiting ? `
|
| 330 |
-
<div class="waiting-panel" data-design-id="waiting-for-parent-approval">
|
| 331 |
-
<strong>${escapeHtml(style.waitingCopy)}</strong>
|
| 332 |
-
<span>Your ${escapeHtml(style.rewardNoun)} unlocks when ${escapeHtml(style.parentTitle)} approves.</span>
|
| 333 |
-
</div>
|
| 334 |
-
` : ""}
|
| 335 |
-
${rewarded ? `
|
| 336 |
-
<div class="reward-panel">
|
| 337 |
-
<strong>${escapeHtml(rewardLabel(goal))} earned!</strong>
|
| 338 |
-
<span>${escapeHtml(style.parentTitle)} approved this goal.</span>
|
| 339 |
-
</div>
|
| 340 |
-
` : ""}
|
| 341 |
</div>
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
`;
|
| 344 |
}
|
| 345 |
|
| 346 |
-
function
|
| 347 |
-
const selected = state.goals.find((goal) => goal.id === state.selectedGoalId);
|
| 348 |
-
if (!selected || state.activeTab !== "kid") return "";
|
| 349 |
return `
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
<
|
| 353 |
-
|
| 354 |
-
</div>
|
| 355 |
</div>
|
|
|
|
|
|
|
| 356 |
`;
|
| 357 |
}
|
| 358 |
|
| 359 |
-
function
|
| 360 |
-
const
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
["diy", "DIY Lab", "experiment"],
|
| 365 |
-
];
|
| 366 |
return `
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
`;
|
| 380 |
}
|
| 381 |
|
| 382 |
function render() {
|
| 383 |
-
|
| 384 |
-
const
|
| 385 |
-
? renderParentGoals()
|
| 386 |
-
: state.activeTab === "kid"
|
| 387 |
-
? renderKidGoals()
|
| 388 |
-
: renderHome();
|
| 389 |
-
|
| 390 |
root.innerHTML = `
|
| 391 |
-
<
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
</div>
|
| 398 |
-
<span class="status-pill">${icon(style.icon)} ${escapeHtml(style.label)}</span>
|
| 399 |
-
</div>
|
| 400 |
-
</header>
|
| 401 |
-
<div class="screen-region">${screen}</div>
|
| 402 |
-
${renderGoalModal()}
|
| 403 |
-
${renderNav()}
|
| 404 |
-
</div>
|
| 405 |
`;
|
| 406 |
}
|
| 407 |
|
| 408 |
-
|
| 409 |
-
const
|
| 410 |
-
if (
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
root.addEventListener("click", (event) => {
|
| 414 |
-
const target = event.target.closest("[data-action]");
|
| 415 |
-
if (!target) return;
|
| 416 |
-
|
| 417 |
-
const action = target.dataset.action;
|
| 418 |
-
if (action === "select-style") {
|
| 419 |
-
state.defaultStyleId = target.dataset.style || state.defaultStyleId;
|
| 420 |
-
render();
|
| 421 |
-
return;
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
if (action === "nav") {
|
| 425 |
-
if (target.getAttribute("aria-disabled") === "true") return;
|
| 426 |
-
state.activeTab = target.dataset.tab || "home";
|
| 427 |
-
if (state.activeTab !== "kid") state.selectedGoalId = null;
|
| 428 |
-
render();
|
| 429 |
-
return;
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
if (action === "open-goal-card") {
|
| 433 |
-
state.selectedGoalId = target.dataset.goalId || state.selectedGoalId;
|
| 434 |
-
render();
|
| 435 |
-
return;
|
| 436 |
-
}
|
| 437 |
-
|
| 438 |
-
if (action === "close-goal-card") {
|
| 439 |
-
state.selectedGoalId = null;
|
| 440 |
-
render();
|
| 441 |
return;
|
| 442 |
}
|
| 443 |
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
updateGoal(goalId, (goal) => {
|
| 447 |
-
goal.kidCompletionState = "completed";
|
| 448 |
-
if (goal.parentRewardState !== "approved_reward_given") {
|
| 449 |
-
goal.parentRewardState = "waiting_for_approval";
|
| 450 |
-
}
|
| 451 |
-
});
|
| 452 |
-
state.selectedGoalId = goalId || state.selectedGoalId;
|
| 453 |
-
render();
|
| 454 |
-
return;
|
| 455 |
-
}
|
| 456 |
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
}
|
| 463 |
-
});
|
| 464 |
-
render();
|
| 465 |
-
}
|
| 466 |
-
});
|
| 467 |
-
|
| 468 |
-
root.addEventListener("submit", (event) => {
|
| 469 |
-
const form = event.target.closest("[data-design-id='parent-goal-create']");
|
| 470 |
-
if (!form) return;
|
| 471 |
-
event.preventDefault();
|
| 472 |
-
|
| 473 |
-
const input = form.querySelector("input[name='ordinaryGoal']");
|
| 474 |
-
const ordinaryGoal = input ? input.value.trim() : "";
|
| 475 |
-
if (!ordinaryGoal) {
|
| 476 |
-
if (input) input.focus();
|
| 477 |
-
return;
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
const goal = {
|
| 481 |
-
id: `goal-${state.nextGoalNumber}`,
|
| 482 |
-
ordinaryGoal,
|
| 483 |
-
styleId: state.defaultStyleId,
|
| 484 |
-
kidCompletionState: "not_started",
|
| 485 |
-
parentRewardState: "not_reviewed",
|
| 486 |
-
};
|
| 487 |
-
state.nextGoalNumber += 1;
|
| 488 |
-
state.goals.unshift(goal);
|
| 489 |
-
state.selectedGoalId = null;
|
| 490 |
-
render();
|
| 491 |
-
});
|
| 492 |
-
|
| 493 |
-
window.addEventListener("keydown", (event) => {
|
| 494 |
-
if (event.key === "Escape" && state.selectedGoalId) {
|
| 495 |
-
state.selectedGoalId = null;
|
| 496 |
-
render();
|
| 497 |
-
}
|
| 498 |
});
|
| 499 |
|
| 500 |
-
|
| 501 |
})();
|
|
|
|
| 1 |
(function () {
|
| 2 |
const ASSET_BASE = window.EPIC_ASSET_BASE || "assets/";
|
| 3 |
const EMBEDDED_ASSETS = window.EPIC_ASSETS || {};
|
| 4 |
+
const root = document.getElementById("app");
|
| 5 |
|
| 6 |
+
const themeToToken = {
|
| 7 |
+
classroom: "storybook",
|
| 8 |
+
comic: "comic",
|
| 9 |
+
questbook: "atelier",
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
const themes = {
|
| 13 |
classroom: {
|
| 14 |
label: "Classroom",
|
| 15 |
+
kidRole: "Star Student",
|
| 16 |
+
parentRole: "Classroom Captain",
|
| 17 |
+
cardLabel: "Classroom Card",
|
| 18 |
+
proofLabel: "Sticker earned",
|
|
|
|
| 19 |
waitingCopy: "Waiting for Classroom Captain approval.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
},
|
| 21 |
comic: {
|
| 22 |
label: "Comic",
|
| 23 |
+
kidRole: "Panel Lead",
|
| 24 |
+
parentRole: "Comic Editor",
|
| 25 |
+
cardLabel: "Comic Card",
|
| 26 |
+
proofLabel: "Badge earned",
|
|
|
|
| 27 |
waitingCopy: "Waiting for Comic Editor approval.",
|
| 28 |
+
},
|
| 29 |
+
questbook: {
|
| 30 |
+
label: "Questbook",
|
| 31 |
+
kidRole: "Young Adventurer",
|
| 32 |
+
parentRole: "Quest Keeper",
|
| 33 |
+
cardLabel: "Questbook Card",
|
| 34 |
+
proofLabel: "Crest earned",
|
| 35 |
+
waitingCopy: "Waiting for Quest Keeper approval.",
|
| 36 |
},
|
| 37 |
};
|
| 38 |
|
| 39 |
+
const generatedVariants = {
|
| 40 |
+
"clean-room": {
|
| 41 |
+
questbook: {
|
| 42 |
+
title: "Questbook: Room Cleanup",
|
| 43 |
+
narration: "You find a magical book that says, 'Before dinner, tidy your room and earn a shiny star!'",
|
| 44 |
+
rewardLabel: "Star",
|
| 45 |
+
image: "generated-v2/clean-room-questbook-d35e6ee10c.png",
|
| 46 |
+
audio: "generated-v2/clean-room-questbook-01493f395c.wav",
|
| 47 |
+
traceId: "epic-errands-v1-live-clean-room-questbook",
|
| 48 |
+
},
|
| 49 |
+
classroom: {
|
| 50 |
+
title: "Room Cleanup",
|
| 51 |
+
narration: "You tidy your room before dinner, making it ready for a fun day.",
|
| 52 |
+
rewardLabel: "Clean Room",
|
| 53 |
+
image: "generated-v2/clean-room-classroom-c5cb506427.png",
|
| 54 |
+
audio: "generated-v2/clean-room-classroom-cf6f43d6ce.wav",
|
| 55 |
+
traceId: "epic-errands-v1-live-clean-room-classroom",
|
| 56 |
+
},
|
| 57 |
+
comic: {
|
| 58 |
+
title: "Comic Cleanup",
|
| 59 |
+
narration: "You find a superhero cape in your room. It's time to put it back where it belongs before dinner.",
|
| 60 |
+
rewardLabel: "Superhero Cleanup",
|
| 61 |
+
image: "generated-v2/clean-room-comic-9bce2b2b21.png",
|
| 62 |
+
audio: "generated-v2/clean-room-comic-88b770a8dc.wav",
|
| 63 |
+
traceId: "epic-errands-v1-live-clean-room-comic",
|
| 64 |
+
},
|
| 65 |
+
},
|
| 66 |
+
"project-outline": {
|
| 67 |
+
questbook: {
|
| 68 |
+
title: "Questbook Quest",
|
| 69 |
+
narration: "You have a class project outline to finish. Gather your supplies and write it out step by step.",
|
| 70 |
+
rewardLabel: "Great job!",
|
| 71 |
+
image: "generated-v2/project-outline-questbook-e1d5ced2e8.png",
|
| 72 |
+
audio: "generated-v2/project-outline-questbook-bddf03af1a.wav",
|
| 73 |
+
traceId: "epic-errands-v1-live-project-outline-questbook",
|
| 74 |
+
},
|
| 75 |
+
classroom: {
|
| 76 |
+
title: "Classroom Project",
|
| 77 |
+
narration: "You finish your class project outline and get a high-five from your teacher.",
|
| 78 |
+
rewardLabel: "Great job!",
|
| 79 |
+
image: "generated-v2/project-outline-classroom-3783f10bae.png",
|
| 80 |
+
audio: "generated-v2/project-outline-classroom-70a886be5a.wav",
|
| 81 |
+
traceId: "epic-errands-v1-live-project-outline-classroom",
|
| 82 |
+
},
|
| 83 |
+
comic: {
|
| 84 |
+
title: "Comic Comic",
|
| 85 |
+
narration: "You finish your class project outline and get a shiny comic badge!",
|
| 86 |
+
rewardLabel: "Comic Badge",
|
| 87 |
+
image: "generated-v2/project-outline-comic-4f9eaa443a.png",
|
| 88 |
+
audio: "generated-v2/project-outline-comic-ef3f180846.wav",
|
| 89 |
+
traceId: "epic-errands-v1-live-project-outline-comic",
|
| 90 |
+
},
|
| 91 |
+
},
|
| 92 |
+
"read-20": {
|
| 93 |
+
questbook: {
|
| 94 |
+
title: "Questbook: Read 20 Minutes",
|
| 95 |
+
narration: "You've found a magical book that whispers stories. Spend 20 minutes reading to hear its magic.",
|
| 96 |
+
rewardLabel: "Storytime Star",
|
| 97 |
+
image: "generated-v2/read-20-questbook-39aac8ba25.png",
|
| 98 |
+
audio: "generated-v2/read-20-questbook-5805518930.wav",
|
| 99 |
+
traceId: "epic-errands-v1-live-read-20-questbook",
|
| 100 |
+
},
|
| 101 |
+
classroom: {
|
| 102 |
+
title: "Read for 20 Minutes",
|
| 103 |
+
narration: "Read for 20 minutes and earn a sticker.",
|
| 104 |
+
rewardLabel: "Sticker",
|
| 105 |
+
image: "generated-v2/read-20-classroom-7c13f43782.png",
|
| 106 |
+
audio: "generated-v2/read-20-classroom-cca67b8288.wav",
|
| 107 |
+
traceId: "epic-errands-v1-live-read-20-classroom",
|
| 108 |
+
},
|
| 109 |
+
comic: {
|
| 110 |
+
title: "Comic Quest",
|
| 111 |
+
narration: "Read for 20 minutes and earn a comic badge!",
|
| 112 |
+
rewardLabel: "Comic Badge",
|
| 113 |
+
image: "generated-v2/read-20-comic-b5ae2d561a.png",
|
| 114 |
+
audio: "generated-v2/read-20-comic-7ec8c91bf8.wav",
|
| 115 |
+
traceId: "epic-errands-v1-live-read-20-comic",
|
| 116 |
+
},
|
| 117 |
+
},
|
| 118 |
};
|
| 119 |
|
| 120 |
const state = {
|
| 121 |
+
activeTheme: "questbook",
|
| 122 |
+
view: "kid",
|
| 123 |
+
selectedGoalId: "goal-1",
|
|
|
|
|
|
|
|
|
|
| 124 |
goals: [
|
| 125 |
{
|
| 126 |
id: "goal-1",
|
| 127 |
+
assetKey: "clean-room",
|
| 128 |
+
plainGoal: "Clean up my room before dinner",
|
| 129 |
kidCompletionState: "completed",
|
| 130 |
parentRewardState: "waiting_for_approval",
|
| 131 |
},
|
| 132 |
{
|
| 133 |
id: "goal-2",
|
| 134 |
+
assetKey: "project-outline",
|
| 135 |
+
plainGoal: "Finish my class project outline",
|
| 136 |
kidCompletionState: "in_progress",
|
| 137 |
parentRewardState: "not_reviewed",
|
| 138 |
},
|
| 139 |
{
|
| 140 |
id: "goal-3",
|
| 141 |
+
assetKey: "read-20",
|
| 142 |
+
plainGoal: "Read for 20 minutes",
|
| 143 |
kidCompletionState: "not_started",
|
| 144 |
parentRewardState: "not_reviewed",
|
| 145 |
},
|
| 146 |
],
|
| 147 |
};
|
| 148 |
|
| 149 |
+
const icons = {
|
| 150 |
+
check: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m5 12.5 4.2 4L19 7" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
| 151 |
+
image: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 7.5A2.5 2.5 0 0 1 6.5 5h11A2.5 2.5 0 0 1 20 7.5v9A2.5 2.5 0 0 1 17.5 19h-11A2.5 2.5 0 0 1 4 16.5v-9Z" stroke="currentColor" stroke-width="2"/><path d="m7 16 3.2-3.3 2.2 2.1 2.3-2.8L18 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.5 9h.01" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>',
|
| 152 |
+
sound: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 10v4h4l5 4V6l-5 4H4Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M16 9.5c.8.7 1.2 1.5 1.2 2.5s-.4 1.8-1.2 2.5M18.6 7c1.5 1.3 2.3 3 2.3 5s-.8 3.7-2.3 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
|
| 153 |
+
sparkle: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 3.5 13.8 9l5.7 1.7-5.7 1.8L12 18l-1.8-5.5-5.7-1.8L10.2 9 12 3.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>',
|
| 154 |
+
parent: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM16.5 12a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM3.5 20c.5-3.8 2.1-5.7 4.5-5.7s4 1.9 4.5 5.7M13 19.5c.4-2.7 1.6-4 3.5-4 1.8 0 3 1.3 3.5 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
|
| 155 |
+
kid: '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 12a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7ZM5.5 20c.7-3.8 2.9-5.8 6.5-5.8s5.8 2 6.5 5.8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
function asset(name) {
|
| 159 |
if (EMBEDDED_ASSETS[name]) return EMBEDDED_ASSETS[name];
|
| 160 |
return `${ASSET_BASE}${name}`;
|
| 161 |
}
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
function escapeHtml(value) {
|
| 164 |
return String(value)
|
| 165 |
.replace(/&/g, "&")
|
|
|
|
| 169 |
.replace(/'/g, "'");
|
| 170 |
}
|
| 171 |
|
| 172 |
+
function goalById(id) {
|
| 173 |
+
return state.goals.find((goal) => goal.id === id) || state.goals[0];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
}
|
| 175 |
|
| 176 |
+
function activeCard(goal, themeId = state.activeTheme) {
|
| 177 |
+
return generatedVariants[goal.assetKey]?.[themeId] || generatedVariants[goal.assetKey]?.questbook;
|
| 178 |
}
|
| 179 |
|
| 180 |
+
function statusLabel(goal) {
|
| 181 |
+
if (goal.parentRewardState === "approved") return "Approved";
|
| 182 |
+
if (goal.parentRewardState === "waiting_for_approval") return "Review";
|
| 183 |
+
if (goal.kidCompletionState === "completed") return "Done";
|
| 184 |
+
if (goal.kidCompletionState === "in_progress") return "Active";
|
| 185 |
+
return "Ready";
|
| 186 |
}
|
| 187 |
|
| 188 |
+
function parentStatusCopy(goal) {
|
| 189 |
+
if (goal.parentRewardState === "approved") return "Reward approved";
|
| 190 |
+
if (goal.parentRewardState === "waiting_for_approval") return "Ready for parent review";
|
| 191 |
+
if (goal.kidCompletionState === "completed") return "Completed by kid";
|
| 192 |
+
return "Not submitted yet";
|
| 193 |
}
|
| 194 |
|
| 195 |
+
function setTheme(themeId) {
|
| 196 |
+
if (!themes[themeId]) return;
|
| 197 |
+
state.activeTheme = themeId;
|
| 198 |
+
document.documentElement.dataset.theme = themeToToken[themeId];
|
| 199 |
+
updateThemeRail();
|
| 200 |
+
render();
|
| 201 |
}
|
| 202 |
|
| 203 |
+
function updateThemeRail() {
|
| 204 |
+
document.querySelectorAll(".theme-btn").forEach((button) => {
|
| 205 |
+
button.setAttribute("aria-pressed", String(button.dataset.appTheme === state.activeTheme));
|
| 206 |
+
});
|
|
|
|
|
|
|
|
|
|
| 207 |
}
|
| 208 |
|
| 209 |
+
function setView(view) {
|
| 210 |
+
state.view = view;
|
| 211 |
+
render();
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
|
| 214 |
+
function selectGoal(goalId) {
|
| 215 |
+
state.selectedGoalId = goalId;
|
| 216 |
+
render();
|
|
|
|
|
|
|
| 217 |
}
|
| 218 |
|
| 219 |
+
function completeSelectedGoal() {
|
| 220 |
+
const goal = goalById(state.selectedGoalId);
|
| 221 |
+
goal.kidCompletionState = "completed";
|
| 222 |
+
if (goal.parentRewardState !== "approved") goal.parentRewardState = "waiting_for_approval";
|
| 223 |
+
render();
|
|
|
|
| 224 |
}
|
| 225 |
|
| 226 |
+
function approveGoal(goalId) {
|
| 227 |
+
const goal = goalById(goalId);
|
| 228 |
+
goal.kidCompletionState = "completed";
|
| 229 |
+
goal.parentRewardState = "approved";
|
| 230 |
+
render();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
}
|
| 232 |
|
| 233 |
+
function flourishCorners() {
|
| 234 |
+
const flourish = '<svg viewBox="0 0 60 60" fill="none"><path d="M8 52c16-3 19-10 20-22 1 12 5 19 24 22M8 8c16 3 19 10 20 22 1-12 5-19 24-22" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>';
|
| 235 |
return `
|
| 236 |
+
<span class="corner-flourish tl">${flourish}</span>
|
| 237 |
+
<span class="corner-flourish tr">${flourish}</span>
|
| 238 |
+
<span class="corner-flourish bl">${flourish}</span>
|
| 239 |
+
<span class="corner-flourish br">${flourish}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
`;
|
| 241 |
}
|
| 242 |
|
| 243 |
+
function headerHtml() {
|
| 244 |
+
const kidPressed = state.view === "kid";
|
| 245 |
+
const parentPressed = state.view === "parent";
|
| 246 |
return `
|
| 247 |
+
<div class="app-head">
|
| 248 |
+
<div class="wordmark" aria-label="Epic Errands">
|
| 249 |
+
<span class="display">Epic Errands</span>
|
| 250 |
+
<span class="sub">${escapeHtml(themes[state.activeTheme].kidRole)} mode</span>
|
| 251 |
</div>
|
| 252 |
+
<div class="viewtoggle" role="group" aria-label="Choose view">
|
| 253 |
+
<button class="vt" type="button" data-action="view" data-view="kid" aria-pressed="${kidPressed}">
|
| 254 |
+
${icons.kid}<span>Kid</span>
|
| 255 |
+
</button>
|
| 256 |
+
<button class="vt" type="button" data-action="view" data-view="parent" aria-pressed="${parentPressed}">
|
| 257 |
+
${icons.parent}<span>Parent</span>
|
| 258 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
</div>
|
| 260 |
+
</div>
|
| 261 |
`;
|
| 262 |
}
|
| 263 |
|
| 264 |
+
function goalStripHtml() {
|
| 265 |
return `
|
| 266 |
+
<div class="section">
|
| 267 |
+
<div class="section__head">
|
| 268 |
+
<div class="section__num">3</div>
|
| 269 |
+
<div class="section__titles">
|
| 270 |
+
<div class="section__title">Loaded goals <span class="note">${escapeHtml(themes[state.activeTheme].label)}</span></div>
|
| 271 |
+
<div class="section__sub">Pick a plain goal to preview its generated card.</div>
|
| 272 |
+
</div>
|
| 273 |
</div>
|
| 274 |
+
<div class="goal-strip">
|
| 275 |
+
${state.goals
|
| 276 |
+
.map((goal, index) => {
|
| 277 |
+
const card = activeCard(goal);
|
| 278 |
+
const selected = goal.id === state.selectedGoalId;
|
| 279 |
+
const complete = goal.kidCompletionState === "completed";
|
| 280 |
+
return `
|
| 281 |
+
<button class="goal-button ${complete ? "is-complete" : ""}" type="button" data-action="select-goal" data-goal-id="${goal.id}" aria-pressed="${selected}">
|
| 282 |
+
<span class="goal-button__mark">${complete ? icons.check : index + 1}</span>
|
| 283 |
+
<span class="goal-button__copy">
|
| 284 |
+
<span class="goal-button__title">${escapeHtml(card.title)}</span>
|
| 285 |
+
<span class="goal-button__meta">${escapeHtml(goal.plainGoal)}</span>
|
| 286 |
+
</span>
|
| 287 |
+
<span class="goal-button__status">${escapeHtml(statusLabel(goal))}</span>
|
| 288 |
+
</button>
|
| 289 |
+
`;
|
| 290 |
+
})
|
| 291 |
+
.join("")}
|
| 292 |
</div>
|
| 293 |
+
</div>
|
| 294 |
`;
|
| 295 |
}
|
| 296 |
|
| 297 |
+
function selectedCardHtml() {
|
| 298 |
+
const goal = goalById(state.selectedGoalId);
|
| 299 |
+
const card = activeCard(goal);
|
| 300 |
+
const theme = themes[state.activeTheme];
|
| 301 |
+
const complete = goal.kidCompletionState === "completed";
|
| 302 |
+
const approved = goal.parentRewardState === "approved";
|
| 303 |
+
const waiting = goal.parentRewardState === "waiting_for_approval";
|
| 304 |
+
const buttonText = complete ? "Send Again" : "I Did It";
|
| 305 |
+
|
|
|
|
| 306 |
return `
|
| 307 |
+
<section class="card-preview" aria-label="Generated goal card">
|
| 308 |
+
<div class="hero">
|
| 309 |
+
<img src="${asset(card.image)}" alt="${escapeHtml(card.title)} generated ${escapeHtml(theme.label)} artwork">
|
| 310 |
+
<span class="hero__rank">${escapeHtml(theme.cardLabel)}</span>
|
| 311 |
+
<span class="hero__points">${icons.sparkle}${escapeHtml(card.rewardLabel)}</span>
|
| 312 |
</div>
|
| 313 |
+
<div class="quest-headline">
|
| 314 |
+
<div class="eyebrow">${escapeHtml(goal.plainGoal)}</div>
|
| 315 |
+
<h1 class="quest-title">${escapeHtml(card.title)}</h1>
|
| 316 |
+
<p class="narration">${escapeHtml(card.narration)}</p>
|
| 317 |
+
</div>
|
| 318 |
+
<div class="media-panel">
|
| 319 |
+
<div class="audio-shell">
|
| 320 |
+
<audio controls preload="metadata" src="${asset(card.audio)}"></audio>
|
| 321 |
+
</div>
|
| 322 |
+
<div class="asset-pills" aria-label="Generated assets">
|
| 323 |
+
<span class="asset-pill">${icons.image} Image</span>
|
| 324 |
+
<span class="asset-pill">${icons.sound} Audio</span>
|
| 325 |
+
<span class="asset-pill">${icons.sparkle} ${escapeHtml(theme.proofLabel)}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
</div>
|
| 328 |
+
<div class="action-row">
|
| 329 |
+
<button class="btn-primary" type="button" data-action="complete-selected" ${approved ? "disabled" : ""}>
|
| 330 |
+
${icons.check}<span>${approved ? "Reward Approved" : buttonText}</span>
|
| 331 |
+
</button>
|
| 332 |
+
${waiting ? `<p class="state-note"><strong>${escapeHtml(theme.parentRole)}:</strong> ${escapeHtml(theme.waitingCopy)}</p>` : ""}
|
| 333 |
+
${approved ? `<p class="state-note"><strong>Reward approved.</strong> The kid card can now show the earned reward in the UI.</p>` : ""}
|
| 334 |
+
</div>
|
| 335 |
+
</section>
|
| 336 |
`;
|
| 337 |
}
|
| 338 |
|
| 339 |
+
function kidViewHtml() {
|
|
|
|
|
|
|
| 340 |
return `
|
| 341 |
+
${headerHtml()}
|
| 342 |
+
<div class="lead">
|
| 343 |
+
<h1>Today's <span class="u">epic errands</span></h1>
|
| 344 |
+
<p>Generated text, artwork, and voice are loaded into the same card system.</p>
|
|
|
|
| 345 |
</div>
|
| 346 |
+
${goalStripHtml()}
|
| 347 |
+
${selectedCardHtml()}
|
| 348 |
`;
|
| 349 |
}
|
| 350 |
|
| 351 |
+
function parentViewHtml() {
|
| 352 |
+
const completeCount = state.goals.filter((goal) => goal.kidCompletionState === "completed").length;
|
| 353 |
+
const waitingCount = state.goals.filter((goal) => goal.parentRewardState === "waiting_for_approval").length;
|
| 354 |
+
const approvedCount = state.goals.filter((goal) => goal.parentRewardState === "approved").length;
|
| 355 |
+
|
|
|
|
|
|
|
| 356 |
return `
|
| 357 |
+
${headerHtml()}
|
| 358 |
+
<div class="lead">
|
| 359 |
+
<h1>${escapeHtml(themes[state.activeTheme].parentRole)}</h1>
|
| 360 |
+
<p>Review generated cards and approve rewards after completion.</p>
|
| 361 |
+
</div>
|
| 362 |
+
<div class="summary-grid" aria-label="Parent review summary">
|
| 363 |
+
<div class="summary-tile"><b>${completeCount}</b><span>Completed</span></div>
|
| 364 |
+
<div class="summary-tile"><b>${waitingCount}</b><span>Review</span></div>
|
| 365 |
+
<div class="summary-tile"><b>${approvedCount}</b><span>Approved</span></div>
|
| 366 |
+
</div>
|
| 367 |
+
<section class="parent">
|
| 368 |
+
<div class="parent__head">
|
| 369 |
+
${icons.parent}
|
| 370 |
+
<h3>Reward Queue</h3>
|
| 371 |
+
</div>
|
| 372 |
+
<div class="parent-list">
|
| 373 |
+
${state.goals
|
| 374 |
+
.map((goal) => {
|
| 375 |
+
const card = activeCard(goal);
|
| 376 |
+
const canApprove = goal.parentRewardState === "waiting_for_approval";
|
| 377 |
+
return `
|
| 378 |
+
<article class="parent-row">
|
| 379 |
+
<div>
|
| 380 |
+
<div class="parent-row__title">${escapeHtml(card.title)}</div>
|
| 381 |
+
<div class="parent-row__meta">${escapeHtml(goal.plainGoal)} · ${escapeHtml(card.rewardLabel)}</div>
|
| 382 |
+
</div>
|
| 383 |
+
<div class="parent-actions">
|
| 384 |
+
<span class="parent-row__status">${escapeHtml(parentStatusCopy(goal))}</span>
|
| 385 |
+
<button class="btn-ghost" type="button" data-action="approve-goal" data-goal-id="${goal.id}" ${canApprove ? "" : "disabled"}>
|
| 386 |
+
${icons.check}<span>${goal.parentRewardState === "approved" ? "Approved" : "Approve"}</span>
|
| 387 |
+
</button>
|
| 388 |
+
</div>
|
| 389 |
+
</article>
|
| 390 |
+
`;
|
| 391 |
+
})
|
| 392 |
+
.join("")}
|
| 393 |
+
</div>
|
| 394 |
+
</section>
|
| 395 |
`;
|
| 396 |
}
|
| 397 |
|
| 398 |
function render() {
|
| 399 |
+
if (!root) return;
|
| 400 |
+
const body = state.view === "parent" ? parentViewHtml() : kidViewHtml();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
root.innerHTML = `
|
| 402 |
+
<article class="card screen-in ornate">
|
| 403 |
+
${flourishCorners()}
|
| 404 |
+
<div class="card-inner app-shell">
|
| 405 |
+
${body}
|
| 406 |
+
</div>
|
| 407 |
+
</article>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
`;
|
| 409 |
}
|
| 410 |
|
| 411 |
+
document.addEventListener("click", (event) => {
|
| 412 |
+
const themeButton = event.target.closest(".theme-btn");
|
| 413 |
+
if (themeButton) {
|
| 414 |
+
setTheme(themeButton.dataset.appTheme);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
return;
|
| 416 |
}
|
| 417 |
|
| 418 |
+
const actionButton = event.target.closest("[data-action]");
|
| 419 |
+
if (!actionButton || actionButton.disabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
+
const action = actionButton.dataset.action;
|
| 422 |
+
if (action === "view") setView(actionButton.dataset.view);
|
| 423 |
+
if (action === "select-goal") selectGoal(actionButton.dataset.goalId);
|
| 424 |
+
if (action === "complete-selected") completeSelectedGoal();
|
| 425 |
+
if (action === "approve-goal") approveGoal(actionButton.dataset.goalId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
});
|
| 427 |
|
| 428 |
+
setTheme(state.activeTheme);
|
| 429 |
})();
|
frontend/styles/app.css
CHANGED
|
@@ -1,913 +1,301 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
--font-quest: "Lora", Georgia, serif;
|
| 4 |
-
--font-body: "Nunito", system-ui, sans-serif;
|
| 5 |
-
--font-heading: var(--font-display);
|
| 6 |
-
--ink: #241d19;
|
| 7 |
-
--muted: #655f59;
|
| 8 |
-
--line: rgba(62, 47, 33, 0.16);
|
| 9 |
-
--paper: #fff8e8;
|
| 10 |
-
--surface: #ffffff;
|
| 11 |
-
--soft: #f5efe3;
|
| 12 |
-
--primary: #e3403a;
|
| 13 |
-
--primary-ink: #ffffff;
|
| 14 |
-
--success: #128a55;
|
| 15 |
-
--warning: #b36b00;
|
| 16 |
-
--shadow: 0 18px 42px rgba(30, 42, 64, 0.14);
|
| 17 |
-
--shadow-tight: 0 8px 22px rgba(30, 42, 64, 0.12);
|
| 18 |
-
--radius-sm: 10px;
|
| 19 |
-
--radius-md: 16px;
|
| 20 |
-
--radius-lg: 24px;
|
| 21 |
-
--nav-h: 76px;
|
| 22 |
-
color-scheme: light;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
* {
|
| 26 |
-
box-sizing: border-box;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
html,
|
| 30 |
-
body {
|
| 31 |
-
min-height: 100%;
|
| 32 |
-
height: 100%;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
body {
|
| 36 |
-
margin: 0;
|
| 37 |
-
background: #e9dcc5;
|
| 38 |
-
color: var(--ink);
|
| 39 |
-
font-family: var(--font-body);
|
| 40 |
-
overflow: hidden;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
button,
|
| 44 |
-
input {
|
| 45 |
-
font: inherit;
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
button {
|
| 49 |
-
cursor: pointer;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
button:focus-visible,
|
| 53 |
-
input:focus-visible {
|
| 54 |
-
outline: 3px solid rgba(31, 105, 255, 0.45);
|
| 55 |
-
outline-offset: 3px;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
.material-symbols-rounded {
|
| 59 |
-
width: 1em;
|
| 60 |
-
max-width: 1em;
|
| 61 |
-
display: inline-block;
|
| 62 |
-
overflow: hidden;
|
| 63 |
-
font-family: "Material Symbols Rounded";
|
| 64 |
-
font-size: 22px;
|
| 65 |
-
font-style: normal;
|
| 66 |
-
font-weight: 500;
|
| 67 |
-
font-feature-settings: "liga";
|
| 68 |
-
line-height: 1;
|
| 69 |
-
text-transform: none;
|
| 70 |
-
vertical-align: middle;
|
| 71 |
-
white-space: nowrap;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
.epic-app {
|
| 75 |
-
min-height: 100dvh;
|
| 76 |
-
height: 100dvh;
|
| 77 |
-
display: grid;
|
| 78 |
-
place-items: center;
|
| 79 |
-
padding: 18px;
|
| 80 |
-
overflow: hidden;
|
| 81 |
-
background:
|
| 82 |
-
radial-gradient(circle at 16% 12%, rgba(255, 252, 240, 0.95), transparent 28%),
|
| 83 |
-
radial-gradient(circle at 86% 16%, rgba(109, 84, 53, 0.18), transparent 22%),
|
| 84 |
-
linear-gradient(135deg, #d7c29e 0%, #fff1d5 48%, #dfe7d5 100%);
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
.phone {
|
| 88 |
-
position: relative;
|
| 89 |
-
width: min(100%, 430px);
|
| 90 |
-
height: min(920px, calc(100dvh - 36px));
|
| 91 |
-
min-height: min(760px, calc(100dvh - 36px));
|
| 92 |
-
max-height: calc(100dvh - 36px);
|
| 93 |
-
overflow: hidden;
|
| 94 |
-
display: grid;
|
| 95 |
-
grid-template-rows: auto minmax(0, 1fr) auto;
|
| 96 |
-
border: 1px solid rgba(31, 42, 55, 0.16);
|
| 97 |
-
border-radius: 26px;
|
| 98 |
-
background: var(--paper);
|
| 99 |
-
box-shadow: var(--shadow);
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
.phone[data-style="classroom"] {
|
| 103 |
-
--font-heading: var(--font-display);
|
| 104 |
-
--theme: #e3403a;
|
| 105 |
-
--theme-2: #1467b3;
|
| 106 |
-
--theme-3: #f7c948;
|
| 107 |
-
--theme-soft: #fff3d7;
|
| 108 |
-
--theme-panel: #f8fbff;
|
| 109 |
-
--theme-border: #9bc4ef;
|
| 110 |
-
--reward: #128a55;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
.phone[data-style="magical"] {
|
| 114 |
-
--font-heading: var(--font-quest);
|
| 115 |
-
--theme: #31583e;
|
| 116 |
-
--theme-2: #5a3f78;
|
| 117 |
-
--theme-3: #c79335;
|
| 118 |
-
--theme-soft: #f2e6cc;
|
| 119 |
-
--theme-panel: #fff6e3;
|
| 120 |
-
--theme-border: #aa8752;
|
| 121 |
-
--reward: #2f6b45;
|
| 122 |
-
--line: rgba(95, 66, 33, 0.24);
|
| 123 |
-
--surface: #fffaf0;
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
.phone[data-style="comic"] {
|
| 127 |
-
--font-heading: var(--font-display);
|
| 128 |
-
--theme: #136bd9;
|
| 129 |
-
--theme-2: #d62957;
|
| 130 |
-
--theme-3: #ffd33d;
|
| 131 |
-
--theme-soft: #fff2a8;
|
| 132 |
-
--theme-panel: #f8fbff;
|
| 133 |
-
--theme-border: #1f2a37;
|
| 134 |
-
--reward: #d64b20;
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
.app-top {
|
| 138 |
-
padding: 22px 20px 12px;
|
| 139 |
-
background:
|
| 140 |
-
linear-gradient(90deg, rgba(255, 252, 244, 0.96), rgba(255, 252, 244, 0.55)),
|
| 141 |
-
var(--theme-panel);
|
| 142 |
-
border-bottom: 1px solid var(--line);
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
.brand-row {
|
| 146 |
-
display: flex;
|
| 147 |
-
align-items: center;
|
| 148 |
-
justify-content: space-between;
|
| 149 |
-
gap: 12px;
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
.brand-lockup {
|
| 153 |
-
min-width: 0;
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
.eyebrow {
|
| 157 |
-
margin: 0 0 2px;
|
| 158 |
-
color: var(--theme-2);
|
| 159 |
-
font-size: 12px;
|
| 160 |
-
font-weight: 900;
|
| 161 |
-
text-transform: uppercase;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
.brand-title {
|
| 165 |
-
margin: 0;
|
| 166 |
-
color: var(--ink);
|
| 167 |
-
font-family: var(--font-heading);
|
| 168 |
-
font-size: 29px;
|
| 169 |
-
line-height: 1;
|
| 170 |
}
|
| 171 |
|
| 172 |
-
.
|
| 173 |
-
display: inline-flex;
|
| 174 |
align-items: center;
|
| 175 |
-
gap: 6px;
|
| 176 |
-
min-height: 34px;
|
| 177 |
-
padding: 0 10px;
|
| 178 |
-
border: 1px solid var(--line);
|
| 179 |
-
border-radius: 999px;
|
| 180 |
-
background: rgba(255, 255, 255, 0.84);
|
| 181 |
-
color: var(--muted);
|
| 182 |
-
font-size: 12px;
|
| 183 |
-
font-weight: 900;
|
| 184 |
-
white-space: nowrap;
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
.screen-region {
|
| 188 |
-
min-height: 0;
|
| 189 |
-
overflow: auto;
|
| 190 |
-
overscroll-behavior: contain;
|
| 191 |
-
-webkit-overflow-scrolling: touch;
|
| 192 |
-
padding: 18px 18px calc(var(--nav-h) + 18px);
|
| 193 |
-
background:
|
| 194 |
-
linear-gradient(180deg, rgba(255, 253, 247, 0.72), rgba(255, 255, 255, 0)),
|
| 195 |
-
var(--theme-panel);
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
.screen {
|
| 199 |
-
display: grid;
|
| 200 |
-
gap: 14px;
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
.screen-head {
|
| 204 |
-
display: grid;
|
| 205 |
-
gap: 4px;
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
.screen-title {
|
| 209 |
-
margin: 0;
|
| 210 |
-
font-family: var(--font-heading);
|
| 211 |
-
font-size: 28px;
|
| 212 |
-
line-height: 1.05;
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
.screen-copy {
|
| 216 |
-
margin: 0;
|
| 217 |
-
color: var(--muted);
|
| 218 |
-
font-size: 15px;
|
| 219 |
-
font-weight: 800;
|
| 220 |
-
line-height: 1.35;
|
| 221 |
}
|
| 222 |
|
| 223 |
-
.
|
| 224 |
-
.goal-row,
|
| 225 |
-
.kid-tile,
|
| 226 |
-
.goal-card,
|
| 227 |
-
.style-option,
|
| 228 |
-
.avatar-slot,
|
| 229 |
-
.goal-create {
|
| 230 |
-
border: 1px solid var(--line);
|
| 231 |
-
border-radius: var(--radius-sm);
|
| 232 |
-
background: rgba(255, 253, 247, 0.92);
|
| 233 |
-
box-shadow: var(--shadow-tight);
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
.panel {
|
| 237 |
-
padding: 15px;
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
.section-label {
|
| 241 |
-
margin: 0 0 10px;
|
| 242 |
-
color: var(--ink);
|
| 243 |
-
font-size: 13px;
|
| 244 |
-
font-weight: 1000;
|
| 245 |
text-transform: uppercase;
|
| 246 |
}
|
| 247 |
|
| 248 |
-
.
|
| 249 |
display: grid;
|
| 250 |
-
gap:
|
| 251 |
}
|
| 252 |
|
| 253 |
-
.
|
| 254 |
width: 100%;
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
display: grid;
|
| 257 |
-
grid-template-columns:
|
|
|
|
| 258 |
align-items: center;
|
| 259 |
-
|
| 260 |
-
padding: 12px;
|
| 261 |
-
color: var(--ink);
|
| 262 |
text-align: left;
|
|
|
|
| 263 |
}
|
| 264 |
|
| 265 |
-
.
|
| 266 |
-
|
| 267 |
-
|
|
|
|
| 268 |
}
|
| 269 |
|
| 270 |
-
.
|
| 271 |
-
|
| 272 |
-
width: 46px;
|
| 273 |
-
height: 46px;
|
| 274 |
-
place-items: center;
|
| 275 |
-
border-radius: 12px;
|
| 276 |
-
background: var(--theme-soft);
|
| 277 |
-
color: var(--theme);
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
.style-name {
|
| 281 |
-
display: block;
|
| 282 |
-
font-family: var(--font-heading);
|
| 283 |
-
font-size: 18px;
|
| 284 |
-
font-weight: 1000;
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
.style-meta {
|
| 288 |
-
display: block;
|
| 289 |
-
color: var(--muted);
|
| 290 |
-
font-size: 13px;
|
| 291 |
-
font-weight: 800;
|
| 292 |
-
line-height: 1.25;
|
| 293 |
}
|
| 294 |
|
| 295 |
-
.
|
| 296 |
-
width:
|
| 297 |
-
height:
|
| 298 |
-
border: 2px solid var(--theme);
|
| 299 |
border-radius: 50%;
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
.style-option[aria-pressed="true"] .selected-dot {
|
| 303 |
-
background: radial-gradient(circle, var(--theme) 45%, transparent 49%);
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
.avatar-grid {
|
| 307 |
-
display: grid;
|
| 308 |
-
grid-template-columns: 1fr 1fr;
|
| 309 |
-
gap: 10px;
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
.avatar-slot {
|
| 313 |
-
min-height: 134px;
|
| 314 |
-
padding: 12px;
|
| 315 |
-
display: grid;
|
| 316 |
-
align-content: space-between;
|
| 317 |
-
background:
|
| 318 |
-
linear-gradient(145deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.92)),
|
| 319 |
-
var(--theme-soft);
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
.avatar-face {
|
| 323 |
-
width: 62px;
|
| 324 |
-
height: 62px;
|
| 325 |
display: grid;
|
| 326 |
place-items: center;
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
|
|
|
| 332 |
}
|
| 333 |
|
| 334 |
-
.
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
}
|
| 339 |
|
| 340 |
-
.
|
| 341 |
-
|
| 342 |
-
color: var(--muted);
|
| 343 |
-
font-size: 12px;
|
| 344 |
-
font-weight: 900;
|
| 345 |
}
|
| 346 |
|
| 347 |
-
.
|
| 348 |
-
|
| 349 |
-
gap: 8px;
|
| 350 |
-
align-items: flex-start;
|
| 351 |
-
margin: 0;
|
| 352 |
-
color: #4d6174;
|
| 353 |
-
font-size: 13px;
|
| 354 |
font-weight: 800;
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
.counter-grid {
|
| 359 |
-
display: grid;
|
| 360 |
-
grid-template-columns: 1fr 1fr;
|
| 361 |
-
gap: 10px;
|
| 362 |
}
|
| 363 |
|
| 364 |
-
.
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
padding: 12px;
|
| 370 |
-
border-radius: var(--radius-md);
|
| 371 |
-
background: var(--surface);
|
| 372 |
-
border: 1px solid var(--line);
|
| 373 |
}
|
| 374 |
|
| 375 |
-
.
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
.counter span {
|
| 383 |
-
color: var(--muted);
|
| 384 |
-
font-size: 13px;
|
| 385 |
font-weight: 900;
|
|
|
|
|
|
|
| 386 |
}
|
| 387 |
|
| 388 |
-
.
|
| 389 |
-
|
| 390 |
-
gap: 10px;
|
| 391 |
}
|
| 392 |
|
| 393 |
-
.
|
| 394 |
-
display:
|
| 395 |
-
|
| 396 |
-
|
|
|
|
|
|
|
| 397 |
}
|
| 398 |
|
| 399 |
-
.
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
line-height: 1.3;
|
| 405 |
}
|
| 406 |
|
| 407 |
-
.
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
}
|
| 413 |
|
| 414 |
-
.
|
|
|
|
| 415 |
display: grid;
|
| 416 |
-
|
| 417 |
-
gap: 8px;
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
.goal-create-row input {
|
| 421 |
-
width: 100%;
|
| 422 |
-
min-width: 0;
|
| 423 |
-
min-height: 46px;
|
| 424 |
-
border: 1px solid var(--line);
|
| 425 |
-
border-radius: 999px;
|
| 426 |
-
padding: 0 14px;
|
| 427 |
-
background: #fff;
|
| 428 |
-
color: var(--ink);
|
| 429 |
-
font-weight: 900;
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
.goal-create-row input::placeholder {
|
| 433 |
-
color: #8a98aa;
|
| 434 |
}
|
| 435 |
|
| 436 |
-
.
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
padding:
|
| 441 |
}
|
| 442 |
|
| 443 |
-
.
|
| 444 |
-
|
|
|
|
|
|
|
| 445 |
}
|
| 446 |
|
| 447 |
-
.
|
| 448 |
display: flex;
|
| 449 |
-
align-items: center;
|
| 450 |
flex-wrap: wrap;
|
| 451 |
-
gap:
|
| 452 |
-
margin-bottom: 5px;
|
| 453 |
-
}
|
| 454 |
-
|
| 455 |
-
.goal-name {
|
| 456 |
-
font-family: var(--font-heading);
|
| 457 |
-
font-size: 17px;
|
| 458 |
-
font-weight: 1000;
|
| 459 |
-
}
|
| 460 |
-
|
| 461 |
-
.goal-plain {
|
| 462 |
-
margin: 0;
|
| 463 |
-
color: var(--muted);
|
| 464 |
-
font-size: 13px;
|
| 465 |
-
font-weight: 800;
|
| 466 |
-
line-height: 1.3;
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
.chip {
|
| 470 |
-
display: inline-flex;
|
| 471 |
-
align-items: center;
|
| 472 |
-
min-height: 24px;
|
| 473 |
-
padding: 0 8px;
|
| 474 |
-
border-radius: 999px;
|
| 475 |
-
background: var(--theme-soft);
|
| 476 |
-
color: var(--theme);
|
| 477 |
-
font-size: 11px;
|
| 478 |
-
font-weight: 1000;
|
| 479 |
-
white-space: nowrap;
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
.chip.success {
|
| 483 |
-
background: #e6f7ee;
|
| 484 |
-
color: var(--success);
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
.chip.waiting {
|
| 488 |
-
background: #fff4d8;
|
| 489 |
-
color: var(--warning);
|
| 490 |
-
}
|
| 491 |
-
|
| 492 |
-
.approve-button,
|
| 493 |
-
.complete-button,
|
| 494 |
-
.audio-button,
|
| 495 |
-
.create-button {
|
| 496 |
-
min-height: 44px;
|
| 497 |
-
border: 0;
|
| 498 |
-
border-radius: 12px;
|
| 499 |
-
padding: 0 14px;
|
| 500 |
-
background: var(--theme);
|
| 501 |
-
color: var(--primary-ink);
|
| 502 |
-
font-weight: 1000;
|
| 503 |
-
box-shadow: var(--shadow-tight);
|
| 504 |
}
|
| 505 |
|
| 506 |
-
.
|
| 507 |
display: inline-flex;
|
| 508 |
align-items: center;
|
| 509 |
gap: 6px;
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
background: #eef2f7;
|
| 519 |
-
color: #6b7888;
|
| 520 |
-
box-shadow: none;
|
| 521 |
-
}
|
| 522 |
-
|
| 523 |
-
.kid-grid {
|
| 524 |
-
display: grid;
|
| 525 |
-
grid-template-columns: 1fr 1fr;
|
| 526 |
-
gap: 10px;
|
| 527 |
-
}
|
| 528 |
-
|
| 529 |
-
.kid-tile {
|
| 530 |
-
position: relative;
|
| 531 |
-
aspect-ratio: 1 / 1;
|
| 532 |
-
min-height: 132px;
|
| 533 |
-
overflow: hidden;
|
| 534 |
-
text-align: left;
|
| 535 |
-
color: var(--ink);
|
| 536 |
-
background: var(--theme-soft);
|
| 537 |
-
}
|
| 538 |
-
|
| 539 |
-
.kid-tile img {
|
| 540 |
-
width: 100%;
|
| 541 |
-
height: 100%;
|
| 542 |
-
display: block;
|
| 543 |
-
object-fit: cover;
|
| 544 |
-
background: var(--theme-soft);
|
| 545 |
-
}
|
| 546 |
-
|
| 547 |
-
.tile-status {
|
| 548 |
-
position: absolute;
|
| 549 |
-
left: 8px;
|
| 550 |
-
bottom: 8px;
|
| 551 |
-
max-width: calc(100% - 16px);
|
| 552 |
-
pointer-events: none;
|
| 553 |
}
|
| 554 |
|
| 555 |
-
.
|
| 556 |
-
|
| 557 |
-
|
|
|
|
| 558 |
}
|
| 559 |
|
| 560 |
-
.
|
| 561 |
-
|
| 562 |
-
z-index: 20;
|
| 563 |
-
inset: 0;
|
| 564 |
display: grid;
|
| 565 |
-
|
| 566 |
-
padding: 18px 14px calc(var(--nav-h) + 14px);
|
| 567 |
}
|
| 568 |
|
| 569 |
-
.
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
}
|
| 575 |
|
| 576 |
-
.
|
| 577 |
-
|
| 578 |
-
z-index: 1;
|
| 579 |
-
width: min(100%, 390px);
|
| 580 |
-
max-height: min(690px, calc(100dvh - var(--nav-h) - 44px));
|
| 581 |
-
overflow: auto;
|
| 582 |
-
border-radius: var(--radius-lg);
|
| 583 |
-
box-shadow: 0 24px 54px rgba(13, 24, 40, 0.28);
|
| 584 |
}
|
| 585 |
|
| 586 |
-
.
|
| 587 |
-
position: absolute;
|
| 588 |
-
z-index: 2;
|
| 589 |
-
top: 10px;
|
| 590 |
-
right: 10px;
|
| 591 |
display: grid;
|
| 592 |
-
|
| 593 |
-
height: 42px;
|
| 594 |
-
place-items: center;
|
| 595 |
-
border: 1px solid rgba(255, 255, 255, 0.72);
|
| 596 |
-
border-radius: 50%;
|
| 597 |
-
background: rgba(255, 255, 255, 0.94);
|
| 598 |
-
color: var(--ink);
|
| 599 |
-
box-shadow: var(--shadow-tight);
|
| 600 |
-
}
|
| 601 |
-
|
| 602 |
-
.goal-card-hero {
|
| 603 |
-
position: relative;
|
| 604 |
-
min-height: 188px;
|
| 605 |
-
background: var(--theme-soft);
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
.goal-card-hero img {
|
| 609 |
-
width: 100%;
|
| 610 |
-
height: 206px;
|
| 611 |
-
display: block;
|
| 612 |
-
object-fit: cover;
|
| 613 |
-
}
|
| 614 |
-
|
| 615 |
-
.hero-badge {
|
| 616 |
-
position: absolute;
|
| 617 |
-
left: 12px;
|
| 618 |
-
bottom: 12px;
|
| 619 |
-
max-width: calc(100% - 24px);
|
| 620 |
-
padding: 8px 10px;
|
| 621 |
-
border: 1px solid rgba(255, 255, 255, 0.72);
|
| 622 |
-
border-radius: 12px;
|
| 623 |
-
background: rgba(255, 255, 255, 0.9);
|
| 624 |
-
color: var(--theme);
|
| 625 |
-
font-size: 12px;
|
| 626 |
-
font-weight: 1000;
|
| 627 |
}
|
| 628 |
|
| 629 |
-
.
|
| 630 |
display: grid;
|
|
|
|
| 631 |
gap: 12px;
|
| 632 |
-
|
| 633 |
-
background:
|
| 634 |
-
|
| 635 |
-
|
|
|
|
|
|
|
| 636 |
}
|
| 637 |
|
| 638 |
-
.
|
| 639 |
-
|
| 640 |
-
font-
|
| 641 |
-
font-
|
| 642 |
-
line-height: 1.
|
| 643 |
}
|
| 644 |
|
| 645 |
-
.
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
}
|
| 652 |
|
| 653 |
-
.
|
| 654 |
-
display:
|
| 655 |
-
grid-template-columns: 32px 1fr;
|
| 656 |
align-items: center;
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
|
|
|
| 662 |
font-weight: 900;
|
|
|
|
|
|
|
| 663 |
}
|
| 664 |
|
| 665 |
-
.
|
| 666 |
-
display:
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
border-radius: 50%;
|
| 671 |
-
background: var(--theme-soft);
|
| 672 |
-
color: var(--theme);
|
| 673 |
-
font-weight: 1000;
|
| 674 |
-
}
|
| 675 |
-
|
| 676 |
-
.card-actions {
|
| 677 |
-
display: grid;
|
| 678 |
-
grid-template-columns: 1fr;
|
| 679 |
-
gap: 10px;
|
| 680 |
-
}
|
| 681 |
-
|
| 682 |
-
.audio-button {
|
| 683 |
-
background: #eef2f7;
|
| 684 |
-
color: #536273;
|
| 685 |
}
|
| 686 |
|
| 687 |
-
.
|
| 688 |
-
|
| 689 |
-
background: var(--reward);
|
| 690 |
-
font-size: 16px;
|
| 691 |
}
|
| 692 |
|
| 693 |
-
.
|
| 694 |
-
.reward-panel {
|
| 695 |
display: grid;
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
border: 1px solid var(--line);
|
| 700 |
-
}
|
| 701 |
-
|
| 702 |
-
.waiting-panel {
|
| 703 |
-
background: #fff5dd;
|
| 704 |
-
color: #744800;
|
| 705 |
-
}
|
| 706 |
-
|
| 707 |
-
.reward-panel {
|
| 708 |
-
background: #eaf9f0;
|
| 709 |
-
color: #105c3a;
|
| 710 |
-
}
|
| 711 |
-
|
| 712 |
-
.waiting-panel strong,
|
| 713 |
-
.reward-panel strong {
|
| 714 |
-
font-size: 15px;
|
| 715 |
}
|
| 716 |
|
| 717 |
-
.
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
|
|
|
| 722 |
}
|
| 723 |
|
| 724 |
-
.
|
| 725 |
-
display:
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
}
|
| 731 |
|
| 732 |
-
.
|
| 733 |
-
display:
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
gap: 4px;
|
| 737 |
-
min-width: 0;
|
| 738 |
-
border: 0;
|
| 739 |
-
background: transparent;
|
| 740 |
-
color: #68778a;
|
| 741 |
font-size: 11px;
|
| 742 |
-
font-weight:
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
.nav-button.is-active {
|
| 746 |
-
color: var(--theme);
|
| 747 |
-
}
|
| 748 |
-
|
| 749 |
-
.nav-button[aria-disabled="true"] {
|
| 750 |
-
cursor: not-allowed;
|
| 751 |
-
opacity: 0.46;
|
| 752 |
}
|
| 753 |
|
| 754 |
-
.
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
min-height: 260px;
|
| 758 |
-
padding: 24px;
|
| 759 |
-
border: 1px dashed var(--line);
|
| 760 |
-
border-radius: var(--radius-lg);
|
| 761 |
-
background: rgba(255, 255, 255, 0.58);
|
| 762 |
-
color: var(--muted);
|
| 763 |
-
text-align: center;
|
| 764 |
-
font-weight: 900;
|
| 765 |
}
|
| 766 |
|
| 767 |
@media (max-width: 520px) {
|
| 768 |
-
.
|
| 769 |
-
padding: 0;
|
| 770 |
-
min-height: 100dvh;
|
| 771 |
-
height: 100dvh;
|
| 772 |
-
}
|
| 773 |
-
|
| 774 |
-
.phone {
|
| 775 |
-
width: 100%;
|
| 776 |
-
height: 100dvh;
|
| 777 |
-
min-height: 0;
|
| 778 |
-
max-height: 100dvh;
|
| 779 |
-
display: grid;
|
| 780 |
-
grid-template-rows: auto minmax(0, 1fr) auto;
|
| 781 |
-
overflow: hidden;
|
| 782 |
-
border: 0;
|
| 783 |
-
border-radius: 0;
|
| 784 |
-
}
|
| 785 |
-
|
| 786 |
-
.app-top {
|
| 787 |
-
padding: 14px 16px 10px;
|
| 788 |
-
}
|
| 789 |
-
|
| 790 |
-
.brand-title {
|
| 791 |
-
font-size: 25px;
|
| 792 |
-
}
|
| 793 |
-
|
| 794 |
-
.status-pill {
|
| 795 |
-
min-height: 30px;
|
| 796 |
-
font-size: 11px;
|
| 797 |
-
}
|
| 798 |
-
|
| 799 |
-
.screen-region {
|
| 800 |
-
min-height: 0;
|
| 801 |
-
overflow-y: auto;
|
| 802 |
-
padding: 14px 14px 16px;
|
| 803 |
-
}
|
| 804 |
-
|
| 805 |
-
.screen {
|
| 806 |
-
min-height: auto;
|
| 807 |
-
gap: 10px;
|
| 808 |
-
}
|
| 809 |
-
|
| 810 |
-
.screen-head {
|
| 811 |
-
gap: 2px;
|
| 812 |
-
}
|
| 813 |
-
|
| 814 |
-
.screen-title {
|
| 815 |
-
font-size: 26px;
|
| 816 |
-
}
|
| 817 |
-
|
| 818 |
-
.screen-copy {
|
| 819 |
-
font-size: 13px;
|
| 820 |
-
}
|
| 821 |
-
|
| 822 |
-
.panel {
|
| 823 |
-
padding: 11px;
|
| 824 |
-
}
|
| 825 |
-
|
| 826 |
-
.goal-create {
|
| 827 |
-
padding: 11px;
|
| 828 |
-
}
|
| 829 |
-
|
| 830 |
-
.goal-create-row {
|
| 831 |
grid-template-columns: 1fr;
|
| 832 |
}
|
| 833 |
|
| 834 |
-
.
|
| 835 |
-
|
| 836 |
-
}
|
| 837 |
-
|
| 838 |
-
.section-label {
|
| 839 |
-
margin-bottom: 7px;
|
| 840 |
-
font-size: 12px;
|
| 841 |
-
}
|
| 842 |
-
|
| 843 |
-
.style-grid {
|
| 844 |
-
gap: 7px;
|
| 845 |
-
}
|
| 846 |
-
|
| 847 |
-
.style-option {
|
| 848 |
-
min-height: 62px;
|
| 849 |
-
grid-template-columns: 38px 1fr auto;
|
| 850 |
-
gap: 9px;
|
| 851 |
-
padding: 9px;
|
| 852 |
-
}
|
| 853 |
-
|
| 854 |
-
.style-mark {
|
| 855 |
-
width: 38px;
|
| 856 |
-
height: 38px;
|
| 857 |
-
border-radius: 12px;
|
| 858 |
-
}
|
| 859 |
-
|
| 860 |
-
.style-name {
|
| 861 |
-
font-size: 16px;
|
| 862 |
-
}
|
| 863 |
-
|
| 864 |
-
.style-meta {
|
| 865 |
-
font-size: 12px;
|
| 866 |
}
|
| 867 |
|
| 868 |
-
.
|
| 869 |
-
|
| 870 |
-
}
|
| 871 |
-
|
| 872 |
-
.avatar-face {
|
| 873 |
-
width: 50px;
|
| 874 |
-
height: 50px;
|
| 875 |
-
border-radius: 18px;
|
| 876 |
-
}
|
| 877 |
-
|
| 878 |
-
.counter {
|
| 879 |
-
min-height: 76px;
|
| 880 |
-
}
|
| 881 |
-
|
| 882 |
-
.counter strong {
|
| 883 |
-
font-size: 30px;
|
| 884 |
-
}
|
| 885 |
-
|
| 886 |
-
.nav {
|
| 887 |
-
min-height: 68px;
|
| 888 |
-
}
|
| 889 |
-
|
| 890 |
-
.goal-modal-backdrop {
|
| 891 |
-
padding: 12px 12px 78px;
|
| 892 |
-
}
|
| 893 |
-
|
| 894 |
-
.goal-modal-shell {
|
| 895 |
-
max-height: calc(100dvh - 96px);
|
| 896 |
-
}
|
| 897 |
-
}
|
| 898 |
-
|
| 899 |
-
@media (max-width: 374px) {
|
| 900 |
-
.screen-region {
|
| 901 |
-
padding-inline: 12px;
|
| 902 |
-
}
|
| 903 |
-
|
| 904 |
-
.kid-grid,
|
| 905 |
-
.counter-grid {
|
| 906 |
-
gap: 8px;
|
| 907 |
}
|
| 908 |
|
| 909 |
-
.
|
| 910 |
-
|
| 911 |
-
font-size: 25px;
|
| 912 |
}
|
| 913 |
}
|
|
|
|
| 1 |
+
.app-shell {
|
| 2 |
+
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
}
|
| 4 |
|
| 5 |
+
.app-head {
|
|
|
|
| 6 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
}
|
| 8 |
|
| 9 |
+
.wordmark .sub {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
text-transform: uppercase;
|
| 11 |
}
|
| 12 |
|
| 13 |
+
.goal-strip {
|
| 14 |
display: grid;
|
| 15 |
+
gap: clamp(8px, 2.4cqw, 12px);
|
| 16 |
}
|
| 17 |
|
| 18 |
+
.goal-button {
|
| 19 |
width: 100%;
|
| 20 |
+
border: 1.5px solid var(--field-border-color);
|
| 21 |
+
border-radius: var(--control-radius);
|
| 22 |
+
background: var(--field-bg);
|
| 23 |
+
box-shadow: var(--control-shadow);
|
| 24 |
+
color: var(--field-fg);
|
| 25 |
+
cursor: pointer;
|
| 26 |
display: grid;
|
| 27 |
+
grid-template-columns: auto 1fr auto;
|
| 28 |
+
gap: clamp(10px, 3cqw, 14px);
|
| 29 |
align-items: center;
|
| 30 |
+
padding: clamp(11px, 3.2cqw, 15px) clamp(13px, 3.6cqw, 17px);
|
|
|
|
|
|
|
| 31 |
text-align: left;
|
| 32 |
+
transition: border-color .15s, background .15s, transform .1s;
|
| 33 |
}
|
| 34 |
|
| 35 |
+
.goal-button:hover,
|
| 36 |
+
.goal-button[aria-pressed="true"] {
|
| 37 |
+
border-color: var(--accent);
|
| 38 |
+
background: var(--accent-soft);
|
| 39 |
}
|
| 40 |
|
| 41 |
+
.goal-button:active {
|
| 42 |
+
transform: translateY(1px);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
}
|
| 44 |
|
| 45 |
+
.goal-button__mark {
|
| 46 |
+
width: 30px;
|
| 47 |
+
height: 30px;
|
|
|
|
| 48 |
border-radius: 50%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
display: grid;
|
| 50 |
place-items: center;
|
| 51 |
+
background: var(--card-bg);
|
| 52 |
+
color: var(--accent);
|
| 53 |
+
border: 1.5px solid var(--field-border-color);
|
| 54 |
+
font-family: var(--font-display);
|
| 55 |
+
font-weight: var(--display-weight);
|
| 56 |
+
font-size: 14px;
|
| 57 |
}
|
| 58 |
|
| 59 |
+
.goal-button.is-complete .goal-button__mark {
|
| 60 |
+
background: var(--success);
|
| 61 |
+
border-color: var(--success);
|
| 62 |
+
color: var(--on-primary);
|
| 63 |
}
|
| 64 |
|
| 65 |
+
.goal-button__copy {
|
| 66 |
+
min-width: 0;
|
|
|
|
|
|
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
+
.goal-button__title {
|
| 70 |
+
color: var(--field-fg);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
font-weight: 800;
|
| 72 |
+
font-size: clamp(15px, 4.2cqw, 18px);
|
| 73 |
+
line-height: 1.2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
|
| 76 |
+
.goal-button__meta {
|
| 77 |
+
margin-top: 3px;
|
| 78 |
+
color: var(--field-fg-muted);
|
| 79 |
+
font-size: clamp(12px, 3.2cqw, 14px);
|
| 80 |
+
font-weight: 700;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
+
.goal-button__status {
|
| 84 |
+
border-radius: var(--pill-radius);
|
| 85 |
+
padding: 5px 9px;
|
| 86 |
+
color: var(--accent-ink);
|
| 87 |
+
background: var(--accent-soft);
|
| 88 |
+
font-size: 11px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
font-weight: 900;
|
| 90 |
+
text-transform: uppercase;
|
| 91 |
+
white-space: nowrap;
|
| 92 |
}
|
| 93 |
|
| 94 |
+
.card-preview {
|
| 95 |
+
margin-top: clamp(20px, 6cqw, 28px);
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
+
.hero img {
|
| 99 |
+
display: block;
|
| 100 |
+
width: 100%;
|
| 101 |
+
aspect-ratio: 16 / 9;
|
| 102 |
+
object-fit: cover;
|
| 103 |
+
background: var(--is-bg);
|
| 104 |
}
|
| 105 |
|
| 106 |
+
.hero__rank {
|
| 107 |
+
max-width: calc(100% - 92px);
|
| 108 |
+
overflow: hidden;
|
| 109 |
+
text-overflow: ellipsis;
|
| 110 |
+
white-space: nowrap;
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
+
.quest-headline .narration {
|
| 114 |
+
max-width: 36rem;
|
| 115 |
+
margin: .65em auto 0;
|
| 116 |
+
color: var(--body-color);
|
| 117 |
+
font-family: var(--font-body);
|
| 118 |
+
font-size: clamp(14px, 3.6cqw, 17px);
|
| 119 |
+
font-weight: 650;
|
| 120 |
+
line-height: 1.45;
|
| 121 |
+
text-transform: none;
|
| 122 |
+
letter-spacing: 0;
|
| 123 |
}
|
| 124 |
|
| 125 |
+
.media-panel {
|
| 126 |
+
margin-top: clamp(16px, 4.4cqw, 22px);
|
| 127 |
display: grid;
|
| 128 |
+
gap: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
+
.audio-shell {
|
| 132 |
+
background: var(--panel-bg);
|
| 133 |
+
border: 1px solid var(--panel-border-color);
|
| 134 |
+
border-radius: var(--control-radius);
|
| 135 |
+
padding: 12px;
|
| 136 |
}
|
| 137 |
|
| 138 |
+
.audio-shell audio {
|
| 139 |
+
display: block;
|
| 140 |
+
width: 100%;
|
| 141 |
+
min-height: 38px;
|
| 142 |
}
|
| 143 |
|
| 144 |
+
.asset-pills {
|
| 145 |
display: flex;
|
|
|
|
| 146 |
flex-wrap: wrap;
|
| 147 |
+
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
}
|
| 149 |
|
| 150 |
+
.asset-pill {
|
| 151 |
display: inline-flex;
|
| 152 |
align-items: center;
|
| 153 |
gap: 6px;
|
| 154 |
+
min-height: 30px;
|
| 155 |
+
border-radius: var(--pill-radius);
|
| 156 |
+
background: var(--panel-bg);
|
| 157 |
+
border: 1px solid var(--panel-border-color);
|
| 158 |
+
color: var(--muted-color);
|
| 159 |
+
font-size: 12px;
|
| 160 |
+
font-weight: 850;
|
| 161 |
+
padding: 6px 10px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
}
|
| 163 |
|
| 164 |
+
.asset-pill svg {
|
| 165 |
+
width: 14px;
|
| 166 |
+
height: 14px;
|
| 167 |
+
color: var(--accent);
|
| 168 |
}
|
| 169 |
|
| 170 |
+
.action-row {
|
| 171 |
+
margin-top: clamp(18px, 5cqw, 24px);
|
|
|
|
|
|
|
| 172 |
display: grid;
|
| 173 |
+
gap: 12px;
|
|
|
|
| 174 |
}
|
| 175 |
|
| 176 |
+
.state-note {
|
| 177 |
+
border-radius: var(--control-radius);
|
| 178 |
+
border: 1px solid var(--panel-border-color);
|
| 179 |
+
background: var(--panel-bg);
|
| 180 |
+
padding: 14px 16px;
|
| 181 |
+
color: var(--body-color);
|
| 182 |
+
font-size: clamp(13px, 3.6cqw, 15px);
|
| 183 |
+
font-weight: 700;
|
| 184 |
+
line-height: 1.45;
|
| 185 |
}
|
| 186 |
|
| 187 |
+
.state-note strong {
|
| 188 |
+
color: var(--accent);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
+
.parent-list {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
display: grid;
|
| 193 |
+
gap: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
| 195 |
|
| 196 |
+
.parent-row {
|
| 197 |
display: grid;
|
| 198 |
+
grid-template-columns: 1fr auto;
|
| 199 |
gap: 12px;
|
| 200 |
+
align-items: center;
|
| 201 |
+
background: var(--field-bg);
|
| 202 |
+
border: 1.5px solid var(--field-border-color);
|
| 203 |
+
border-radius: var(--control-radius);
|
| 204 |
+
box-shadow: var(--control-shadow);
|
| 205 |
+
padding: clamp(12px, 3.4cqw, 16px);
|
| 206 |
}
|
| 207 |
|
| 208 |
+
.parent-row__title {
|
| 209 |
+
color: var(--field-fg);
|
| 210 |
+
font-size: clamp(15px, 4cqw, 18px);
|
| 211 |
+
font-weight: 850;
|
| 212 |
+
line-height: 1.2;
|
| 213 |
}
|
| 214 |
|
| 215 |
+
.parent-row__meta {
|
| 216 |
+
margin-top: 5px;
|
| 217 |
+
color: var(--field-fg-muted);
|
| 218 |
+
font-size: clamp(12px, 3.2cqw, 14px);
|
| 219 |
+
font-weight: 700;
|
| 220 |
+
line-height: 1.35;
|
| 221 |
}
|
| 222 |
|
| 223 |
+
.parent-row__status {
|
| 224 |
+
display: inline-flex;
|
|
|
|
| 225 |
align-items: center;
|
| 226 |
+
min-height: 30px;
|
| 227 |
+
border-radius: var(--pill-radius);
|
| 228 |
+
color: var(--accent-ink);
|
| 229 |
+
background: var(--accent-soft);
|
| 230 |
+
padding: 5px 10px;
|
| 231 |
+
font-size: 11px;
|
| 232 |
font-weight: 900;
|
| 233 |
+
text-transform: uppercase;
|
| 234 |
+
white-space: nowrap;
|
| 235 |
}
|
| 236 |
|
| 237 |
+
.parent-actions {
|
| 238 |
+
display: flex;
|
| 239 |
+
flex-direction: column;
|
| 240 |
+
gap: 8px;
|
| 241 |
+
align-items: flex-end;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
+
.parent-actions .btn-ghost {
|
| 245 |
+
justify-content: center;
|
|
|
|
|
|
|
| 246 |
}
|
| 247 |
|
| 248 |
+
.summary-grid {
|
|
|
|
| 249 |
display: grid;
|
| 250 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 251 |
+
gap: 10px;
|
| 252 |
+
margin-top: clamp(16px, 4.4cqw, 22px);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
+
.summary-tile {
|
| 256 |
+
background: var(--panel-bg);
|
| 257 |
+
border: 1px solid var(--panel-border-color);
|
| 258 |
+
border-radius: var(--control-radius);
|
| 259 |
+
padding: 12px;
|
| 260 |
+
text-align: center;
|
| 261 |
}
|
| 262 |
|
| 263 |
+
.summary-tile b {
|
| 264 |
+
display: block;
|
| 265 |
+
color: var(--heading-color);
|
| 266 |
+
font-family: var(--font-display);
|
| 267 |
+
font-size: clamp(20px, 5cqw, 26px);
|
| 268 |
+
line-height: 1;
|
| 269 |
}
|
| 270 |
|
| 271 |
+
.summary-tile span {
|
| 272 |
+
display: block;
|
| 273 |
+
margin-top: 5px;
|
| 274 |
+
color: var(--muted-color);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
font-size: 11px;
|
| 276 |
+
font-weight: 850;
|
| 277 |
+
text-transform: uppercase;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
|
| 280 |
+
.corner-flourish svg,
|
| 281 |
+
.corner-flourish {
|
| 282 |
+
pointer-events: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
}
|
| 284 |
|
| 285 |
@media (max-width: 520px) {
|
| 286 |
+
.parent-row {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
grid-template-columns: 1fr;
|
| 288 |
}
|
| 289 |
|
| 290 |
+
.parent-actions {
|
| 291 |
+
align-items: stretch;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
}
|
| 293 |
|
| 294 |
+
.summary-grid {
|
| 295 |
+
grid-template-columns: 1fr;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
}
|
| 297 |
|
| 298 |
+
.vt {
|
| 299 |
+
padding-inline: 10px;
|
|
|
|
| 300 |
}
|
| 301 |
}
|
frontend/styles/components.css
CHANGED
|
@@ -1,224 +1,400 @@
|
|
| 1 |
-
*
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
background:
|
| 8 |
-
radial-gradient(circle at 8% 0%, rgba(115, 202, 241, 0.28), transparent 34%),
|
| 9 |
-
radial-gradient(circle at 90% 18%, rgba(255, 242, 181, 0.45), transparent 30%),
|
| 10 |
-
linear-gradient(180deg, #f9fdff 0%, #fffaf3 100%);
|
| 11 |
-
}
|
| 12 |
|
| 13 |
body {
|
| 14 |
-
min-height: 100%;
|
| 15 |
-
margin: 0;
|
| 16 |
-
color: var(--color-ink);
|
| 17 |
font-family: var(--font-body);
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
-
button
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
}
|
| 26 |
|
| 27 |
-
|
| 28 |
-
cursor: pointer;
|
| 29 |
-
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
align-items: center;
|
| 41 |
justify-content: center;
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
white-space: nowrap;
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
| 55 |
position: relative;
|
| 56 |
-
width:
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
| 59 |
overflow: hidden;
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
border-radius: 10px;
|
| 65 |
-
box-shadow: var(--shadow-card);
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
.app-frame::before {
|
| 69 |
-
position: absolute;
|
| 70 |
-
inset: -10px -10px auto -10px;
|
| 71 |
-
height: 160px;
|
| 72 |
content: "";
|
|
|
|
|
|
|
| 73 |
pointer-events: none;
|
| 74 |
-
|
| 75 |
-
radial-gradient(ellipse at 12% 12%, rgba(95, 185, 231, 0.16), transparent 44%),
|
| 76 |
-
radial-gradient(ellipse at 82% 0%, rgba(165, 221, 247, 0.42), transparent 36%);
|
| 77 |
}
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
| 88 |
font-family: var(--font-display);
|
| 89 |
-
font-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
.icon-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
.
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
background: var(--color-paper);
|
| 113 |
-
color: var(--color-ink);
|
| 114 |
-
box-shadow: var(--shadow-soft);
|
| 115 |
}
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
border-radius: 50%;
|
| 125 |
-
background: rgba(255, 255, 255, 0.95);
|
| 126 |
-
color: #10264a;
|
| 127 |
-
}
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
border-radius: var(--radius-pill);
|
| 137 |
-
font-weight: 800;
|
| 138 |
-
}
|
| 139 |
|
| 140 |
-
.
|
| 141 |
-
.
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
font-size: 13px;
|
| 148 |
-
font-weight: 700;
|
| 149 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
}
|
| 157 |
-
|
| 158 |
-
.
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
justify-content: center;
|
| 165 |
-
gap: var(--space-3);
|
| 166 |
-
border: 0;
|
| 167 |
-
border-radius: var(--radius-pill);
|
| 168 |
-
color: white;
|
| 169 |
-
font-family: var(--font-display);
|
| 170 |
-
font-size: 24px;
|
| 171 |
-
font-weight: 800;
|
| 172 |
}
|
| 173 |
-
|
| 174 |
-
.
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
-
.
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
}
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
|
|
|
|
|
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
| 207 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
-
.
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
}
|
|
|
|
| 1 |
+
/* ============================================================================
|
| 2 |
+
EPIC ERRANDS — STRUCTURAL COMPONENT CSS
|
| 3 |
+
Layout + structure only. Every color / font / radius / shadow comes from a
|
| 4 |
+
SEMANTIC token (see tokens.css). No literal theme values live here, so the
|
| 5 |
+
same stylesheet paints all three skins.
|
| 6 |
+
========================================================================== */
|
| 7 |
|
| 8 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
+
html, body { min-height: 100%; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
body {
|
|
|
|
|
|
|
|
|
|
| 12 |
font-family: var(--font-body);
|
| 13 |
+
color: var(--body-color);
|
| 14 |
+
background: var(--page-bg);
|
| 15 |
+
background-attachment: fixed;
|
| 16 |
+
-webkit-font-smoothing: antialiased;
|
| 17 |
+
text-rendering: optimizeLegibility;
|
| 18 |
+
min-height: 100dvh;
|
| 19 |
}
|
| 20 |
|
| 21 |
+
button { font-family: inherit; }
|
| 22 |
+
[contenteditable] { outline: none; }
|
| 23 |
+
[contenteditable]:focus { box-shadow: 0 0 0 2px var(--accent); border-radius: 4px; }
|
|
|
|
| 24 |
|
| 25 |
+
.display { font-family: var(--font-display); font-weight: var(--display-weight); text-transform: var(--display-transform); letter-spacing: var(--display-tracking); line-height: var(--display-line); }
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
/* ============================================================================
|
| 28 |
+
APP SHELL (theme rail + stage)
|
| 29 |
+
========================================================================== */
|
| 30 |
+
.stage {
|
| 31 |
+
min-height: 100dvh;
|
| 32 |
+
display: grid;
|
| 33 |
+
place-items: start center;
|
| 34 |
+
padding: clamp(64px, 8vh, 96px) clamp(12px, 4vw, 40px) clamp(32px, 6vh, 64px);
|
| 35 |
}
|
| 36 |
+
#app { width: 100%; max-width: var(--card-max); display: flex; justify-content: center; }
|
| 37 |
|
| 38 |
+
/* ---- Theme switcher rail (fixed top) ---- */
|
| 39 |
+
.theme-rail {
|
| 40 |
+
position: fixed;
|
| 41 |
+
top: 0; left: 0; right: 0;
|
| 42 |
+
z-index: 50;
|
| 43 |
+
display: flex;
|
| 44 |
align-items: center;
|
| 45 |
justify-content: center;
|
| 46 |
+
gap: 10px;
|
| 47 |
+
padding: 10px 16px;
|
| 48 |
+
background: rgba(20,22,28,.72);
|
| 49 |
+
backdrop-filter: blur(14px) saturate(1.2);
|
| 50 |
+
-webkit-backdrop-filter: blur(14px) saturate(1.2);
|
| 51 |
+
border-bottom: 1px solid rgba(255,255,255,.08);
|
| 52 |
+
}
|
| 53 |
+
.theme-rail__label {
|
| 54 |
+
font-family: 'Hanken Grotesk', system-ui, sans-serif;
|
| 55 |
+
font-size: 11px;
|
| 56 |
+
font-weight: 700;
|
| 57 |
+
letter-spacing: .14em;
|
| 58 |
+
text-transform: uppercase;
|
| 59 |
+
color: rgba(255,255,255,.5);
|
| 60 |
+
margin-right: 4px;
|
| 61 |
+
}
|
| 62 |
+
.theme-seg { display: flex; gap: 4px; background: rgba(255,255,255,.07); border-radius: 999px; padding: 4px; }
|
| 63 |
+
.theme-btn {
|
| 64 |
+
display: inline-flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
gap: 8px;
|
| 67 |
+
border: none;
|
| 68 |
+
cursor: pointer;
|
| 69 |
+
background: transparent;
|
| 70 |
+
color: rgba(255,255,255,.7);
|
| 71 |
+
font-family: 'Hanken Grotesk', system-ui, sans-serif;
|
| 72 |
+
font-size: 13px;
|
| 73 |
+
font-weight: 700;
|
| 74 |
+
padding: 8px 16px;
|
| 75 |
+
border-radius: 999px;
|
| 76 |
+
transition: background .18s, color .18s;
|
| 77 |
white-space: nowrap;
|
| 78 |
+
}
|
| 79 |
+
.theme-btn:hover { color: #fff; }
|
| 80 |
+
.theme-btn .swatch {
|
| 81 |
+
width: 13px; height: 13px; border-radius: 50%;
|
| 82 |
+
flex: none;
|
| 83 |
+
box-shadow: 0 0 0 2px rgba(255,255,255,.25);
|
| 84 |
+
}
|
| 85 |
+
.theme-btn[aria-pressed="true"] { background: #fff; color: #15171d; }
|
| 86 |
+
.theme-btn[aria-pressed="true"] .swatch { box-shadow: 0 0 0 2px rgba(0,0,0,.12); }
|
| 87 |
+
|
| 88 |
+
@media (max-width: 560px) {
|
| 89 |
+
.theme-rail__label { display: none; }
|
| 90 |
+
.theme-btn { padding: 8px 12px; }
|
| 91 |
+
.theme-btn .label { display: none; }
|
| 92 |
+
.theme-btn .swatch { width: 16px; height: 16px; }
|
| 93 |
}
|
| 94 |
|
| 95 |
+
/* ============================================================================
|
| 96 |
+
CARD (the device-sized surface, shared by both screens)
|
| 97 |
+
========================================================================== */
|
| 98 |
+
.card {
|
| 99 |
position: relative;
|
| 100 |
+
width: 100%;
|
| 101 |
+
max-width: var(--card-max);
|
| 102 |
+
background: var(--card-bg);
|
| 103 |
+
border-radius: var(--card-radius);
|
| 104 |
+
border: var(--card-border-width) solid var(--card-border-color);
|
| 105 |
+
box-shadow: var(--card-shadow);
|
| 106 |
overflow: hidden;
|
| 107 |
+
isolation: isolate;
|
| 108 |
+
container-type: inline-size;
|
| 109 |
+
}
|
| 110 |
+
.card::before {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
content: "";
|
| 112 |
+
position: absolute; inset: 0;
|
| 113 |
+
background: var(--card-texture);
|
| 114 |
pointer-events: none;
|
| 115 |
+
z-index: 0;
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
+
.card-inner { position: relative; z-index: 1; padding: clamp(22px, 6cqw, 34px); }
|
| 118 |
|
| 119 |
+
/* ornate corner flourishes (atelier only — hidden elsewhere by token CSS) */
|
| 120 |
+
.corner-flourish { position: absolute; width: 50px; height: 50px; color: var(--accent); z-index: 2; pointer-events: none; }
|
| 121 |
+
.corner-flourish.tl { top: 10px; left: 10px; }
|
| 122 |
+
.corner-flourish.tr { top: 10px; right: 10px; transform: scaleX(-1); }
|
| 123 |
+
.corner-flourish.bl { bottom: 10px; left: 10px; transform: scaleY(-1); }
|
| 124 |
+
.corner-flourish.br { bottom: 10px; right: 10px; transform: scale(-1,-1); }
|
| 125 |
+
|
| 126 |
+
/* ============================================================================
|
| 127 |
+
SHARED PRIMITIVES
|
| 128 |
+
========================================================================== */
|
| 129 |
+
.divider { width: 100%; margin: clamp(18px, 5cqw, 26px) 0; border: none; border-top: 1px solid var(--field-border-color); }
|
| 130 |
|
| 131 |
+
/* Banner pill (top of quest screen) */
|
| 132 |
+
.banner {
|
| 133 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 134 |
+
background: var(--banner-bg);
|
| 135 |
+
color: var(--banner-text);
|
| 136 |
font-family: var(--font-display);
|
| 137 |
+
font-weight: var(--display-weight);
|
| 138 |
+
text-transform: var(--display-transform);
|
| 139 |
+
letter-spacing: var(--display-tracking);
|
| 140 |
+
font-size: clamp(16px, 3.6cqw, 22px);
|
| 141 |
+
padding: 11px 22px;
|
| 142 |
+
box-shadow: var(--control-shadow);
|
| 143 |
}
|
| 144 |
|
| 145 |
+
/* Primary CTA */
|
| 146 |
+
.btn-primary {
|
| 147 |
+
position: relative;
|
| 148 |
+
display: inline-flex; align-items: center; justify-content: center;
|
| 149 |
+
gap: .55em;
|
| 150 |
+
width: 100%;
|
| 151 |
+
border: none; cursor: pointer;
|
| 152 |
+
padding: clamp(15px, 4.2cqw, 19px);
|
| 153 |
+
border-radius: var(--primary-radius);
|
| 154 |
+
background: var(--primary);
|
| 155 |
+
color: var(--primary-text);
|
| 156 |
+
font-family: var(--font-display);
|
| 157 |
+
font-weight: var(--display-weight);
|
| 158 |
+
text-transform: var(--display-transform);
|
| 159 |
+
letter-spacing: var(--display-tracking);
|
| 160 |
+
font-size: clamp(19px, 5cqw, 25px);
|
| 161 |
+
box-shadow: var(--primary-shadow);
|
| 162 |
+
transition: transform .08s, box-shadow .08s, filter .15s;
|
| 163 |
}
|
| 164 |
+
.btn-primary:hover:not(:disabled) { filter: brightness(1.04); }
|
| 165 |
+
.btn-primary:disabled { opacity: .45; cursor: not-allowed; }
|
| 166 |
+
.btn-primary svg { width: 1.2em; height: 1.2em; }
|
| 167 |
|
| 168 |
+
/* Secondary / ghost button */
|
| 169 |
+
.btn-ghost {
|
| 170 |
+
display: inline-flex; align-items: center; gap: .5em;
|
| 171 |
+
border: 1.5px solid var(--field-border-color);
|
| 172 |
+
background: var(--field-bg);
|
| 173 |
+
color: var(--field-fg);
|
| 174 |
+
cursor: pointer;
|
| 175 |
+
font-family: var(--font-body);
|
| 176 |
+
font-weight: 700;
|
| 177 |
+
font-size: clamp(13px, 3.4cqw, 15px);
|
| 178 |
+
padding: 10px 16px;
|
| 179 |
+
border-radius: var(--control-radius);
|
| 180 |
+
box-shadow: var(--control-shadow);
|
| 181 |
+
white-space: nowrap;
|
| 182 |
+
transition: background .15s, border-color .15s, transform .1s;
|
| 183 |
}
|
| 184 |
+
.btn-ghost:hover { border-color: var(--accent); }
|
| 185 |
+
.btn-ghost:active { transform: translateY(1px); }
|
| 186 |
+
.btn-ghost svg { width: 1.1em; height: 1.1em; flex: none; }
|
| 187 |
|
| 188 |
+
.icon-btn {
|
| 189 |
+
flex: none; width: 44px; height: 44px; border-radius: var(--control-radius);
|
| 190 |
+
display: grid; place-items: center; cursor: pointer;
|
| 191 |
+
border: 1.5px solid var(--field-border-color);
|
| 192 |
+
background: var(--field-bg);
|
| 193 |
+
color: var(--field-fg);
|
| 194 |
+
box-shadow: var(--control-shadow);
|
| 195 |
+
transition: background .15s, transform .1s;
|
|
|
|
|
|
|
|
|
|
| 196 |
}
|
| 197 |
+
.icon-btn:hover { border-color: var(--accent); }
|
| 198 |
+
.icon-btn:active { transform: translateY(1px); }
|
| 199 |
+
.icon-btn svg { width: 22px; height: 22px; }
|
| 200 |
|
| 201 |
+
/* ============================================================================
|
| 202 |
+
HEADER (shared)
|
| 203 |
+
========================================================================== */
|
| 204 |
+
.app-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
|
| 205 |
+
.wordmark { display: flex; flex-direction: column; line-height: 1; }
|
| 206 |
+
.wordmark .display { font-size: clamp(26px, 7cqw, 36px); color: var(--heading-color); }
|
| 207 |
+
.wordmark .sub { font-family: var(--font-body); font-weight: 700; font-size: clamp(11px, 3cqw, 13px); color: var(--muted-color); letter-spacing: .04em; margin-top: 4px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
+
/* ============================================================================
|
| 210 |
+
TITLE / SECTION
|
| 211 |
+
========================================================================== */
|
| 212 |
+
.lead { text-align: center; margin-top: clamp(10px, 3cqw, 18px); }
|
| 213 |
+
.lead h1 { font-family: var(--font-display); font-weight: var(--display-weight); text-transform: var(--display-transform); letter-spacing: var(--display-tracking); line-height: var(--display-line); color: var(--heading-color); font-size: clamp(32px, 10cqw, 48px); }
|
| 214 |
+
.lead p { margin-top: .55em; color: var(--muted-color); font-weight: 600; font-size: clamp(14px, 4cqw, 18px); }
|
| 215 |
+
.lead .u { background-image: linear-gradient(var(--success), var(--success)); background-size: 100% 3px; background-position: 0 100%; background-repeat: no-repeat; padding-bottom: 1px; }
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
+
.section { margin-top: clamp(20px, 6cqw, 28px); }
|
| 218 |
+
.section__head { display: flex; align-items: center; gap: 12px; margin-bottom: clamp(12px, 3.4cqw, 16px); }
|
| 219 |
+
.section__num {
|
| 220 |
+
flex: none; width: 30px; height: 30px; border-radius: 50%;
|
| 221 |
+
display: grid; place-items: center;
|
| 222 |
+
background: var(--accent-soft); color: var(--accent-ink);
|
| 223 |
+
font-family: var(--font-display); font-weight: var(--display-weight); font-size: 16px;
|
|
|
|
|
|
|
| 224 |
}
|
| 225 |
+
.section__titles { min-width: 0; }
|
| 226 |
+
.section__title { font-family: var(--font-display); font-weight: var(--display-weight); text-transform: var(--display-transform); letter-spacing: var(--display-tracking); color: var(--heading-color); font-size: clamp(18px, 5cqw, 24px); display: flex; align-items: baseline; gap: .4em; flex-wrap: wrap; }
|
| 227 |
+
.section__title .note { font-family: var(--font-body); font-weight: 600; text-transform: none; letter-spacing: 0; font-size: .62em; color: var(--faint-color); }
|
| 228 |
+
.section__sub { color: var(--muted-color); font-size: clamp(13px, 3.6cqw, 15px); margin-top: 2px; }
|
| 229 |
|
| 230 |
+
/* ============================================================================
|
| 231 |
+
GOAL ROWS
|
| 232 |
+
========================================================================== */
|
| 233 |
+
.goals { display: flex; flex-direction: column; gap: clamp(8px, 2.4cqw, 12px); }
|
| 234 |
+
.goal {
|
| 235 |
+
display: grid; grid-template-columns: auto 1fr auto; align-items: center;
|
| 236 |
+
gap: clamp(10px, 3cqw, 14px);
|
| 237 |
+
padding: clamp(11px, 3.2cqw, 15px) clamp(13px, 3.6cqw, 17px);
|
| 238 |
+
border: 1.5px solid var(--field-border-color);
|
| 239 |
+
border-radius: var(--control-radius);
|
| 240 |
+
background: var(--field-bg);
|
| 241 |
+
box-shadow: var(--control-shadow);
|
| 242 |
+
transition: border-color .15s;
|
| 243 |
}
|
| 244 |
+
.goal:focus-within { border-color: var(--accent); }
|
| 245 |
+
.goal__check {
|
| 246 |
+
flex: none; width: 24px; height: 24px; border-radius: 6px;
|
| 247 |
+
border: 2px solid var(--field-border-color);
|
| 248 |
+
background: var(--card-bg); cursor: pointer;
|
| 249 |
+
display: grid; place-items: center;
|
| 250 |
+
transition: background .12s, border-color .12s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
}
|
| 252 |
+
.goal__check svg { width: 70%; height: 70%; opacity: 0; transition: opacity .12s; color: #fff; }
|
| 253 |
+
.goal.done .goal__check { background: var(--success); border-color: var(--success); }
|
| 254 |
+
.goal.done .goal__check svg { opacity: 1; }
|
| 255 |
+
.goal__input {
|
| 256 |
+
border: none; outline: none; background: transparent;
|
| 257 |
+
font-family: var(--font-body); font-weight: 700;
|
| 258 |
+
font-size: clamp(15px, 4.2cqw, 18px); color: var(--field-fg);
|
| 259 |
+
min-width: 0;
|
| 260 |
}
|
| 261 |
+
.goal__input::placeholder { color: var(--field-fg-muted); font-weight: 600; }
|
| 262 |
+
.goal.done .goal__input { text-decoration: line-through; color: var(--field-fg-muted); }
|
| 263 |
+
.goal__clear { flex: none; width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent; color: var(--field-fg-muted); cursor: pointer; display: grid; place-items: center; transition: background .15s, color .15s; }
|
| 264 |
+
.goal__clear:hover { background: rgba(0,0,0,.06); color: var(--field-fg); }
|
| 265 |
+
.goal__clear svg { width: 18px; height: 18px; }
|
| 266 |
|
| 267 |
+
.goal-add {
|
| 268 |
+
display: flex; align-items: center; justify-content: center; gap: .5em;
|
| 269 |
+
width: 100%;
|
| 270 |
+
padding: clamp(11px, 3.2cqw, 15px);
|
| 271 |
+
border: 1.5px dashed var(--control-border-color);
|
| 272 |
+
border-radius: var(--control-radius);
|
| 273 |
+
background: none; cursor: pointer;
|
| 274 |
+
font-family: var(--font-body); font-weight: 700;
|
| 275 |
+
color: var(--muted-color);
|
| 276 |
+
font-size: clamp(14px, 4cqw, 17px);
|
| 277 |
+
transition: border-color .15s, color .15s;
|
| 278 |
}
|
| 279 |
+
.goal-add:hover { border-color: var(--accent); color: var(--accent); }
|
| 280 |
+
.goal-add svg { width: 20px; height: 20px; }
|
| 281 |
|
| 282 |
+
/* ============================================================================
|
| 283 |
+
PHOTOS
|
| 284 |
+
========================================================================== */
|
| 285 |
+
.photos { display: flex; flex-wrap: wrap; gap: clamp(14px, 4cqw, 20px); }
|
| 286 |
+
.photo { display: flex; flex-direction: column; align-items: center; gap: 8px; width: 88px; }
|
| 287 |
+
.photo__slot { position: relative; width: 88px; height: 88px; }
|
| 288 |
+
.photo__slot image-slot { display: block; width: 88px; height: 88px; border: 3px solid var(--card-bg); border-radius: 50%; box-shadow: 0 0 0 1.5px var(--control-border-color), var(--control-shadow); }
|
| 289 |
+
.photo__rm { position: absolute; top: -2px; right: -2px; width: 26px; height: 26px; border-radius: 50%; border: 1.5px solid var(--control-border-color); background: var(--card-bg); color: var(--body-color); cursor: pointer; display: grid; place-items: center; box-shadow: var(--control-shadow); }
|
| 290 |
+
.photo__rm svg { width: 13px; height: 13px; }
|
| 291 |
+
.photo__name { width: 100%; text-align: center; border: none; outline: none; background: transparent; font-family: var(--font-body); font-weight: 600; font-size: 13px; color: var(--muted-color); }
|
| 292 |
+
.photo-add {
|
| 293 |
+
width: 88px; height: 88px; border-radius: 50%; cursor: pointer;
|
| 294 |
+
border: 2px dashed var(--control-border-color); background: transparent;
|
| 295 |
+
color: var(--muted-color);
|
| 296 |
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
|
| 297 |
+
font-family: var(--font-body); font-weight: 700; font-size: 12px;
|
| 298 |
+
transition: border-color .15s, color .15s, background .15s;
|
| 299 |
}
|
| 300 |
+
.photo-add:hover { border-color: var(--accent); color: var(--accent); }
|
| 301 |
+
.photo-add svg { width: 24px; height: 24px; }
|
| 302 |
|
| 303 |
+
/* ============================================================================
|
| 304 |
+
TONE CHIPS
|
| 305 |
+
========================================================================== */
|
| 306 |
+
.tones { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(8px, 2.4cqw, 12px); }
|
| 307 |
+
.tone-chip {
|
| 308 |
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .5em;
|
| 309 |
+
padding: clamp(12px, 3.4cqw, 17px) 4px;
|
| 310 |
+
border: 1.5px solid var(--field-border-color);
|
| 311 |
+
border-radius: var(--chip-radius);
|
| 312 |
+
background: var(--field-bg); cursor: pointer;
|
| 313 |
+
font-family: var(--font-body); font-weight: 700;
|
| 314 |
+
font-size: clamp(11px, 3cqw, 14px); color: var(--field-fg);
|
| 315 |
+
box-shadow: var(--control-shadow);
|
| 316 |
+
transition: border-color .15s, background .15s, box-shadow .15s;
|
| 317 |
}
|
| 318 |
+
.tone-chip svg { width: clamp(26px, 7cqw, 32px); height: clamp(26px, 7cqw, 32px); color: var(--field-fg-muted); transition: color .15s; }
|
| 319 |
+
.tone-chip[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
| 320 |
+
.tone-chip[aria-pressed="true"] svg { color: var(--accent-ink); }
|
| 321 |
+
.tone-chip[aria-pressed="true"] span { color: var(--accent-ink); }
|
| 322 |
|
| 323 |
+
/* ============================================================================
|
| 324 |
+
STATUS FOOTER (builder)
|
| 325 |
+
========================================================================== */
|
| 326 |
+
.statusbar {
|
| 327 |
+
display: flex; align-items: center; gap: 14px;
|
| 328 |
+
margin-top: clamp(20px, 6cqw, 28px);
|
| 329 |
+
padding: 14px 18px;
|
| 330 |
+
background: var(--panel-bg);
|
| 331 |
+
border: 1px solid var(--panel-border-color);
|
| 332 |
+
border-radius: var(--control-radius);
|
| 333 |
}
|
| 334 |
+
.statusbar__dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--success); box-shadow: 0 0 8px var(--success); }
|
| 335 |
+
.statusbar__info { flex: 1; min-width: 0; font-size: clamp(12px, 3.4cqw, 14px); line-height: 1.5; }
|
| 336 |
+
.statusbar__info b { color: var(--accent); font-weight: 800; }
|
| 337 |
+
.statusbar__info .k { color: var(--muted-color); font-weight: 700; }
|
| 338 |
|
| 339 |
+
/* ============================================================================
|
| 340 |
+
QUEST SCREEN
|
| 341 |
+
========================================================================== */
|
| 342 |
+
.quest-top { display: flex; align-items: center; gap: 12px; margin-bottom: clamp(14px, 4cqw, 20px); }
|
| 343 |
+
.quest-top .banner { flex: 1 1 auto; }
|
| 344 |
+
.viewtoggle { display: inline-flex; gap: 4px; background: var(--panel-bg); border: 1px solid var(--panel-border-color); padding: 4px; border-radius: var(--pill-radius); flex: none; }
|
| 345 |
+
.vt { display: inline-flex; align-items: center; gap: 6px; border: none; cursor: pointer; background: transparent; color: var(--muted-color); font-family: var(--font-body); font-weight: 700; font-size: clamp(11px, 3cqw, 13px); padding: 8px 12px; border-radius: var(--pill-radius); transition: background .15s, color .15s; white-space: nowrap; }
|
| 346 |
+
.vt svg { width: 15px; height: 15px; }
|
| 347 |
+
.vt[aria-pressed="true"] { background: var(--accent); color: var(--on-primary); }
|
| 348 |
+
|
| 349 |
+
.hero { position: relative; border-radius: var(--control-radius); overflow: hidden; box-shadow: var(--control-shadow); }
|
| 350 |
+
.hero image-slot { display: block; width: 100%; aspect-ratio: 16 / 9; }
|
| 351 |
+
.hero__points { position: absolute; top: 14px; right: 14px; display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px 5px 8px; border-radius: var(--pill-radius); background: rgba(20,16,10,.5); backdrop-filter: blur(4px); color: #fff; font-family: var(--font-display); font-weight: var(--display-weight); font-size: 16px; }
|
| 352 |
+
.hero__points svg { width: 17px; height: 17px; color: var(--reward); }
|
| 353 |
+
.hero__rank { position: absolute; top: 14px; left: 14px; padding: 5px 12px; border-radius: 6px; background: var(--accent); color: var(--on-primary); font-family: var(--font-display); font-weight: var(--display-weight); text-transform: uppercase; letter-spacing: .06em; font-size: 12px; box-shadow: 0 2px 6px rgba(0,0,0,.3); }
|
| 354 |
+
|
| 355 |
+
.quest-headline { position: relative; margin-top: clamp(-30px, -6cqw, -18px); text-align: center; z-index: 2; }
|
| 356 |
+
.quest-headline .eyebrow { font-family: var(--font-display); font-weight: var(--display-weight); text-transform: var(--display-transform); letter-spacing: var(--display-tracking); color: var(--banner-text); font-size: clamp(15px, 3.6cqw, 20px); }
|
| 357 |
+
.quest-headline .quest-title { font-family: var(--font-display); font-weight: var(--display-weight); text-transform: var(--display-transform); letter-spacing: var(--display-tracking); line-height: var(--display-line); color: var(--quest-title-color); font-size: clamp(30px, 8cqw, 46px); margin: .12em 0; }
|
| 358 |
+
|
| 359 |
+
/* parent panel */
|
| 360 |
+
.parent {
|
| 361 |
+
margin-top: clamp(18px, 5cqw, 24px);
|
| 362 |
+
background: var(--panel-bg);
|
| 363 |
+
border: 1px solid var(--panel-border-color);
|
| 364 |
+
border-radius: var(--control-radius);
|
| 365 |
+
padding: clamp(16px, 4.4cqw, 22px);
|
| 366 |
}
|
| 367 |
+
.parent__head { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
|
| 368 |
+
.parent__head svg { width: 18px; height: 18px; color: var(--accent); }
|
| 369 |
+
.parent__head h3 { font-family: var(--font-display); font-weight: var(--display-weight); text-transform: var(--display-transform); letter-spacing: var(--display-tracking); color: var(--heading-color); font-size: clamp(16px, 4cqw, 20px); }
|
| 370 |
+
.parent__grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr; gap: 16px; margin-bottom: 18px; }
|
| 371 |
+
.field .lab { font-family: var(--font-body); font-size: 12px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--muted-color); margin-bottom: 7px; }
|
| 372 |
+
.field__input { width: 100%; background: var(--field-bg); color: var(--field-fg); border: 1.5px solid var(--field-border-color); border-radius: var(--control-radius); padding: 11px 13px; font-family: var(--font-body); font-size: 15px; font-weight: 600; outline: none; }
|
| 373 |
+
.field__input:focus { border-color: var(--accent); }
|
| 374 |
+
.field .big { font-family: var(--font-display); font-weight: var(--display-weight); font-size: 20px; color: var(--heading-color); }
|
| 375 |
+
.field .pts { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-display); font-weight: var(--display-weight); font-size: 20px; color: var(--heading-color); }
|
| 376 |
+
.field .pts svg { width: 18px; height: 18px; color: var(--reward); }
|
| 377 |
+
.parent__btns { display: flex; gap: 12px; }
|
| 378 |
+
.parent__btns .btn-ghost { flex: 1; justify-content: center; }
|
| 379 |
+
.parent__btns .danger { flex: none; color: var(--accent); }
|
| 380 |
|
| 381 |
+
.kidnote { margin-top: clamp(18px, 5cqw, 24px); border: 2px dashed var(--control-border-color); border-radius: var(--control-radius); padding: 22px; text-align: center; color: var(--muted-color); font-weight: 600; font-size: clamp(13px, 3.6cqw, 15px); }
|
| 382 |
+
.kidnote b { display: block; margin-bottom: 6px; font-family: var(--font-display); font-weight: var(--display-weight); text-transform: var(--display-transform); letter-spacing: var(--display-tracking); color: var(--accent); font-size: 18px; }
|
| 383 |
+
|
| 384 |
+
/* footer line on builder */
|
| 385 |
+
.foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px 18px; margin-top: clamp(16px, 4cqw, 20px); color: var(--faint-color); font-weight: 700; font-size: clamp(11px, 3.2cqw, 14px); }
|
| 386 |
+
.foot__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); }
|
| 387 |
+
.foot span { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
|
| 388 |
+
|
| 389 |
+
/* screen transition */
|
| 390 |
+
@keyframes screenIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
| 391 |
+
@media (prefers-reduced-motion: no-preference) { .screen-in { animation: screenIn .34s cubic-bezier(.2,.7,.3,1); } }
|
| 392 |
+
|
| 393 |
+
/* loading spinner inside CTA */
|
| 394 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 395 |
+
.spinner { width: 1.2em; height: 1.2em; border-radius: 50%; border: 3px solid rgba(255,255,255,.4); border-top-color: currentColor; animation: spin .7s linear infinite; }
|
| 396 |
+
|
| 397 |
+
@media (max-width: 520px) {
|
| 398 |
+
.tones { grid-template-columns: repeat(2, 1fr); }
|
| 399 |
+
.parent__grid { grid-template-columns: 1fr 1fr; }
|
| 400 |
}
|
frontend/styles/tokens.css
CHANGED
|
@@ -1,54 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
:root {
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
--font-
|
| 5 |
-
--
|
| 6 |
-
|
| 7 |
-
--
|
| 8 |
-
--
|
| 9 |
-
--
|
| 10 |
-
--
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
--
|
| 14 |
-
--
|
| 15 |
-
--
|
| 16 |
-
--
|
| 17 |
-
--
|
| 18 |
-
--
|
| 19 |
-
--
|
| 20 |
-
--
|
| 21 |
-
--
|
| 22 |
-
|
| 23 |
-
--
|
| 24 |
-
--
|
| 25 |
-
--
|
| 26 |
-
--
|
| 27 |
-
--
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
--
|
| 32 |
-
--
|
| 33 |
-
--
|
| 34 |
-
--
|
| 35 |
-
--
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
--
|
| 40 |
-
--
|
| 41 |
-
--
|
| 42 |
-
--
|
| 43 |
-
--
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
--
|
| 47 |
-
--
|
| 48 |
-
--
|
| 49 |
-
|
| 50 |
-
--
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================================
|
| 2 |
+
EPIC ERRANDS — THEMEABLE TOKEN SYSTEM
|
| 3 |
+
----------------------------------------------------------------------------
|
| 4 |
+
One app, three skins. The whole UI is painted from a single SEMANTIC token
|
| 5 |
+
contract (declared in :root below as documentation defaults). Each theme is
|
| 6 |
+
a complete re-binding of those tokens, scoped under [data-theme="…"], plus a
|
| 7 |
+
small block of theme-scoped "flourish" rules for decoration that a flat
|
| 8 |
+
token cannot express (halftone, speech bubbles, ornate frames, lined paper).
|
| 9 |
+
|
| 10 |
+
The DOM never changes between themes — only the values behind these names.
|
| 11 |
+
|
| 12 |
+
THEMES
|
| 13 |
+
storybook — crayon / kids' picture-book (Baloo 2 · Nunito)
|
| 14 |
+
comic — comic-book command center (Bangers · Hanken Grotesk)
|
| 15 |
+
atelier — parchment questbook (Playfair Display · Lora)
|
| 16 |
+
========================================================================== */
|
| 17 |
+
|
| 18 |
+
/* ============================================================================
|
| 19 |
+
SEMANTIC TOKEN CONTRACT (names every component is allowed to reference)
|
| 20 |
+
The values here are neutral fallbacks; a theme always overrides them.
|
| 21 |
+
========================================================================== */
|
| 22 |
:root {
|
| 23 |
+
/* — Type — */
|
| 24 |
+
--font-display: system-ui, sans-serif;
|
| 25 |
+
--font-body: system-ui, sans-serif;
|
| 26 |
+
--display-weight: 800;
|
| 27 |
+
--display-transform: none; /* none | uppercase */
|
| 28 |
+
--display-tracking: 0;
|
| 29 |
+
--display-line: 1.05;
|
| 30 |
+
--display-stroke: 0; /* px of -webkit-text-stroke on display text */
|
| 31 |
+
--display-stroke-color: transparent;
|
| 32 |
+
|
| 33 |
+
/* — Page & surfaces — */
|
| 34 |
+
--page-bg: #f4f5f7;
|
| 35 |
+
--page-fg-pattern: none; /* optional page texture overlay */
|
| 36 |
+
--card-bg: #ffffff;
|
| 37 |
+
--card-radius: 24px;
|
| 38 |
+
--card-border-width: 0px;
|
| 39 |
+
--card-border-color: transparent;
|
| 40 |
+
--card-shadow: 0 24px 60px -22px rgba(30,40,60,.4);
|
| 41 |
+
--card-texture: none; /* background-image overlay inside the card */
|
| 42 |
+
--card-max: 540px;
|
| 43 |
+
|
| 44 |
+
--panel-bg: #f3f4f6; /* raised inner panels: status bar, parent box */
|
| 45 |
+
--panel-border-color: #e5e7eb;
|
| 46 |
+
--field-bg: #fcfcfd; /* text inputs / goal rows */
|
| 47 |
+
--field-border-color: #e2e5e9;
|
| 48 |
+
--field-fg: #353a42; /* text ON a field surface (may differ from body) */
|
| 49 |
+
--field-fg-muted: #8b9097; /* placeholder / secondary text on a field */
|
| 50 |
+
|
| 51 |
+
/* — Text roles — */
|
| 52 |
+
--heading-color: #1f2430;
|
| 53 |
+
--body-color: #353a42;
|
| 54 |
+
--muted-color: #8b9097;
|
| 55 |
+
--faint-color: #b3b8bf;
|
| 56 |
+
--on-primary: #ffffff;
|
| 57 |
+
|
| 58 |
+
/* — Accent + primary action — */
|
| 59 |
+
--accent: #2f7fd6; /* selection ring, links, secondary highlight */
|
| 60 |
+
--accent-soft: #e7f0fb; /* selected chip fill */
|
| 61 |
+
--accent-ink: #1d4f88; /* text/icon on accent-soft */
|
| 62 |
+
--primary: #2f7fd6; /* CTA fill (may be a gradient) */
|
| 63 |
+
--primary-edge: #1d5fa8; /* 3D bottom edge under CTA */
|
| 64 |
+
--primary-text: #ffffff;
|
| 65 |
+
|
| 66 |
+
/* — Semantic status colors — */
|
| 67 |
+
--success: #2f9e4e; /* checks and completed states */
|
| 68 |
+
--reward: #f6b81e; /* points, stars, ribbons */
|
| 69 |
+
--quest-title-color: #1f2430; /* the big generated quest title */
|
| 70 |
+
--banner-bg: #2f7fd6; /* top banner on the quest screen */
|
| 71 |
+
--banner-text: #ffffff;
|
| 72 |
+
|
| 73 |
+
/* — Controls — */
|
| 74 |
+
--control-radius: 14px;
|
| 75 |
+
--chip-radius: 14px;
|
| 76 |
+
--pill-radius: 999px;
|
| 77 |
+
--control-border-color: #e2e5e9;
|
| 78 |
+
--control-shadow: 0 2px 5px -2px rgba(30,40,60,.12);
|
| 79 |
+
|
| 80 |
+
/* — Button geometry per theme — */
|
| 81 |
+
--primary-radius: 999px;
|
| 82 |
+
--primary-shadow: 0 10px 22px -8px rgba(47,127,214,.55);
|
| 83 |
+
|
| 84 |
+
/* image-slot theming hook */
|
| 85 |
+
--is-bg: #eef1f5;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* ============================================================================
|
| 89 |
+
THEME 1 — STORYBOOK (crayon, picture-book, soft & sunny)
|
| 90 |
+
========================================================================== */
|
| 91 |
+
[data-theme="storybook"] {
|
| 92 |
+
--font-display: 'Baloo 2', cursive;
|
| 93 |
+
--font-body: 'Nunito', system-ui, sans-serif;
|
| 94 |
+
--display-weight: 800;
|
| 95 |
+
--display-transform: none;
|
| 96 |
+
--display-tracking: -.01em;
|
| 97 |
+
--display-line: 1.02;
|
| 98 |
+
--display-stroke: 0;
|
| 99 |
+
|
| 100 |
+
--page-bg: radial-gradient(125% 90% at 50% 0%, #eef6fd 0%, #e4eff9 50%, #d9e7f4 100%);
|
| 101 |
+
--card-bg: #ffffff;
|
| 102 |
+
--card-radius: 30px;
|
| 103 |
+
--card-border-width: 0px;
|
| 104 |
+
--card-border-color: transparent;
|
| 105 |
+
--card-shadow: 0 24px 60px -22px rgba(40,55,80,.42);
|
| 106 |
+
--card-texture:
|
| 107 |
+
radial-gradient(60px 34px at 16% 5%, rgba(120,180,230,.10), transparent 70%),
|
| 108 |
+
radial-gradient(80px 40px at 88% 8%, rgba(120,180,230,.09), transparent 70%),
|
| 109 |
+
repeating-linear-gradient(180deg, transparent 0 33px, rgba(120,150,180,.045) 33px 34px);
|
| 110 |
+
--card-max: 500px;
|
| 111 |
+
|
| 112 |
+
--panel-bg: #f3f7fb;
|
| 113 |
+
--panel-border-color: #e4e9ef;
|
| 114 |
+
--field-bg: #fcfcfd;
|
| 115 |
+
--field-border-color: #e4e6e9;
|
| 116 |
+
--field-fg: #34373a;
|
| 117 |
+
--field-fg-muted: #a9aeb4;
|
| 118 |
+
|
| 119 |
+
--heading-color: #163154;
|
| 120 |
+
--body-color: #34373a;
|
| 121 |
+
--muted-color: #8b9097;
|
| 122 |
+
--faint-color: #a9aeb4;
|
| 123 |
+
--on-primary: #ffffff;
|
| 124 |
+
|
| 125 |
+
--accent: #e9382d;
|
| 126 |
+
--accent-soft: #eaf5ee;
|
| 127 |
+
--accent-ink: #2f7f47;
|
| 128 |
+
--primary: linear-gradient(180deg, #e9382d 0%, #c52b22 100%);
|
| 129 |
+
--primary-edge: #a82018;
|
| 130 |
+
--primary-text: #ffffff;
|
| 131 |
+
--primary-radius: 999px;
|
| 132 |
+
--primary-shadow: 0 7px 0 var(--primary-edge), 0 10px 22px -8px rgba(233,56,45,.55);
|
| 133 |
+
|
| 134 |
+
--success: #46a05d;
|
| 135 |
+
--reward: #f6b81e;
|
| 136 |
+
--quest-title-color: #3a2a15;
|
| 137 |
+
--banner-bg: #f4e8c8;
|
| 138 |
+
--banner-text: #3a2a15;
|
| 139 |
+
|
| 140 |
+
--control-radius: 13px;
|
| 141 |
+
--chip-radius: 13px;
|
| 142 |
+
--control-border-color: #e4e6e9;
|
| 143 |
+
--control-shadow: 0 2px 5px -2px rgba(40,55,80,.12);
|
| 144 |
+
|
| 145 |
+
--is-bg: #eaf2fa;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* ============================================================================
|
| 149 |
+
THEME 2 — COMIC (dark command center, halftone, bursts, bold ink)
|
| 150 |
+
========================================================================== */
|
| 151 |
+
[data-theme="comic"] {
|
| 152 |
+
--font-display: 'Bangers', system-ui, sans-serif;
|
| 153 |
+
--font-body: 'Hanken Grotesk', system-ui, sans-serif;
|
| 154 |
+
--display-weight: 400;
|
| 155 |
+
--display-transform: uppercase;
|
| 156 |
+
--display-tracking: .04em;
|
| 157 |
+
--display-line: .92;
|
| 158 |
+
--display-stroke: 0;
|
| 159 |
+
|
| 160 |
+
--page-bg: radial-gradient(120% 80% at 50% -10%, #1a1a1f 0%, #0a0a0b 55%);
|
| 161 |
+
--card-bg: #121214;
|
| 162 |
+
--card-radius: 26px;
|
| 163 |
+
--card-border-width: 6px;
|
| 164 |
+
--card-border-color: #f6a81d;
|
| 165 |
+
--card-shadow: 0 30px 70px rgba(0,0,0,.55), 0 0 0 1px rgba(0,0,0,.6);
|
| 166 |
+
--card-texture: radial-gradient(rgba(255,255,255,.035) 1px, transparent 1.4px);
|
| 167 |
+
--card-max: 620px;
|
| 168 |
+
|
| 169 |
+
--panel-bg: #1b1b1f;
|
| 170 |
+
--panel-border-color: rgba(255,255,255,.12);
|
| 171 |
+
--field-bg: #f1ecdd; /* cream input plates */
|
| 172 |
+
--field-border-color: rgba(0,0,0,.15);
|
| 173 |
+
--field-fg: #1a1a1a; /* dark ink on cream plates */
|
| 174 |
+
--field-fg-muted: #8a8268;
|
| 175 |
+
|
| 176 |
+
--heading-color: #f6a81d; /* gold display headings */
|
| 177 |
+
--body-color: #ededf0;
|
| 178 |
+
--muted-color: #a6a6ad;
|
| 179 |
+
--faint-color: #6f6f77;
|
| 180 |
+
--on-primary: #1a1208;
|
| 181 |
+
|
| 182 |
+
--accent: #2cb4e8;
|
| 183 |
+
--accent-soft: rgba(44,180,232,.12);
|
| 184 |
+
--accent-ink: #2cb4e8;
|
| 185 |
+
--primary: #f6a81d;
|
| 186 |
+
--primary-edge: #b9790a;
|
| 187 |
+
--primary-text: #1a1208;
|
| 188 |
+
--primary-radius: 16px;
|
| 189 |
+
--primary-shadow: 0 8px 0 var(--primary-edge), 0 16px 30px -8px rgba(246,168,29,.5);
|
| 190 |
+
|
| 191 |
+
--success: #74b62e;
|
| 192 |
+
--reward: #f6a81d;
|
| 193 |
+
--quest-title-color: #e23a2c;
|
| 194 |
+
--banner-bg: #f1ecdd;
|
| 195 |
+
--banner-text: #1a1a1a;
|
| 196 |
+
|
| 197 |
+
--control-radius: 12px;
|
| 198 |
+
--chip-radius: 12px;
|
| 199 |
+
--control-border-color: rgba(255,255,255,.16);
|
| 200 |
+
--control-shadow: 0 6px 18px rgba(0,0,0,.35);
|
| 201 |
+
|
| 202 |
+
--is-bg: #1b1b1f;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* ============================================================================
|
| 206 |
+
THEME 3 — ATELIER (parchment questbook, serif, ornate gold, calm)
|
| 207 |
+
========================================================================== */
|
| 208 |
+
[data-theme="atelier"] {
|
| 209 |
+
--font-display: 'Playfair Display', Georgia, serif;
|
| 210 |
+
--font-body: 'Lora', Georgia, serif;
|
| 211 |
+
--display-weight: 800;
|
| 212 |
+
--display-transform: none;
|
| 213 |
+
--display-tracking: 0;
|
| 214 |
+
--display-line: 1.05;
|
| 215 |
+
--display-stroke: 0;
|
| 216 |
+
|
| 217 |
+
--page-bg:
|
| 218 |
+
radial-gradient(120% 80% at 50% -10%, rgba(255,250,238,.9), transparent 60%),
|
| 219 |
+
radial-gradient(100% 60% at 100% 110%, rgba(214,196,156,.35), transparent 55%),
|
| 220 |
+
radial-gradient(80% 50% at 0% 100%, rgba(214,196,156,.25), transparent 55%),
|
| 221 |
+
#f3ead6;
|
| 222 |
+
--card-bg: #f8f1e1;
|
| 223 |
+
--card-radius: 22px;
|
| 224 |
+
--card-border-width: 1px;
|
| 225 |
+
--card-border-color: #e3d6b8;
|
| 226 |
+
--card-shadow: 0 2px 4px rgba(70,50,20,.12), 0 18px 40px -16px rgba(70,50,20,.40);
|
| 227 |
+
--card-texture:
|
| 228 |
+
radial-gradient(140% 90% at 50% 0%, rgba(255,251,242,.7), transparent 55%),
|
| 229 |
+
radial-gradient(100% 70% at 100% 100%, rgba(220,202,162,.25), transparent 60%);
|
| 230 |
+
--card-max: 600px;
|
| 231 |
+
|
| 232 |
+
--panel-bg: #f1e7cf;
|
| 233 |
+
--panel-border-color: #e3d6b8;
|
| 234 |
+
--field-bg: #fcf8ee;
|
| 235 |
+
--field-border-color: #d4c39c;
|
| 236 |
+
--field-fg: #574b39;
|
| 237 |
+
--field-fg-muted: #b0a286;
|
| 238 |
+
|
| 239 |
+
--heading-color: #2d2417;
|
| 240 |
+
--body-color: #574b39;
|
| 241 |
+
--muted-color: #8c7e66;
|
| 242 |
+
--faint-color: #b0a286;
|
| 243 |
+
--on-primary: #f4efd9;
|
| 244 |
+
|
| 245 |
+
--accent: #c8a25c;
|
| 246 |
+
--accent-soft: #f1e4c5;
|
| 247 |
+
--accent-ink: #8a6c3c;
|
| 248 |
+
--primary: linear-gradient(180deg, #4e6038 0%, #43542f 100%);
|
| 249 |
+
--primary-edge: #283318;
|
| 250 |
+
--primary-text: #f4efd9;
|
| 251 |
+
--primary-radius: 12px;
|
| 252 |
+
--primary-shadow: 0 2px 0 var(--primary-edge), 0 8px 18px -4px rgba(40,55,25,.5);
|
| 253 |
+
|
| 254 |
+
--success: #2e9e6a;
|
| 255 |
+
--reward: #e5b23c;
|
| 256 |
+
--quest-title-color: #573b7e;
|
| 257 |
+
--banner-bg: transparent;
|
| 258 |
+
--banner-text: #2d2417;
|
| 259 |
+
|
| 260 |
+
--control-radius: 12px;
|
| 261 |
+
--chip-radius: 12px;
|
| 262 |
+
--control-border-color: #d4c39c;
|
| 263 |
+
--control-shadow: 0 1px 2px rgba(80,60,30,.05), 0 8px 24px -12px rgba(80,60,30,.18);
|
| 264 |
+
|
| 265 |
+
--is-bg: #f1e7cf;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/* ============================================================================
|
| 269 |
+
FLOURISHES — theme-scoped decoration on the SHARED DOM.
|
| 270 |
+
These do not change structure; they layer ornament that flat tokens cannot.
|
| 271 |
+
========================================================================== */
|
| 272 |
+
|
| 273 |
+
/* ---- STORYBOOK: chunky candy buttons, dashed dividers, sunny chips ---- */
|
| 274 |
+
[data-theme="storybook"] .divider { border: none; border-top: 2px dashed var(--field-border-color); }
|
| 275 |
+
[data-theme="storybook"] .btn-primary:active { transform: translateY(5px); box-shadow: 0 2px 0 var(--primary-edge), 0 6px 12px -8px rgba(233,56,45,.5); }
|
| 276 |
+
[data-theme="storybook"] .quest-headline { background: linear-gradient(180deg,#fbf3dd,#f4e8c8 55%,#e8d6a8); border-radius: 14px; padding: 22px 26px; box-shadow: inset 0 2px 0 rgba(255,255,255,.6), inset 0 -10px 22px -10px rgba(150,120,60,.35), 0 14px 26px -14px rgba(80,60,20,.5); }
|
| 277 |
+
[data-theme="storybook"] .tone-chip[aria-pressed="true"] { border-color: var(--success); background: var(--accent-soft); }
|
| 278 |
+
[data-theme="storybook"] .banner { border-radius: 999px; }
|
| 279 |
+
|
| 280 |
+
/* ---- COMIC: halftone, black outlines, speech bubble, burst CTA ---- */
|
| 281 |
+
[data-theme="comic"] .halftone { background-image: radial-gradient(rgba(0,0,0,.20) 1.3px, transparent 1.6px); background-size: 8px 8px; }
|
| 282 |
+
[data-theme="comic"] .card-inner > * { color: inherit; }
|
| 283 |
+
[data-theme="comic"] .btn-primary {
|
| 284 |
+
-webkit-text-stroke: 1px rgba(0,0,0,.25); paint-order: stroke fill;
|
| 285 |
}
|
| 286 |
+
[data-theme="comic"] .btn-primary:active { transform: translateY(4px); box-shadow: 0 4px 0 var(--primary-edge), 0 8px 14px -8px rgba(0,0,0,.4); }
|
| 287 |
+
[data-theme="comic"] .tone-chip { border-width: 2px; }
|
| 288 |
+
[data-theme="comic"] .tone-chip[aria-pressed="true"] { border-color: var(--reward); background: var(--accent-soft); box-shadow: 0 0 0 2px var(--reward) inset; }
|
| 289 |
+
/* cream banner with torn comic edge */
|
| 290 |
+
[data-theme="comic"] .banner {
|
| 291 |
+
transform: rotate(-1.2deg);
|
| 292 |
+
background-image: radial-gradient(rgba(0,0,0,.10) 1.1px, transparent 1.4px);
|
| 293 |
+
background-size: 8px 8px;
|
| 294 |
+
clip-path: polygon(0 14%, 6% 0, 22% 10%, 40% 2%, 62% 11%, 82% 1%, 96% 12%, 100% 4%, 99% 84%, 92% 100%, 74% 90%, 52% 99%, 30% 89%, 12% 100%, 3% 88%, 0 96%);
|
| 295 |
+
}
|
| 296 |
+
/* quest headline becomes a speech bubble */
|
| 297 |
+
[data-theme="comic"] .quest-headline {
|
| 298 |
+
background: var(--banner-bg);
|
| 299 |
+
color: var(--banner-text);
|
| 300 |
+
padding: 26px 30px 30px;
|
| 301 |
+
filter: drop-shadow(0 8px 16px rgba(0,0,0,.4));
|
| 302 |
+
clip-path: polygon(3% 18%, 12% 6%, 22% 16%, 33% 2%, 45% 14%, 57% 0%, 68% 13%, 80% 3%, 90% 15%, 99% 8%, 96% 30%, 100% 44%, 95% 58%, 100% 72%, 90% 80%, 94% 96%, 78% 90%, 70% 100%, 60% 90%, 50% 99%, 40% 89%, 30% 99%, 21% 88%, 10% 96%, 6% 80%, 0% 66%, 4% 50%, 0% 34%);
|
| 303 |
+
}
|
| 304 |
+
[data-theme="comic"] .quest-headline .quest-title { -webkit-text-stroke: 2px #1a1a1a; paint-order: stroke fill; }
|
| 305 |
+
[data-theme="comic"] .divider { border: none; border-top: 1px solid var(--panel-border-color); }
|
| 306 |
+
/* ---- ATELIER: ornate gold inset frame, diamond dividers, serif calm ---- */
|
| 307 |
+
[data-theme="atelier"] .card.ornate {
|
| 308 |
+
box-shadow: var(--card-shadow), inset 0 0 0 2px var(--accent), inset 0 0 0 3px var(--card-bg), inset 0 0 0 4px rgba(200,162,92,.4);
|
| 309 |
+
}
|
| 310 |
+
[data-theme="atelier"] .card.ornate .corner-flourish { display: block; }
|
| 311 |
+
[data-theme="atelier"] .divider {
|
| 312 |
+
border: none; height: 1px; background: var(--field-border-color);
|
| 313 |
+
position: relative; overflow: visible;
|
| 314 |
+
}
|
| 315 |
+
[data-theme="atelier"] .divider::after {
|
| 316 |
+
content: ""; position: absolute; left: 50%; top: 50%;
|
| 317 |
+
width: 9px; height: 9px; transform: translate(-50%,-50%) rotate(45deg);
|
| 318 |
+
background: var(--accent); box-shadow: 0 0 0 3px var(--card-bg);
|
| 319 |
+
}
|
| 320 |
+
[data-theme="atelier"] .quest-headline { background: linear-gradient(180deg, #fcf7ea, #f6eedb); border-radius: 14px; padding: clamp(20px,3.5vw,30px); box-shadow: 0 -2px 0 rgba(255,255,255,.6), 0 10px 30px -10px rgba(70,50,20,.35), inset 0 0 0 1px rgba(180,150,100,.18); }
|
| 321 |
+
[data-theme="atelier"] .tone-chip[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
| 322 |
+
[data-theme="atelier"] .banner { border-radius: 12px; }
|
| 323 |
+
|
| 324 |
+
/* corner flourishes are hidden unless the atelier ornate card shows them */
|
| 325 |
+
.corner-flourish { display: none; }
|