codemble 0.6.2__tar.gz → 0.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. {codemble-0.6.2 → codemble-0.6.4}/CHANGELOG.md +73 -0
  2. {codemble-0.6.2 → codemble-0.6.4}/CLAUDE.md +40 -12
  3. {codemble-0.6.2 → codemble-0.6.4}/PKG-INFO +8 -3
  4. {codemble-0.6.2 → codemble-0.6.4}/README.md +7 -2
  5. {codemble-0.6.2 → codemble-0.6.4}/TESTING.md +7 -1
  6. {codemble-0.6.2 → codemble-0.6.4}/codemble/__init__.py +1 -1
  7. {codemble-0.6.2 → codemble-0.6.4}/codemble/checks/service.py +14 -8
  8. {codemble-0.6.2 → codemble-0.6.4}/codemble/lens/python.py +6 -1
  9. {codemble-0.6.2 → codemble-0.6.4}/codemble/progress/store.py +44 -7
  10. {codemble-0.6.2 → codemble-0.6.4}/codemble/server/project_selection.py +16 -1
  11. codemble-0.6.4/codemble/web_dist/assets/index-BBC-ag63.css +1 -0
  12. codemble-0.6.2/codemble/web_dist/assets/index-_4l8WmkY.js → codemble-0.6.4/codemble/web_dist/assets/index-DvJD5a9M.js +1 -1
  13. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/index.html +2 -2
  14. codemble-0.6.4/docs/releases/v0.6.3.md +40 -0
  15. codemble-0.6.4/docs/releases/v0.6.4.md +89 -0
  16. {codemble-0.6.2 → codemble-0.6.4}/docs-site/package.json +1 -1
  17. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/quickstart.md +5 -0
  18. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/study-panel.md +5 -1
  19. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/the-galaxy.md +12 -3
  20. {codemble-0.6.2 → codemble-0.6.4}/pyproject.toml +1 -1
  21. {codemble-0.6.2 → codemble-0.6.4}/tests/test_checks.py +29 -0
  22. {codemble-0.6.2 → codemble-0.6.4}/tests/test_progress.py +55 -0
  23. {codemble-0.6.2 → codemble-0.6.4}/tests/test_project_selection.py +34 -0
  24. {codemble-0.6.2 → codemble-0.6.4}/web/package-lock.json +2 -2
  25. {codemble-0.6.2 → codemble-0.6.4}/web/package.json +2 -2
  26. codemble-0.6.4/web/scripts/check_focus_handoffs.mjs +22 -0
  27. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/check_graph_data.mjs +32 -2
  28. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/check_learner_projection.mjs +125 -1
  29. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/check_learner_session.mjs +25 -2
  30. codemble-0.6.4/web/scripts/check_map_viewport.mjs +38 -0
  31. {codemble-0.6.2 → codemble-0.6.4}/web/src/App.jsx +392 -98
  32. {codemble-0.6.2 → codemble-0.6.4}/web/src/GuidanceLayer.jsx +7 -5
  33. {codemble-0.6.2 → codemble-0.6.4}/web/src/MapView.jsx +97 -50
  34. {codemble-0.6.2 → codemble-0.6.4}/web/src/ModeControl.jsx +1 -20
  35. {codemble-0.6.2 → codemble-0.6.4}/web/src/StudyPanel.jsx +8 -3
  36. {codemble-0.6.2 → codemble-0.6.4}/web/src/graphData.js +24 -6
  37. {codemble-0.6.2 → codemble-0.6.4}/web/src/learnerProjection.js +74 -10
  38. {codemble-0.6.2 → codemble-0.6.4}/web/src/learnerSession.js +31 -0
  39. codemble-0.6.4/web/src/mapViewport.js +45 -0
  40. {codemble-0.6.2 → codemble-0.6.4}/web/src/styles.css +121 -28
  41. codemble-0.6.2/codemble/web_dist/assets/index-CsUbeE2C.css +0 -1
  42. {codemble-0.6.2 → codemble-0.6.4}/.env.example +0 -0
  43. {codemble-0.6.2 → codemble-0.6.4}/.gitattributes +0 -0
  44. {codemble-0.6.2 → codemble-0.6.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  45. {codemble-0.6.2 → codemble-0.6.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  46. {codemble-0.6.2 → codemble-0.6.4}/.github/ISSUE_TEMPLATE/early_tester.yml +0 -0
  47. {codemble-0.6.2 → codemble-0.6.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  48. {codemble-0.6.2 → codemble-0.6.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  49. {codemble-0.6.2 → codemble-0.6.4}/.github/dependabot.yml +0 -0
  50. {codemble-0.6.2 → codemble-0.6.4}/.github/workflows/ci.yml +0 -0
  51. {codemble-0.6.2 → codemble-0.6.4}/.github/workflows/pages.yml +0 -0
  52. {codemble-0.6.2 → codemble-0.6.4}/.github/workflows/publish-pypi.yml +0 -0
  53. {codemble-0.6.2 → codemble-0.6.4}/.gitignore +0 -0
  54. {codemble-0.6.2 → codemble-0.6.4}/AGENTS.md +0 -0
  55. {codemble-0.6.2 → codemble-0.6.4}/CODE_OF_CONDUCT.md +0 -0
  56. {codemble-0.6.2 → codemble-0.6.4}/CONTEXT.md +0 -0
  57. {codemble-0.6.2 → codemble-0.6.4}/CONTRIBUTING.md +0 -0
  58. {codemble-0.6.2 → codemble-0.6.4}/LICENSE +0 -0
  59. {codemble-0.6.2 → codemble-0.6.4}/NOTICE +0 -0
  60. {codemble-0.6.2 → codemble-0.6.4}/SECURITY.md +0 -0
  61. {codemble-0.6.2 → codemble-0.6.4}/assets/demo.gif +0 -0
  62. {codemble-0.6.2 → codemble-0.6.4}/codemble/adapters/__init__.py +0 -0
  63. {codemble-0.6.2 → codemble-0.6.4}/codemble/adapters/base.py +0 -0
  64. {codemble-0.6.2 → codemble-0.6.4}/codemble/adapters/discovery.py +0 -0
  65. {codemble-0.6.2 → codemble-0.6.4}/codemble/adapters/parse_progress.py +0 -0
  66. {codemble-0.6.2 → codemble-0.6.4}/codemble/adapters/project.py +0 -0
  67. {codemble-0.6.2 → codemble-0.6.4}/codemble/adapters/python_ast.py +0 -0
  68. {codemble-0.6.2 → codemble-0.6.4}/codemble/adapters/typescript_tree_sitter.py +0 -0
  69. {codemble-0.6.2 → codemble-0.6.4}/codemble/checks/__init__.py +0 -0
  70. {codemble-0.6.2 → codemble-0.6.4}/codemble/cli.py +0 -0
  71. {codemble-0.6.2 → codemble-0.6.4}/codemble/graph/__init__.py +0 -0
  72. {codemble-0.6.2 → codemble-0.6.4}/codemble/graph/finalize.py +0 -0
  73. {codemble-0.6.2 → codemble-0.6.4}/codemble/graph/layout.py +0 -0
  74. {codemble-0.6.2 → codemble-0.6.4}/codemble/graph/mapview.py +0 -0
  75. {codemble-0.6.2 → codemble-0.6.4}/codemble/lens/__init__.py +0 -0
  76. {codemble-0.6.2 → codemble-0.6.4}/codemble/lens/javascript_typescript.py +0 -0
  77. {codemble-0.6.2 → codemble-0.6.4}/codemble/llm/__init__.py +0 -0
  78. {codemble-0.6.2 → codemble-0.6.4}/codemble/llm/local_status.py +0 -0
  79. {codemble-0.6.2 → codemble-0.6.4}/codemble/llm/providers.py +0 -0
  80. {codemble-0.6.2 → codemble-0.6.4}/codemble/llm/structural.py +0 -0
  81. {codemble-0.6.2 → codemble-0.6.4}/codemble/llm/study.py +0 -0
  82. {codemble-0.6.2 → codemble-0.6.4}/codemble/paths.py +0 -0
  83. {codemble-0.6.2 → codemble-0.6.4}/codemble/progress/__init__.py +0 -0
  84. {codemble-0.6.2 → codemble-0.6.4}/codemble/server/__init__.py +0 -0
  85. {codemble-0.6.2 → codemble-0.6.4}/codemble/server/app.py +0 -0
  86. {codemble-0.6.2 → codemble-0.6.4}/codemble/server/parse_job.py +0 -0
  87. {codemble-0.6.2 → codemble-0.6.4}/codemble/server/project_activation.py +0 -0
  88. {codemble-0.6.2 → codemble-0.6.4}/codemble/server/runtime.py +0 -0
  89. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  90. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  91. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  92. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  93. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/shippori-mincho-latin-500-normal-C-QwvIb3.woff +0 -0
  94. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/shippori-mincho-latin-500-normal-XI1O8euf.woff2 +0 -0
  95. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/shippori-mincho-latin-700-normal-CkoCYOiI.woff +0 -0
  96. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/shippori-mincho-latin-700-normal-DHcmzUO5.woff2 +0 -0
  97. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/zen-kaku-gothic-new-latin-400-normal-BEdayliK.woff2 +0 -0
  98. {codemble-0.6.2 → codemble-0.6.4}/codemble/web_dist/assets/zen-kaku-gothic-new-latin-400-normal-CPSmNJAU.woff +0 -0
  99. {codemble-0.6.2 → codemble-0.6.4}/docs/adr/README.md +0 -0
  100. {codemble-0.6.2 → codemble-0.6.4}/docs/plans/2026-07-19-audience-modes-and-local-narration-design.md +0 -0
  101. {codemble-0.6.2 → codemble-0.6.4}/docs/plans/2026-07-19-audience-modes-backend-plan.md +0 -0
  102. {codemble-0.6.2 → codemble-0.6.4}/docs/plans/2026-07-19-audience-modes-frontend-plan.md +0 -0
  103. {codemble-0.6.2 → codemble-0.6.4}/docs/plans/2026-07-19-install-ux-folder-picker-design.md +0 -0
  104. {codemble-0.6.2 → codemble-0.6.4}/docs/plans/2026-07-19-install-ux-folder-picker-plan.md +0 -0
  105. {codemble-0.6.2 → codemble-0.6.4}/docs/plans/2026-07-20-oss-inspired-map-and-constellations.md +0 -0
  106. {codemble-0.6.2 → codemble-0.6.4}/docs/plans/README.md +0 -0
  107. {codemble-0.6.2 → codemble-0.6.4}/docs/plans/phase-1.md +0 -0
  108. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/checklist.md +0 -0
  109. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.1.0.md +0 -0
  110. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.2.0.md +0 -0
  111. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.4.0.md +0 -0
  112. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.5.0.md +0 -0
  113. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.5.1.md +0 -0
  114. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.5.2.md +0 -0
  115. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.5.3.md +0 -0
  116. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.6.1.md +0 -0
  117. {codemble-0.6.2 → codemble-0.6.4}/docs/releases/v0.6.2.md +0 -0
  118. {codemble-0.6.2 → codemble-0.6.4}/docs/research/2026-07-20-opensource-graph-architecture-inspiration.md +0 -0
  119. {codemble-0.6.2 → codemble-0.6.4}/docs/research/README.md +0 -0
  120. {codemble-0.6.2 → codemble-0.6.4}/docs/superpowers/plans/2026-07-19-galaxy-ux-phase-a.md +0 -0
  121. {codemble-0.6.2 → codemble-0.6.4}/docs/superpowers/plans/2026-07-19-galaxy-ux-phase-b.md +0 -0
  122. {codemble-0.6.2 → codemble-0.6.4}/docs/superpowers/plans/2026-07-19-galaxy-ux-phase-c.md +0 -0
  123. {codemble-0.6.2 → codemble-0.6.4}/docs/superpowers/plans/2026-07-19-galaxy-ux-shared-contract.md +0 -0
  124. {codemble-0.6.2 → codemble-0.6.4}/docs/superpowers/specs/2026-07-19-galaxy-ux-overhaul-design.md +0 -0
  125. {codemble-0.6.2 → codemble-0.6.4}/docs/superpowers/specs/2026-07-21-navigation-design-clarity-design.md +0 -0
  126. {codemble-0.6.2 → codemble-0.6.4}/docs-site/.gitignore +0 -0
  127. {codemble-0.6.2 → codemble-0.6.4}/docs-site/astro.config.mjs +0 -0
  128. {codemble-0.6.2 → codemble-0.6.4}/docs-site/design.md +0 -0
  129. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/favicon-16.png +0 -0
  130. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/favicon-32.png +0 -0
  131. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/icon-180.png +0 -0
  132. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/icon-192.png +0 -0
  133. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/icon-512.png +0 -0
  134. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/icon.svg +0 -0
  135. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/icons/asterism.svg +0 -0
  136. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/icons/install.svg +0 -0
  137. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/mark-animated.svg +0 -0
  138. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/mark.svg +0 -0
  139. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/plates/hero-chart.svg +0 -0
  140. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/plates/hero-field.svg +0 -0
  141. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/plates/hero-gold.svg +0 -0
  142. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/plates/kasumi-rule.svg +0 -0
  143. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/plates/plate-galaxy.svg +0 -0
  144. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/plates/plate-study.svg +0 -0
  145. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/plates/plate-system.svg +0 -0
  146. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/brand/plates/seal.svg +0 -0
  147. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/favicon.svg +0 -0
  148. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/shots/easy-mode.png +0 -0
  149. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/shots/galaxy-lit.png +0 -0
  150. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/shots/galaxy.png +0 -0
  151. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/shots/loading.png +0 -0
  152. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/shots/map-architecture.png +0 -0
  153. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/shots/map-workflow.png +0 -0
  154. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/shots/study-panel.png +0 -0
  155. {codemble-0.6.2 → codemble-0.6.4}/docs-site/public/shots/system.png +0 -0
  156. {codemble-0.6.2 → codemble-0.6.4}/docs-site/scripts/build-plates.mjs +0 -0
  157. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/assets/codemble-mark-dark.svg +0 -0
  158. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/assets/codemble-mark-light.svg +0 -0
  159. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/components/Search.astro +0 -0
  160. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/components/Tatebanko.astro +0 -0
  161. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/architecture.md +0 -0
  162. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/build-from-source.md +0 -0
  163. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/checks-and-lighting.md +0 -0
  164. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/contributing.md +0 -0
  165. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/correctness.md +0 -0
  166. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/early-testing.md +0 -0
  167. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/installation.md +0 -0
  168. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/introduction.md +0 -0
  169. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m1-parser.md +0 -0
  170. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m10-polyglot-release.md +0 -0
  171. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m12-galaxy-look.md +0 -0
  172. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m13-scale.md +0 -0
  173. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m2-galaxy.md +0 -0
  174. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m3-study.md +0 -0
  175. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m4-lens.md +0 -0
  176. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m5-checks.md +0 -0
  177. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m6-release.md +0 -0
  178. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m8-typescript.md +0 -0
  179. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/progress/m9-typescript-lens.md +0 -0
  180. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/roadmap.md +0 -0
  181. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content/docs/star-chart.md +0 -0
  182. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/content.config.ts +0 -0
  183. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/pages/index.astro +0 -0
  184. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/styles/custom.css +0 -0
  185. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/styles/landing.css +0 -0
  186. {codemble-0.6.2 → codemble-0.6.4}/docs-site/src/styles/tokens.css +0 -0
  187. {codemble-0.6.2 → codemble-0.6.4}/scripts/dev.sh +0 -0
  188. {codemble-0.6.2 → codemble-0.6.4}/scripts/record_demo.sh +0 -0
  189. {codemble-0.6.2 → codemble-0.6.4}/tests/conftest.py +0 -0
  190. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/check_suites.json +0 -0
  191. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/concepts_sample.py +0 -0
  192. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/impact/alpha.py +0 -0
  193. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/impact/beta.py +0 -0
  194. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/impact/gamma.py +0 -0
  195. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/impact/helpers.py +0 -0
  196. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/impact/noisy.py +0 -0
  197. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/polyglot/python_worker.py +0 -0
  198. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/polyglot/src/broken.ts +0 -0
  199. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/polyglot/src/legacy.js +0 -0
  200. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/polyglot/src/local.js +0 -0
  201. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/polyglot/src/main.ts +0 -0
  202. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/polyglot/src/reexport.ts +0 -0
  203. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/polyglot/src/util.ts +0 -0
  204. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/polyglot/src/widget.tsx +0 -0
  205. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/repeated_calls.py +0 -0
  206. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/.gitignore +0 -0
  207. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/ambiguous.py +0 -0
  208. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/api.py +0 -0
  209. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/app.py +0 -0
  210. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/broken.py +0 -0
  211. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/cli.py +0 -0
  212. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/pkg/__init__.py +0 -0
  213. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/pkg/helpers.py +0 -0
  214. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/pkg/service.py +0 -0
  215. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/pkg/util.py +0 -0
  216. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/runner/__main__.py +0 -0
  217. {codemble-0.6.2 → codemble-0.6.4}/tests/fixtures/sampleproj/shared.py +0 -0
  218. {codemble-0.6.2 → codemble-0.6.4}/tests/test_dependency_compatibility.py +0 -0
  219. {codemble-0.6.2 → codemble-0.6.4}/tests/test_graph_finalization.py +0 -0
  220. {codemble-0.6.2 → codemble-0.6.4}/tests/test_mapview.py +0 -0
  221. {codemble-0.6.2 → codemble-0.6.4}/tests/test_parse_job.py +0 -0
  222. {codemble-0.6.2 → codemble-0.6.4}/tests/test_parse_progress.py +0 -0
  223. {codemble-0.6.2 → codemble-0.6.4}/tests/test_project_activation.py +0 -0
  224. {codemble-0.6.2 → codemble-0.6.4}/tests/test_project_parser.py +0 -0
  225. {codemble-0.6.2 → codemble-0.6.4}/tests/test_providers.py +0 -0
  226. {codemble-0.6.2 → codemble-0.6.4}/tests/test_python_ast.py +0 -0
  227. {codemble-0.6.2 → codemble-0.6.4}/tests/test_server.py +0 -0
  228. {codemble-0.6.2 → codemble-0.6.4}/tests/test_smoke.py +0 -0
  229. {codemble-0.6.2 → codemble-0.6.4}/tests/test_structural.py +0 -0
  230. {codemble-0.6.2 → codemble-0.6.4}/tests/test_study.py +0 -0
  231. {codemble-0.6.2 → codemble-0.6.4}/tests/test_typescript_tree_sitter.py +0 -0
  232. {codemble-0.6.2 → codemble-0.6.4}/web/README.md +0 -0
  233. {codemble-0.6.2 → codemble-0.6.4}/web/index.html +0 -0
  234. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/benchmark_learner_projection.mjs +0 -0
  235. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/check_mode_gate_portal.mjs +0 -0
  236. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/check_name_atlas.mjs +0 -0
  237. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/check_orbit_pointer_guard.mjs +0 -0
  238. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/check_project_mapping.mjs +0 -0
  239. {codemble-0.6.2 → codemble-0.6.4}/web/scripts/record_demo.mjs +0 -0
  240. {codemble-0.6.2 → codemble-0.6.4}/web/src/GalaxyCanvas.jsx +0 -0
  241. {codemble-0.6.2 → codemble-0.6.4}/web/src/galaxyEffects.js +0 -0
  242. {codemble-0.6.2 → codemble-0.6.4}/web/src/galaxyMaterials.js +0 -0
  243. {codemble-0.6.2 → codemble-0.6.4}/web/src/main.jsx +0 -0
  244. {codemble-0.6.2 → codemble-0.6.4}/web/src/nameAtlas.js +0 -0
  245. {codemble-0.6.2 → codemble-0.6.4}/web/src/orbitPointerGuard.js +0 -0
  246. {codemble-0.6.2 → codemble-0.6.4}/web/src/projectMapping.js +0 -0
  247. {codemble-0.6.2 → codemble-0.6.4}/web/src/tokens.css +0 -0
  248. {codemble-0.6.2 → codemble-0.6.4}/web/vite.config.js +0 -0
@@ -5,6 +5,79 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.6.4] - 2026-07-21
9
+
10
+ ### Fixed
11
+ - **A missed check no longer hands back its own answer.** Getting a question
12
+ wrong printed the parser answer and its evidence, and the same question then
13
+ accepted that answer — so a region could light without understanding. A miss
14
+ now returns neither the answer nor the citations (an importer check's
15
+ evidence *is* its answer); both appear once the learner has proven it.
16
+ - **The Map can show you the code it is asking about.** Easy mode lands on the
17
+ Map, which offered checks but no way to read a module. Region focus now
18
+ carries **Read the source** beside **Prove understanding**, and Easy guidance
19
+ recommends reading before proving instead of sending the learner to another
20
+ layer. Escape steps back a level on the Map, as it always has in the Galaxy.
21
+ - **The checks panel is usable from the keyboard.** Opening it hands focus to
22
+ the panel instead of leaving it on the trigger behind ~114 tab stops, and
23
+ submitting keeps focus on the result rather than dropping it to the document.
24
+ - **Enter opens the structure the arrow keys selected.** At study level the
25
+ canvas announced a sibling while the panel kept showing the old one; Enter
26
+ was a no-op despite the view's own instructions.
27
+ - **Guidance is docked, and waits its turn.** The hint chip took its own strip
28
+ above the footer instead of floating over captions, name plates and its own
29
+ button, and it no longer appears during the audience, Home and coach steps —
30
+ where it used to recommend a target "because no import route reaches it from
31
+ Home" while no Home existed.
32
+ - **Compact Map controls no longer stack.** At 375 px the Key button covered
33
+ 40% of the "What runs first" tab and the zoom pill covered the top row of
34
+ boxes. Measured at 320 and 375: zero overlapping interactive rectangles.
35
+ - **The study panel stops contradicting itself.** Easy mode showed "Used by 0"
36
+ above a summary saying five other parts use the file; the call-edge stat is
37
+ now "Called by", leaving "use" to imports.
38
+ - **The audience question is asked once per learner, not once per project.**
39
+ A new project inherits the last answer; the header toggle still overrides any
40
+ single project.
41
+ - **Home calibration is navigable on real projects.** It is now a modal sized
42
+ to the window rather than a card capped to a short stage, states how many
43
+ candidates exist, groups them by their real top-level scope (so test fixtures
44
+ no longer sit unmarked beside your entrypoint), and keeps "Explore without
45
+ Home" on screen instead of thousands of pixels below the fold.
46
+ - **The star chart is reachable from anywhere and closes with Escape.** It kept
47
+ its header slot at every level instead of surrendering it to the level-return
48
+ button, and every exit now names the layer it returns to.
49
+ - **Galaxy plates are named by path tail**, the rule the Map's boxes already
50
+ use, so two different `__init__.py` modules no longer wear identical names.
51
+ - **Find opens on Home and the most-used modules** instead of a screen of
52
+ identical `__init__.py` rows.
53
+ - **Easy guidance prefers a substantive module** when several sit the same
54
+ number of import hops from Home, instead of the alphabetically-first
55
+ four-line package init.
56
+ - **The exception-handling lens note is true of `raise`,** not only of
57
+ `try`/`except`, since the parser files all three under one concept.
58
+ - **Counts of one read as one** ("1 structure", not "1 structures").
59
+ - **The picker opens beside your last project** rather than at your home
60
+ directory every time.
61
+
62
+ ## [0.6.3] - 2026-07-21
63
+
64
+ ### Fixed
65
+ - **Easy guidance always advances or becomes an honest instruction.** Opening
66
+ the suggested system from the Map now changes the next action to **View
67
+ structures**, which switches to the Galaxy. Once those structures are on
68
+ screen, the chip asks the learner to choose one and removes the button instead
69
+ of offering an enabled action that leaves the session unchanged.
70
+ - **The compact Map opens readable and stays where the learner put it.** A phone
71
+ now starts at 100% around Home or the selected parser-backed target; **Fit**
72
+ remains the explicit whole-diagram overview. Zoom and scroll survive Map data
73
+ refreshes and layer remounts, while a project lifecycle reset clears them.
74
+ - **Switch project confirms on the first compact-Menu click.** The trigger and
75
+ its confirmation now share one disclosure boundary, and Menu state is cleared
76
+ when the project leaves ready state or the rail crosses to its desktop layout.
77
+ - **Keyboard focus follows every global surface.** Home calibration, the coach,
78
+ Modules, Find, and the Star chart receive focus on entry and return it to the
79
+ invoking control—or the visible compact Menu fallback—when they close.
80
+
8
81
  ## [0.6.2] - 2026-07-21
9
82
 
10
83
  ### Fixed
@@ -168,18 +168,38 @@ Polish, then the coordinated launch (Show HN / X; lit-galaxy GIF as hero).
168
168
  ## Current State **[AGENT-MAINTAINED]**
169
169
 
170
170
  **Current milestone: Phase 1 tester evidence** · Last updated: 2026-07-21 ·
171
- Session note: v0.6.2 is the immediate installed-artifact hotfix for v0.6.1:
172
- the first-run audience modal now portals to the document top layer, so the
173
- closed compact Menu cannot hide the dialog while its modal backdrop blocks a
174
- fresh narrow window. The public PyPI smoke caught the containment defect after
175
- v0.6.1 publication; v0.6.2 supersedes it rather than pretending an immutable
176
- artifact can be replaced. v0.6.1 closes the four verified user-flow gaps from the
177
- 2026-07-21 usability audit: the 320 px shell reserves a usable map/study stage,
178
- Modules and Find render from the Star chart, required Home calibration finishes
179
- before coach marks begin, and parser-owned 3D nodes no longer install drag
180
- controls that can corrupt OrbitControls pointer state. The release is verified
181
- against the real bundled app at desktop and 320 px widths; the milestone does
182
- not advance because issue #13 still requires human tester evidence. The v0.6.0
171
+ Session note: v0.6.4 closes all twenty findings of a fresh end-to-end user-flow
172
+ audit run against the served build on three real projects (Codemble, Golavo,
173
+ FolioOrb) at 1280/375/320 px with a keyboard pass. The headline fix is a
174
+ Correctness Contract one: a missed check printed the parser answer and its
175
+ evidence and then accepted that answer, so a region could light without
176
+ understanding; a miss now returns neither, and both appear only after the
177
+ learner proves it. The Easy default layer gained the reading path it never had
178
+ (**Read the source** on the Map, guidance that says read-before-prove, Escape
179
+ stepping back a level), the checks panel became keyboard-usable (focus handoff
180
+ on open, focus preserved across submits), Enter now opens the arrow-selected
181
+ structure at study level, and the guidance chip is docked into its own strip
182
+ and hidden until the first-run decisions finish. Home calibration is now a
183
+ viewport-sized modal that states its candidate count, groups candidates by
184
+ their real scope, and keeps its escape hatch on screen; the audience question
185
+ is asked once per learner instead of once per project; the star chart is
186
+ reachable from every level and closes with Escape; exits name the layer they
187
+ return to; galaxy plates and the module index use path tails; Find opens on
188
+ Home and the busiest modules; and compact Map controls no longer share touch
189
+ targets (zero overlapping interactive rectangles at 320 and 375). Parser,
190
+ graph, checks, progress, and provider contracts are unchanged except for the
191
+ deliberate withholding of answers on a failed submission. Previously, v0.6.3
192
+ closed the four follow-up findings from the earlier user-flow audit. Easy guidance is now level-aware and never offers an enabled
193
+ no-op; compact Maps open at readable 100% around Home and preserve zoom/pan
194
+ through data refreshes; Switch project confirms on the first compact-Menu click
195
+ without leaking disclosure state across project or breakpoint changes; and Home
196
+ calibration, the coach, Modules, Find, and the Star chart own explicit keyboard
197
+ focus handoffs. Parser, graph, checks, progress, and provider contracts remain
198
+ unchanged. The bundled app is verified across compact and desktop widths; the
199
+ milestone does not advance because issue #13 still requires human tester
200
+ evidence. v0.6.2 was the immediate installed-artifact fix that moved the
201
+ first-run audience modal to the document top layer, and v0.6.1 remains the
202
+ responsive learning-loop release. The v0.6.0
183
203
  architecture-depth pass is complete in five
184
204
  behavior-preserving waves: project selection owns the home-jailed filesystem
185
205
  policy; project activation atomically owns parse-to-live binding and graph/map
@@ -548,6 +568,14 @@ shows lower repeated-commit work without changing derived values.
548
568
  | 2026-07-21 | v0.6.0 deepens five private boundaries without changing the HTTP, graph, check, persistence, or learner-visible contracts: Project Selection, Project Activation, Project Mapping Run, Name Atlas, and Learner Projection | Approved by UD as five behavior-preserving waves in one release PR. The deletion test now holds at each seam, stale activation and mapping responses lose atomically, and dependency-scoped learner projections measured ~0.331 ms → ~0.001 ms per hover commit on a synthetic 1,000-node project while preserving derived outputs |
549
569
  | 2026-07-21 | v0.6.1 treats Modules and Find as global surfaces, sequences first-run decisions as audience → required Home → coach, and makes the 3D parser-owned layout explicitly non-draggable | Approved by UD as implementation of every verified user-flow audit finding. Global commands must never accept hidden state, onboarding must expose one foreground decision at a time, and learners orbit the immutable graph rather than editing its coordinates. The compact shell is a structural breakpoint of the existing Formal Edo interface, not a new visual system |
550
570
  | 2026-07-21 | First-run audience modal portals to `document.body`; the persistent Easy/Expert toggle remains in responsive header chrome | A native modal inside the closed compact Menu entered the top layer but inherited `display:none` from its ancestor, leaving an invisible backdrop that blocked fresh mobile runs. Modal ownership is a document boundary, not header layout. Caught only by the clean public v0.6.1 installed-artifact smoke; PyPI immutability requires v0.6.2 rather than replacing 0.6.1 |
571
+ | 2026-07-21 | Easy guidance actions are derived from level, region, and layer, then executed by `LearnerSession`; the chip renders no button when the next step is already on screen | React must not guess a structure or own navigation truth, and an enabled action that commits the same state is a false promise. The nearest unlit region remains graph-derived; only the honest route to it changes with the learner's current context |
572
+ | 2026-07-21 | Map zoom/pan is renderer-local state keyed by tab and Home, preserved through transient data remounts but cleared with the project lifecycle; compact Maps start at 100% centred on the parser-backed target | Auto-fitting made 56 px boxes as little as 8–18 px tall and re-ran after check-driven map refreshes. Fit is still a valid explicit overview, while session state stays reserved for graph and learning truth |
573
+ | 2026-07-21 | Responsive disclosures and global surfaces own explicit focus handoffs; compact Menu closes on project exit and when crossing to the desktop rail | DOM focus and disclosure visibility are view concerns, but leaving focus on removed or hidden controls makes a successful navigation indistinguishable from a dead action to a keyboard or screen-reader user |
574
+ | 2026-07-21 | A wrong check submission returns no answer, no answer labels and no evidence; all three are returned only once the learner answers correctly | The response printed the parser answer on every miss and the same question then accepted it, so a region could light on an answer the app itself had just displayed — illumination stopped meaning understanding, the same failure class as the 2026-07-19 "every check needs a wrong option" fix. Evidence is withheld with the answer because an importer check cites exactly the files that *are* its answer |
575
+ | 2026-07-21 | The 2D Map gets a reading path: region focus offers **Read the source** beside the checks, Easy guidance recommends reading before proving, and Escape steps back a level there as it does in the Galaxy | Easy mode lands on the Map, where the only action was a quiz about code the layer could not show. The study panel is layer-neutral (`/api/node/:id/study`), so the Map only needed to select the module node the parser already produced — no new truth, and the audience that most needs to read first stops being sent to another layer to do it |
576
+ | 2026-07-21 | The audience answer is stored per learner as well as per project (`learner.json` beside progress); a fresh bind seeds from it and skips the gate, while the header toggle still overrides one project | The gate asks who the *learner* is, but the answer lived only under the project key, so every new project re-asked an expert whether they were new to coding. The file carries no `schema_version`, which is what keeps recents from reading it as a project |
577
+ | 2026-07-21 | Home calibration is a native modal sized to the viewport, grouped by the candidates' real top-level scope, with the candidate count stated and "Explore without Home" outside the scrolling list | It is the second step of the same required sequence as the audience gate and deserves the same shape. As a card capped to a share of the stage it showed one candidate of eleven with the escape hatch thousands of pixels below the fold, and a flat list put `tests/fixtures/...` beside the learner's entrypoint with nothing to tell them apart. Scope and rank are parser facts already in the payload; the leading group always opens so a project whose best candidate is rank 1 is never met by an all-collapsed list |
578
+ | 2026-07-21 | Galaxy name plates use the same path-tail rule as map schema 3's `short_label`; the shared module index and the command palette use it too, and the palette's unfiltered order is Home → lit → centrality | Basenames collide hard in a Python project — every package carries an `__init__.py` — so identical plates named different modules, which is precisely the wrong a learner cannot detect, and the palette opened on a screen of indistinguishable rows |
551
579
 
552
580
  ## Non-Goals — do NOT build (point here when asked)
553
581
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codemble
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: A learning game that turns the code AI wrote for you into a galaxy you light up by understanding it.
5
5
  Project-URL: Homepage, https://udhawan97.github.io/Codemble/
6
6
  Project-URL: Repository, https://github.com/udhawan97/Codemble
@@ -188,6 +188,9 @@ computed in the graph layer and served as data.
188
188
  | **Map · Workflow** | The call tree from your entrypoint, depth by depth | Seeing what runs first |
189
189
 
190
190
  The Map is plain SVG, so it still works on a machine that cannot draw WebGL.
191
+ On a compact screen it opens at readable 100% around Home or the selected
192
+ target; **Fit** is an explicit whole-diagram overview. Zoom and pan survive Map
193
+ refreshes and layer switches instead of snapping back after a passed check.
191
194
 
192
195
  ## Open a structure, read what the parser knows first
193
196
 
@@ -204,8 +207,10 @@ Sections other than the narration never involve a model at all.
204
207
  A header toggle changes how Codemble talks to you and how much it puts on
205
208
  screen: plain language, larger type, the Map by default, and a hint chip naming
206
209
  the nearest unlit region to Home — counted in import hops over the graph, not
207
- chosen by a model. It never changes graph truth, coordinates, progress, or how a
208
- check is scored.
210
+ chosen by a model. The hint opens that system, switches from Map to Galaxy when
211
+ its internal structures are the next step, then becomes an instruction rather
212
+ than an enabled no-op. It never changes graph truth, coordinates, progress, or
213
+ how a check is scored.
209
214
 
210
215
  You can also switch project or change Home without leaving the app.
211
216
 
@@ -156,6 +156,9 @@ computed in the graph layer and served as data.
156
156
  | **Map · Workflow** | The call tree from your entrypoint, depth by depth | Seeing what runs first |
157
157
 
158
158
  The Map is plain SVG, so it still works on a machine that cannot draw WebGL.
159
+ On a compact screen it opens at readable 100% around Home or the selected
160
+ target; **Fit** is an explicit whole-diagram overview. Zoom and pan survive Map
161
+ refreshes and layer switches instead of snapping back after a passed check.
159
162
 
160
163
  ## Open a structure, read what the parser knows first
161
164
 
@@ -172,8 +175,10 @@ Sections other than the narration never involve a model at all.
172
175
  A header toggle changes how Codemble talks to you and how much it puts on
173
176
  screen: plain language, larger type, the Map by default, and a hint chip naming
174
177
  the nearest unlit region to Home — counted in import hops over the graph, not
175
- chosen by a model. It never changes graph truth, coordinates, progress, or how a
176
- check is scored.
178
+ chosen by a model. The hint opens that system, switches from Map to Galaxy when
179
+ its internal structures are the next step, then becomes an instruction rather
180
+ than an enabled no-op. It never changes graph truth, coordinates, progress, or
181
+ how a check is scored.
177
182
 
178
183
  You can also switch project or change Home without leaving the app.
179
184
 
@@ -35,10 +35,16 @@ Then, without reading the docs first:
35
35
  1. Identify Home, or choose it if Codemble asks.
36
36
  2. If the project is mixed, focus one language and return to **All**; report any
37
37
  missing system or relationship.
38
- 3. Enter one system and open one source structure.
38
+ 3. Enter one system and open one source structure. On a narrow window, also use
39
+ **Menu** to open and close **Modules** and **Find**; keyboard focus should
40
+ move into each surface and return to **Menu** when it closes.
39
41
  4. Return to the system, choose **Prove understanding**, and light it.
40
42
  5. Quit Codemble, run the same command again, and confirm the system stays lit.
41
43
 
44
+ On the Map, compact screens should start at readable 100% around Home. **Fit**
45
+ is the whole-diagram overview; after zooming or panning, a passed check should
46
+ not throw you to a different scale or location.
47
+
42
48
  ## Report what happened
43
49
 
44
50
  Use the [early-tester issue form](https://github.com/udhawan97/Codemble/issues/new/choose).
@@ -1,4 +1,4 @@
1
1
  """Codemble — a learning game that turns the code AI wrote for you into a galaxy
2
2
  you light up by understanding it. See CLAUDE.md for the agent brief and roadmap."""
3
3
 
4
- __version__ = "0.6.2"
4
+ __version__ = "0.6.4"
@@ -124,22 +124,28 @@ class CheckService:
124
124
  if complete:
125
125
  self._progress.mark_understood(region_id)
126
126
 
127
- answer_labels = [
128
- option.label for option in check.options if option.id in check.answer_ids
129
- ]
130
- return {
127
+ result: dict[str, object] = {
131
128
  "correct": correct,
132
129
  "check_id": check.id,
133
- "answer_ids": list(check.answer_ids),
134
- "answer_labels": answer_labels,
135
- "evidence": list(check.evidence),
136
130
  "message": (
137
131
  "Correct. That answer is fixed by the parser graph."
138
132
  if correct
139
- else "Not yet. Re-read the graph relationship and try again."
133
+ else "Not yet. Re-read the relationship in your own code and try again."
140
134
  ),
141
135
  "region_understood": complete,
142
136
  }
137
+ if not correct:
138
+ # A miss returns no answer and no citations. An importer check
139
+ # cites the very files that are its answer, so handing either back
140
+ # let the next submission replay what the screen had just shown --
141
+ # and a region lit that way proves nothing.
142
+ return result
143
+ result["answer_ids"] = list(check.answer_ids)
144
+ result["answer_labels"] = [
145
+ option.label for option in check.options if option.id in check.answer_ids
146
+ ]
147
+ result["evidence"] = list(check.evidence)
148
+ return result
143
149
 
144
150
  def _region_checks(self, region_id: str) -> tuple[Check, ...]:
145
151
  if region_id not in self._checks:
@@ -47,10 +47,15 @@ _PYTHON_NOTES = {
47
47
  "expert": "Python calls this specially named method through a language protocol such as length, comparison, or display.",
48
48
  },
49
49
  ),
50
+ # One concept id covers `try`, `try*` and `raise` (python_ast visits all
51
+ # three), so the copy has to be true of a line that *signals* a problem as
52
+ # well as one that catches it. The old easy note taught catching only, and
53
+ # anchored to `raise SystemExit(main())` it claimed roughly the opposite of
54
+ # what that line does.
50
55
  "exception-handling": (
51
56
  "Exception handling",
52
57
  {
53
- "easy": "This plans for something going wrong, so the program can react instead of crashing.",
58
+ "easy": "This treats failure as part of the plan: code like this either raises a problem for whoever called it, or catches one so the program can react instead of crashing.",
54
59
  "expert": "This construct makes failure part of explicit control flow by catching, grouping, or raising an exception.",
55
60
  },
56
61
  ),
@@ -12,6 +12,11 @@ from codemble.adapters.base import Graph
12
12
  from codemble.paths import data_dir
13
13
 
14
14
  _SCHEMA_VERSION = 1
15
+ _MODES = frozenset({"easy", "expert"})
16
+ # The audience answer is about the learner, not the project, so it is also kept
17
+ # once per data directory. Project payloads still win, which is what keeps the
18
+ # header toggle a genuine per-project override.
19
+ _LEARNER_FILE = "learner.json"
15
20
 
16
21
 
17
22
  class UnknownRegionError(KeyError):
@@ -26,6 +31,7 @@ class ProgressStore:
26
31
  self._root = root or data_dir() / "progress"
27
32
  project_key = hashlib.sha256(graph.project_root.encode()).hexdigest()[:20]
28
33
  self.path = self._root / f"{project_key}.json"
34
+ self._learner_path = self._root / _LEARNER_FILE
29
35
  self._signatures = _region_signatures(graph)
30
36
 
31
37
  def understood_regions(self) -> frozenset[str]:
@@ -72,24 +78,55 @@ class ProgressStore:
72
78
  def mode(self) -> str:
73
79
  """Return the learner's audience mode; this never affects progress."""
74
80
 
75
- payload = self._read()
76
- value = payload.get("mode")
77
- return value if value in {"easy", "expert"} else "easy"
81
+ value = self._read().get("mode")
82
+ if value in _MODES:
83
+ return value
84
+ return self._learner_mode() or "easy"
78
85
 
79
86
  def mode_chosen(self) -> bool:
80
- """Return whether the learner has explicitly chosen an audience mode."""
87
+ """Return whether this learner has answered the audience question.
81
88
 
82
- payload = self._read()
83
- return payload.get("mode") in {"easy", "expert"}
89
+ Per project first, then the learner-level answer. The gate asks who
90
+ the *learner* is, so re-asking it on every project would be asking a
91
+ question they have already answered; the header toggle still overrides
92
+ any single project.
93
+ """
94
+
95
+ return self._read().get("mode") in _MODES or self._learner_mode() is not None
84
96
 
85
97
  def set_mode(self, mode: str) -> None:
86
98
  """Persist the audience mode beside progress without touching signatures."""
87
99
 
88
- if mode not in {"easy", "expert"}:
100
+ if mode not in _MODES:
89
101
  raise ValueError("Mode must be 'easy' or 'expert'.")
90
102
  payload = self._read()
91
103
  payload["mode"] = mode
92
104
  self._write(payload)
105
+ self._write_learner_mode(mode)
106
+
107
+ def _learner_mode(self) -> str | None:
108
+ """Read the last audience answered on any project in this data dir."""
109
+
110
+ try:
111
+ payload = json.loads(self._learner_path.read_text(encoding="utf-8"))
112
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError):
113
+ return None
114
+ if not isinstance(payload, dict):
115
+ return None
116
+ value = payload.get("mode")
117
+ return value if value in _MODES else None
118
+
119
+ def _write_learner_mode(self, mode: str) -> None:
120
+ self._root.mkdir(parents=True, exist_ok=True)
121
+ temporary = self._learner_path.with_suffix(f".{os.getpid()}.tmp")
122
+ try:
123
+ temporary.write_text(
124
+ json.dumps({"mode": mode}, indent=2, sort_keys=True) + "\n",
125
+ encoding="utf-8",
126
+ )
127
+ temporary.replace(self._learner_path)
128
+ finally:
129
+ temporary.unlink(missing_ok=True)
93
130
 
94
131
  def selected_entrypoint(self) -> str | None:
95
132
  """Return the learner's persisted Home choice, if one was stored.
@@ -69,7 +69,7 @@ class ProjectSelector:
69
69
  def browse(self, path: str | Path | None = None) -> FolderListing:
70
70
  """List non-hidden child directories for one allowed folder."""
71
71
 
72
- resolved = self.resolve(path if path is not None else self._root)
72
+ resolved = self.resolve(path if path is not None else self._default_start())
73
73
  if not resolved.is_dir():
74
74
  raise ProjectFolderMissing("That folder does not exist.")
75
75
  try:
@@ -97,6 +97,21 @@ class ProjectSelector:
97
97
  if self._is_inside_root(Path(str(entry["project_root"])).resolve())
98
98
  )
99
99
 
100
+ def _default_start(self) -> Path:
101
+ """Open browsing beside the newest remembered project, else at the root.
102
+
103
+ Projects tend to live as siblings, so starting at the browse root made
104
+ every switch re-walk the same folders. Derived from recents rather than
105
+ stored, so it cannot drift, and it stays inside the same jail because
106
+ ``recents`` already filtered by it.
107
+ """
108
+
109
+ for entry in self.recents():
110
+ parent = Path(str(entry["project_root"])).parent
111
+ if self._is_inside_root(parent) and parent.is_dir():
112
+ return parent
113
+ return self._root
114
+
100
115
  def _require_inside_root(self, path: Path) -> None:
101
116
  if not self._is_inside_root(path):
102
117
  raise ProjectFolderForbidden(
@@ -0,0 +1 @@
1
+ @font-face{font-family:Zen Kaku Gothic New;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/zen-kaku-gothic-new-latin-400-normal-BEdayliK.woff2)format("woff2"),url(/assets/zen-kaku-gothic-new-latin-400-normal-CPSmNJAU.woff)format("woff")}@font-face{font-family:Shippori Mincho;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/shippori-mincho-latin-500-normal-XI1O8euf.woff2)format("woff2"),url(/assets/shippori-mincho-latin-500-normal-C-QwvIb3.woff)format("woff")}@font-face{font-family:Shippori Mincho;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/shippori-mincho-latin-700-normal-DHcmzUO5.woff2)format("woff2"),url(/assets/shippori-mincho-latin-700-normal-CkoCYOiI.woff)format("woff")}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff)format("woff")}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff)format("woff")}:root{--cm-font-display:"Shippori Mincho", "Hiragino Mincho ProN", "Yu Mincho", serif;--cm-font-body:"Zen Kaku Gothic New", "Hiragino Sans", system-ui, sans-serif;--cm-font-mono:"JetBrains Mono", ui-monospace, monospace;--cm-text-xs:.75rem;--cm-text-sm:.875rem;--cm-text-base:1rem;--cm-text-lg:clamp(1.0625rem, .2vw + 1rem, 1.1875rem);--cm-text-xl:clamp(1.25rem, .5vw + 1.1rem, 1.5rem);--cm-text-2xl:clamp(1.5rem, 1vw + 1.25rem, 2rem);--cm-text-3xl:clamp(1.75rem, 2vw + 1.25rem, 3rem);--cm-text-display:clamp(2.25rem, 4.4vw + .9rem, 4.75rem);--cm-space-2xs:.25rem;--cm-space-xs:.5rem;--cm-space-sm:.75rem;--cm-space-md:1rem;--cm-space-lg:2rem;--cm-space-xl:4rem;--cm-space-2xl:6rem;--cm-space-3xl:9rem;--cm-radius:3px;--cm-radius-lg:6px;--cm-rule:1px;--cm-ease:cubic-bezier(.16, .7, .3, 1);--cm-dur-quick:.16s;--cm-dur-reveal:.42s;--cm-dur-slow:.9s;--cm-z-canvas:0;--cm-z-content:1;--cm-z-rail:40;--cm-z-nav:50}:root[data-theme=dark],:root{--cm-ground:#070b1c;--cm-ground-2:#101a3e;--cm-ground-3:#1a2750;--cm-hairline:#24325c;--cm-hairline-soft:#17203f;--cm-ink:#eef2fa;--cm-ink-2:#9aa8c4;--cm-ink-3:#7d8aa8;--cm-star:#e89b2e;--cm-star-high:#f4c46a;--cm-star-low:#3a2a0c;--cm-star-tint:#e89b2e24;--cm-orbit:#82abec;--cm-orbit-fill:#3f6ac0;--cm-orbit-deep:#2b4d96;--cm-orbit-low:#14264a;--cm-on-fill:#f7faff;--cm-sky-far:#070b1c;--cm-sky-near:#16224a}:root[data-theme=light]{--cm-ground:#faf7f0;--cm-ground-2:#fff;--cm-ground-3:#f2eee4;--cm-hairline:#e6e2d6;--cm-hairline-soft:#efece3;--cm-ink:#101a3e;--cm-ink-2:#3c4a74;--cm-ink-3:#5c688c;--cm-star:#7d4a06;--cm-star-high:#a86408;--cm-star-low:#fff3d6;--cm-star-tint:#7d4a061a;--cm-orbit:#2b4d96;--cm-orbit-fill:#2b4d96;--cm-orbit-deep:#1e3a6e;--cm-orbit-low:#e3ecfb;--cm-on-fill:#faf7f0;--cm-sky-far:#e9edf7;--cm-sky-near:#fff}:root{--cm-route-possible:color-mix(in srgb, var(--cm-ink-2) 30%, var(--cm-ink-3));--cm-node-unlit:color-mix(in srgb, var(--cm-ink-3) 78%, var(--cm-ground));--cm-neb-python:#599079;--cm-neb-js:#8b7ea9;--cm-neb-ts:#568baa;--cm-star-halo:#f2efe9;--cm-label-plate:#090e1cd1;--cm-label-ink:#d6dbe7;--cm-sidebar-width:min(19rem, 82vw);--cm-scrim:color-mix(in srgb, var(--cm-ground) 82%, transparent);--cm-error:oklch(72% .16 28);--cm-space-2xs:.125rem;--cm-space-2xl:6rem;--cm-radius:.25rem;--cm-text-xs:.75rem;--cm-text-sm:.875rem;--cm-text-base:1rem;--cm-text-lg:1.25rem;--cm-text-display:clamp(1.75rem, 3vw + 1rem, 3.25rem);--cm-z-base:1;--cm-z-raised:10;--cm-z-sticky:200;--cm-z-panel:400}*{box-sizing:border-box}html,body,#root{width:100%;min-height:100%;margin:0;overflow-x:clip}body{background:var(--cm-ground);color:var(--cm-ink);font-family:var(--cm-font-body);font-size:var(--cm-text-base);line-height:1.55}button,output{font:inherit}button:focus-visible,[href]:focus-visible,[tabindex]:focus-visible,input:focus-visible{outline:2px solid var(--cm-orbit);outline-offset:3px}.app-shell{background:var(--cm-ground);width:100%;height:100dvh;min-height:100dvh;color:var(--cm-ink);grid-template-rows:auto minmax(0,1fr) auto auto;grid-template-columns:minmax(0,1fr);display:grid;position:relative;overflow:hidden}.instrument-rail{z-index:var(--cm-z-sticky);align-items:center;gap:var(--cm-space-sm) var(--cm-space-md);min-width:0;max-width:100%;padding:var(--cm-space-sm) max(var(--cm-space-md), env(safe-area-inset-right)) var(--cm-space-sm) max(var(--cm-space-md), env(safe-area-inset-left));border-block-end:1px solid var(--cm-hairline);background:var(--cm-ground);grid-row:1;grid-template-columns:minmax(0,1fr) auto;display:grid;position:relative}.brand-lockup{align-items:center;gap:var(--cm-space-sm);min-width:0;display:flex}.brand-lockup div{min-width:0;display:grid}.brand-lockup strong{font-family:var(--cm-font-display);letter-spacing:-.02em;font-weight:700}.brand-lockup span:not(.brand-mark),.home-readout,.location{color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.brand-mark{width:var(--cm-space-md);height:var(--cm-space-md);border:1px solid var(--cm-ink-2);background:var(--cm-ink);border-radius:50%}.location{grid-area:2/1/auto/-1;margin:0}.rail-overflow{grid-area:1/2;justify-self:end}.mobile-menu-trigger{min-width:44px;min-height:44px;padding-inline:var(--cm-space-md);border:1px solid var(--cm-orbit);border-radius:var(--cm-radius);color:var(--cm-orbit);cursor:pointer;place-items:center;font-weight:600;display:grid}.rail-overflow__panel{z-index:var(--cm-z-panel);gap:var(--cm-space-md);max-height:calc(100dvh - 5rem);padding:var(--cm-space-md);border-block:1px solid var(--cm-hairline);background:var(--cm-ground-2);display:none;position:absolute;inset-block-start:100%;inset-inline:0;overflow-y:auto;box-shadow:0 1rem 2.5rem #00000061}.rail-overflow[data-open] .rail-overflow__panel{display:grid}.instrument-rail:has(.rail-overflow[data-open]){z-index:var(--cm-z-panel)}.rail-overflow__panel .rail-actions,.rail-overflow__panel .rail-controls{grid-area:auto;justify-content:start}.rail-action{min-height:44px;padding-inline:var(--cm-space-md);border:1px solid var(--cm-orbit);border-radius:var(--cm-radius);background:var(--cm-ground);color:var(--cm-orbit);cursor:pointer;white-space:nowrap;transition:background-color var(--cm-dur-quick) var(--cm-ease), color var(--cm-dur-quick) var(--cm-ease), transform .1s var(--cm-ease);font-weight:600}@media (hover:hover) and (pointer:fine){.rail-action:hover{background:var(--cm-orbit-low)}}.rail-action:active{transform:translateY(1px)}.rail-action:disabled,.rail-action[aria-disabled=true]{opacity:.5;cursor:not-allowed}.rail-action[data-state=loading]{color:var(--cm-ink-2)}.rail-action[data-state=error]{border-color:var(--cm-error)}.rail-action[data-state=success]{border-color:var(--cm-star);color:var(--cm-star)}.rail-actions{justify-content:end;align-items:center;gap:var(--cm-space-sm);flex-wrap:wrap;grid-area:2/1/auto/-1;min-width:0;display:flex}.rail-controls{align-items:center;gap:var(--cm-space-md);flex-wrap:wrap;grid-area:4/1/auto/-1;min-width:0;display:flex}.switch-project{display:contents}.switch-project[data-confirming]{gap:var(--cm-space-xs);padding:var(--cm-space-sm);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground-2);display:grid}.switch-project[data-confirming] p{max-width:26ch;color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);margin:0}.switch-project__error{color:var(--cm-error)!important}.switch-project[data-confirming]>div{gap:var(--cm-space-xs);display:flex}.language-focus{align-items:center;gap:var(--cm-space-sm);min-width:0;color:var(--cm-ink-3);scrollbar-width:none;display:flex;overflow-x:auto}.language-focus::-webkit-scrollbar{display:none}.language-focus__label{font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);text-transform:uppercase;letter-spacing:.08em;flex:none}.language-focus>div{border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);min-width:max-content;display:flex}.language-focus button{justify-content:center;align-items:center;gap:var(--cm-space-xs);min-width:44px;min-height:44px;padding-inline:var(--cm-space-sm);border:0;border-inline-end:1px solid var(--cm-hairline);color:var(--cm-ink-2);cursor:pointer;font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);white-space:nowrap;background:0 0;display:inline-flex}.language-focus button:last-child{border-inline-end:0}.language-focus button[aria-pressed=true]{background:var(--cm-orbit-low);color:var(--cm-orbit)}.language-focus button small{color:var(--cm-ink-3);font-size:inherit;font-variant-numeric:tabular-nums}@media (hover:hover) and (pointer:fine){.language-focus button:hover{background:var(--cm-ground-2);color:var(--cm-ink)}.language-focus button[aria-pressed=true]:hover{background:var(--cm-orbit-low);color:var(--cm-orbit)}}.mode-toggle{align-items:center;gap:var(--cm-space-sm);border:0;grid-area:4/1/auto/-1;min-width:0;margin:0;padding:0;display:flex}.mode-toggle__label{color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);text-transform:uppercase;letter-spacing:.08em;flex:none}.mode-toggle__options{border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);min-width:max-content;display:flex}.mode-toggle__options label{align-items:center;gap:var(--cm-space-2xs);min-width:44px;min-height:44px;padding-inline:var(--cm-space-sm);border-inline-end:1px solid var(--cm-hairline);color:var(--cm-ink-2);cursor:pointer;font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);display:inline-flex}.mode-toggle__options label:last-child{border-inline-end:0}.mode-toggle__options label:has(input:checked){background:var(--cm-orbit-low);color:var(--cm-orbit)}.mode-toggle__options input{width:1rem;height:1rem;accent-color:var(--cm-orbit);margin:0}@media (hover:hover) and (pointer:fine){.mode-toggle__options label:hover{background:var(--cm-ground-2);color:var(--cm-ink)}.mode-toggle__options label:has(input:checked):hover{background:var(--cm-orbit-low);color:var(--cm-orbit)}}.mode-gate{width:min(26rem, calc(100% - var(--cm-space-xl)));max-height:min(86%,30rem);padding:var(--cm-space-lg);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground-2);color:var(--cm-ink);animation:panel-enter var(--cm-dur-reveal) var(--cm-ease) both;margin:auto;overflow:auto}.mode-gate::backdrop{background:var(--cm-scrim)}.mode-gate h1{overflow-wrap:anywhere;font-family:var(--cm-font-display);font-size:var(--cm-text-xl);margin:0;font-style:normal;line-height:1.15}.mode-gate p{max-width:42ch;margin:var(--cm-space-sm) 0 0;color:var(--cm-ink-2)}.mode-gate__options{border-block-start:1px solid var(--cm-hairline);margin-block-start:var(--cm-space-lg);display:grid}.mode-gate__options button{min-height:52px;padding:var(--cm-space-sm) 0;border:0;border-block-end:1px solid var(--cm-hairline);color:var(--cm-ink);cursor:pointer;font:inherit;text-align:start;background:0 0;font-weight:600}.mode-gate__options button:last-child{border-block-end:0}@media (hover:hover) and (pointer:fine){.mode-gate__options button:hover{color:var(--cm-orbit)}}.map-stage{background:var(--cm-ground);grid-row:2;min-width:0;max-width:100%;min-height:0;position:relative;overflow:clip}.chart-stage{background:var(--cm-ground);grid-row:2;min-width:0;min-height:0;position:relative;overflow:hidden}.chart-stage .star-chart-screen{height:100%}.galaxy-frame,.galaxy-canvas{z-index:var(--cm-z-base);position:absolute;inset:0}.galaxy-frame{overflow:hidden}.galaxy-canvas canvas{display:block}.keyboard-focus{max-width:min(34rem, calc(100% - var(--cm-space-lg)));padding:var(--cm-space-sm) var(--cm-space-md);border-inline-start:2px solid var(--cm-orbit);background:var(--cm-ground-2);color:var(--cm-ink);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);position:absolute;inset-block-end:var(--cm-space-md);inset-inline-start:var(--cm-space-md)}.orientation-bar{z-index:var(--cm-z-raised);gap:var(--cm-space-sm);color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);pointer-events:none;flex-wrap:wrap;margin:0;display:flex;position:absolute;inset-block-start:var(--cm-space-md);inset-inline-start:max(var(--cm-space-md), env(safe-area-inset-left))}.orientation-bar>*+:before{content:"· ";color:var(--cm-hairline)}.orientation-bar__charted{color:var(--cm-ink-2)}.index-sidebar{z-index:var(--cm-z-panel);width:var(--cm-sidebar-width);border-inline-end:1px solid var(--cm-hairline);background:var(--cm-ground-2);flex-direction:column;display:flex;position:absolute;inset-block:0;inset-inline-start:0}@media (width>=60rem){:is(.map-stage:has(.index-sidebar) .galaxy-frame,.map-stage:has(.index-sidebar) .map-view){inset-inline-start:var(--cm-sidebar-width)}:is(.map-stage:has(.index-sidebar) .orientation-bar,.map-stage:has(.index-sidebar) .orientation-copy){inset-inline-start:calc(var(--cm-sidebar-width) + var(--cm-space-md))}}.index-sidebar>header{justify-content:space-between;align-items:center;gap:var(--cm-space-sm);padding:var(--cm-space-sm) var(--cm-space-md);border-block-end:1px solid var(--cm-hairline);display:flex}.index-sidebar h2{color:var(--cm-ink);font-size:var(--cm-text-sm);letter-spacing:.08em;text-transform:uppercase;margin:0}.index-sidebar>header button{border-radius:var(--cm-radius);min-width:32px;min-height:32px;color:var(--cm-ink-2);font-size:var(--cm-text-lg);cursor:pointer;background:0 0;border:1px solid #0000;line-height:1}.index-sidebar>header button:hover,.index-sidebar>header button:focus-visible{border-color:var(--cm-orbit);color:var(--cm-ink)}.index-sidebar__scroll{flex:1;padding-block-end:var(--cm-space-lg);overflow-y:auto}.index-sidebar section{padding-block-start:var(--cm-space-sm)}.index-sidebar h3{padding:var(--cm-space-2xs) var(--cm-space-md);color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);text-overflow:ellipsis;white-space:nowrap;margin:0;font-weight:400;overflow:hidden}.index-sidebar ul{margin:0;padding:0;list-style:none}.index-sidebar li button{align-items:baseline;gap:var(--cm-space-sm);width:100%;min-height:32px;padding:var(--cm-space-2xs) var(--cm-space-md);color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);text-align:start;cursor:pointer;background:0 0;border:0;border-inline-start:2px solid #0000;display:flex}.index-sidebar li button span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.index-sidebar li button:hover,.index-sidebar li button:focus-visible{border-inline-start-color:var(--cm-orbit);background:var(--cm-ground);color:var(--cm-ink)}.index-sidebar li button[aria-current=true]{border-inline-start-color:var(--cm-orbit);color:var(--cm-ink)}.index-sidebar li button[data-lit] span{color:var(--cm-star-high)}.index-sidebar li em{color:var(--cm-ink-3);font-size:var(--cm-text-xs);font-style:normal}.module-finder{border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground-2);width:min(34rem,92vw);max-height:70vh;color:var(--cm-ink);padding:0}.module-finder::backdrop{background:var(--cm-scrim)}.module-finder input{width:100%;padding:var(--cm-space-md);border:0;border-block-end:1px solid var(--cm-hairline);color:var(--cm-ink);font-family:var(--cm-font-mono);font-size:var(--cm-text-base);background:0 0}.module-finder input:focus-visible{outline:2px solid var(--cm-orbit);outline-offset:-2px}.module-finder ul{max-height:50vh;padding:var(--cm-space-2xs) 0;margin:0;list-style:none;overflow-y:auto}.module-finder li button{align-items:baseline;gap:var(--cm-space-sm);width:100%;min-height:36px;padding:var(--cm-space-2xs) var(--cm-space-md);color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);text-align:start;cursor:pointer;background:0 0;border:0;border-inline-start:2px solid #0000;display:flex}.module-finder li button[data-active]{border-inline-start-color:var(--cm-orbit);background:var(--cm-ground);color:var(--cm-ink)}.module-finder li strong{font-weight:400}.module-finder li small{color:var(--cm-ink-3);text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.module-finder li em{color:var(--cm-star-high);font-style:normal}.module-finder__empty{padding:var(--cm-space-md);color:var(--cm-ink-3);font-size:var(--cm-text-sm);margin:0}.rail-action kbd{color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);margin-inline-start:var(--cm-space-2xs)}.rail-action--toggle[aria-pressed=true]{border-color:var(--cm-orbit);color:var(--cm-orbit)}.orientation-copy{z-index:var(--cm-z-raised);max-width:min(28rem, calc(100% - var(--cm-space-lg)));pointer-events:none;position:absolute;inset-block-start:clamp(var(--cm-space-lg), 8vh, var(--cm-space-2xl));inset-inline-start:max(var(--cm-space-md), env(safe-area-inset-left))}.orientation-copy--system{inset-block-start:var(--cm-space-lg)}.orientation-copy h1,.study-preview h1,.load-state h1,.webgl-error h1{overflow-wrap:anywhere;min-width:0;font-family:var(--cm-font-display);font-size:var(--cm-text-display);letter-spacing:-.035em;margin:0;font-style:normal;font-weight:700;line-height:1.08}.orientation-copy p,.study-preview p{max-width:55ch;margin:var(--cm-space-sm) 0 0;color:var(--cm-ink-2)}.orientation-copy--inline{z-index:auto;pointer-events:auto;flex:none;max-block-size:45%;position:static;overflow-y:auto}.orientation-copy--inline h1{font-size:var(--cm-text-lg)}.orientation-copy--inline p{font-size:var(--cm-text-sm);margin-block-start:var(--cm-space-2xs)}.orientation-copy--inline .check-launch{margin-block-start:var(--cm-space-sm)}.orientation-copy__actions{gap:var(--cm-space-sm);flex-wrap:wrap;align-items:center;display:flex}.check-launch--read{border-color:var(--cm-hairline);color:var(--cm-ink)}.check-launch--read:hover,.check-launch--read:focus-visible{border-color:var(--cm-orbit);color:var(--cm-orbit)}.check-launch,.check-primary,.check-close{border:1px solid var(--cm-orbit);border-radius:var(--cm-radius);background:var(--cm-ground);min-height:44px;color:var(--cm-orbit);cursor:pointer;transition:background-color var(--cm-dur-quick) var(--cm-ease), color var(--cm-dur-quick) var(--cm-ease), transform .1s var(--cm-ease);font-weight:600}.check-launch{padding-inline:var(--cm-space-md);pointer-events:auto;margin-block-start:var(--cm-space-md);position:relative}.check-primary{padding-inline:var(--cm-space-lg);justify-self:start}.check-close{padding-inline:var(--cm-space-md);border-color:var(--cm-hairline);color:var(--cm-ink-2)}@media (hover:hover) and (pointer:fine){.check-launch:hover,.check-primary:hover,.check-close:hover{background:var(--cm-orbit-low);color:var(--cm-ink)}}.check-launch:active,.check-primary:active,.check-close:active{transform:translateY(1px)}.check-primary:disabled{opacity:.45;cursor:not-allowed;transform:none}.legend-toggle{z-index:var(--cm-z-sticky);min-height:32px;padding:var(--cm-space-2xs) var(--cm-space-sm);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground-2);color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);cursor:pointer;position:absolute;inset-block-start:var(--cm-space-md);inset-inline-end:var(--cm-space-md)}.legend-toggle:hover,.legend-toggle:focus-visible{border-color:var(--cm-orbit);color:var(--cm-ink)}.legend-toggle[aria-expanded=true]{border-color:var(--cm-orbit);color:var(--cm-orbit)}.map-legend{z-index:var(--cm-z-raised);gap:var(--cm-space-2xs) var(--cm-space-md);max-height:min(60vh,30rem);padding:var(--cm-space-sm) var(--cm-space-md);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground-2);color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);grid-template-columns:repeat(2,auto);display:none;position:absolute;inset-block-start:calc(var(--cm-space-md) + 40px);inset-inline-end:var(--cm-space-md);overflow-y:auto}.map-legend span{align-items:center;gap:var(--cm-space-sm);white-space:nowrap;display:inline-flex}.legend-dot{width:var(--cm-space-sm);height:var(--cm-space-sm);border-radius:50%;display:inline-block}.legend-dot--dim{background:var(--cm-ink-3)}.legend-dot--lit{background:var(--cm-star-high)}.legend-dot--partial{background:var(--cm-route-possible)}.legend-route{width:var(--cm-space-md);border-block-start:1px solid var(--cm-ink-3)}.legend-route--possible{border-block-start-color:var(--cm-route-possible)}.partial-summary{border-inline-start:2px solid var(--cm-route-possible);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);padding-inline-start:var(--cm-space-sm);color:var(--cm-ink)!important}.entrypoint-picker{z-index:var(--cm-z-panel);width:min(36rem, calc(100vw - var(--cm-space-xl)));max-height:min(80dvh,42rem);padding:var(--cm-space-lg);border:1px solid var(--cm-hairline);background:var(--cm-ground-2);color:var(--cm-ink);animation:entrypoint-enter var(--cm-dur-reveal) var(--cm-ease) both}.entrypoint-picker[open]{flex-direction:column;display:flex;overflow:hidden}.entrypoint-picker::backdrop{background:color-mix(in srgb, var(--cm-ground) 88%, transparent)}.entrypoint-picker>p:first-child{color:var(--cm-orbit);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);margin:0}.entrypoint-picker h1{margin:var(--cm-space-sm) 0 0;overflow-wrap:anywhere;font-family:var(--cm-font-display);font-size:var(--cm-text-xl);line-height:1.1}.entrypoint-picker>p:not(:first-child){margin:var(--cm-space-md) 0 0;color:var(--cm-ink-2)}.entrypoint-count{margin-block:var(--cm-space-md) 0;color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.entrypoint-scroll{min-height:0;border-block-start:1px solid var(--cm-hairline);flex:auto;margin-block-start:var(--cm-space-sm);overflow-y:auto}.entrypoint-scroll details{border-block-end:1px solid var(--cm-hairline)}.entrypoint-scroll summary{justify-content:space-between;align-items:baseline;gap:var(--cm-space-sm);padding:var(--cm-space-sm) 0;color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);cursor:pointer;display:flex}.entrypoint-scroll summary small{color:var(--cm-ink-3)}.entrypoint-candidates{border-block-start:1px solid var(--cm-hairline)}.entrypoint-candidates button{gap:var(--cm-space-xs);width:100%;min-height:58px;padding:var(--cm-space-sm) 0;border:0;border-block-end:1px solid var(--cm-hairline);color:var(--cm-ink);cursor:pointer;text-align:start;background:0 0;display:grid}.entrypoint-candidates button span{overflow-wrap:anywhere;font-family:var(--cm-font-mono)}.entrypoint-candidates button small{color:var(--cm-ink-3);font-family:var(--cm-font-mono)}@media (hover:hover) and (pointer:fine){.entrypoint-candidates button:hover{color:var(--cm-orbit)}}.entrypoint-continue{width:100%;min-height:44px;padding:var(--cm-space-xs) 0 0;border:0;border-block-start:1px solid var(--cm-hairline);color:var(--cm-ink-2);cursor:pointer;text-underline-offset:3px;background:0 0;flex:none;margin-block-start:var(--cm-space-md);text-decoration:underline}.entrypoint-error{color:var(--cm-error)!important}.illumination-pulse{z-index:var(--cm-z-raised);color:var(--cm-star);pointer-events:none;animation:illumination-pulse .42s var(--cm-ease) both;justify-items:center;display:grid;position:absolute;inset-block-start:50%;inset-inline-start:25%;transform:translate(-50%,-50%)}.illumination-pulse span{filter:drop-shadow(0 0 18px var(--cm-star));font-size:clamp(3rem,8vw,6rem);line-height:1}.illumination-pulse strong{color:var(--cm-ink);font-family:var(--cm-font-display)}.structural-summary p{max-width:62ch;margin:var(--cm-space-sm) 0 0;color:var(--cm-ink)}.mini-constellation{width:100%;max-width:22rem;height:auto;margin-block-start:var(--cm-space-md)}.mini-constellation line{stroke:var(--cm-hairline);stroke-width:1px}.mini-constellation circle{fill:var(--cm-ink-3)}.mini-constellation__self{fill:var(--cm-orbit)}.connections h3{margin:var(--cm-space-lg) 0 0;color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);text-transform:uppercase;letter-spacing:.08em;font-weight:500}.connection-list{margin:var(--cm-space-xs) 0 0;border-block-start:1px solid var(--cm-hairline);padding:0;list-style:none}.connection-list button{gap:var(--cm-space-2xs);width:100%;min-height:52px;padding:var(--cm-space-sm) 0;border:0;border-block-end:1px solid var(--cm-hairline);color:var(--cm-ink);cursor:pointer;text-align:start;background:0 0;display:grid}.connection-name{overflow-wrap:anywhere;font-family:var(--cm-font-mono);font-size:var(--cm-text-sm)}.connection-meta{color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}@media (hover:hover) and (pointer:fine){.connection-list button:hover .connection-name{color:var(--cm-orbit)}}.load-state .check-primary,.study-notice .check-primary{justify-self:start;margin-block-start:var(--cm-space-md)}.study-preview{z-index:var(--cm-z-panel);width:100%;min-width:0;max-width:100%;max-height:72%;padding:var(--cm-space-lg) max(var(--cm-space-md), env(safe-area-inset-right)) max(var(--cm-space-lg), env(safe-area-inset-bottom)) max(var(--cm-space-md), env(safe-area-inset-left));border-block-start:1px solid var(--cm-hairline);background:var(--cm-ground-2);color:var(--cm-ink);animation:panel-enter var(--cm-dur-reveal) var(--cm-ease) both;position:absolute;inset-block-end:0;inset-inline:0;overflow:auto}.check-panel{z-index:var(--cm-z-panel);width:100%;min-width:0;max-width:100%;max-height:76%;padding:var(--cm-space-lg) max(var(--cm-space-md), env(safe-area-inset-right)) max(var(--cm-space-lg), env(safe-area-inset-bottom)) max(var(--cm-space-md), env(safe-area-inset-left));border-block-start:1px solid var(--cm-hairline);background:var(--cm-ground-2);color:var(--cm-ink);animation:panel-enter var(--cm-dur-reveal) var(--cm-ease) both;position:absolute;inset-block-end:0;inset-inline:0;overflow:auto}.check-panel__header{justify-content:space-between;align-items:start;gap:var(--cm-space-md);border-block-end:1px solid var(--cm-hairline);padding-block-end:var(--cm-space-lg);display:flex}.check-panel__header p,.check-panel__header h1{margin:0}.check-panel__header p,.check-progress span{color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.check-panel__header h1{overflow-wrap:anywhere;font-family:var(--cm-font-display);font-size:var(--cm-text-xl);margin-block-start:var(--cm-space-xs);line-height:1.1}.check-loading,.check-state,.check-complete,.active-check{margin-block-start:var(--cm-space-lg)}.check-loading,.check-state p,.check-complete p,.active-check fieldset>p{max-width:60ch;color:var(--cm-ink-2)}.check-state h2,.check-complete h2{font-family:var(--cm-font-display);font-size:var(--cm-text-lg);margin:0}.check-state p,.check-complete p{margin:var(--cm-space-sm) 0 0}.check-complete{gap:var(--cm-space-sm);display:grid}.check-complete__star{color:var(--cm-star);font-size:var(--cm-text-display);line-height:1}.check-progress{align-items:center;gap:var(--cm-space-md);grid-template-columns:auto minmax(5rem,1fr);margin-block-end:var(--cm-space-xl);display:grid}.check-progress progress{background:var(--cm-hairline);width:100%;height:2px;color:var(--cm-star);border:0}.check-progress progress::-webkit-progress-bar{background:var(--cm-hairline)}.check-progress progress::-webkit-progress-value{background:var(--cm-star)}.check-progress progress::-moz-progress-bar{background:var(--cm-star)}.active-check{gap:var(--cm-space-lg);display:grid}.active-check fieldset{border:0;min-width:0;margin:0;padding:0}.active-check legend{overflow-wrap:anywhere;max-width:28ch;font-family:var(--cm-font-display);font-size:var(--cm-text-xl);padding:0;font-weight:700;line-height:1.15}.active-check fieldset>p{margin:var(--cm-space-sm) 0 0}.check-options{border-block-start:1px solid var(--cm-hairline);margin-block-start:var(--cm-space-lg)}.check-options label{align-items:center;gap:var(--cm-space-md);min-height:52px;padding-block:var(--cm-space-sm);border-block-end:1px solid var(--cm-hairline);color:var(--cm-ink-2);cursor:pointer;grid-template-columns:auto minmax(0,1fr);display:grid}.check-options label:has(input:checked){color:var(--cm-ink)}.check-options input{width:1rem;height:1rem;accent-color:var(--cm-star);margin:0}.check-options span{overflow-wrap:anywhere;font-family:var(--cm-font-mono);font-size:var(--cm-text-sm)}.check-feedback{gap:var(--cm-space-xs);padding:var(--cm-space-md);border-inline-start:2px solid var(--cm-error);background:var(--cm-ground);color:var(--cm-ink-2);display:grid}.check-feedback strong{color:var(--cm-ink)}.check-feedback span{overflow-wrap:anywhere;font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.check-affirmation{align-items:baseline;gap:var(--cm-space-xs);margin:0 0 var(--cm-space-lg);border-inline-start:2px solid var(--cm-star);color:var(--cm-star-high);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);padding-inline-start:var(--cm-space-sm);display:flex}.study-preview__path{font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);color:var(--cm-orbit)!important}.study-preview__header{display:block}.study-preview dl{gap:var(--cm-space-md);margin:var(--cm-space-lg) 0 0;grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.study-preview dl div{border-block-start:1px solid var(--cm-hairline);padding-block-start:var(--cm-space-sm)}.study-preview dt{color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.study-preview dd{margin:var(--cm-space-2xs) 0 0;font-variant-numeric:tabular-nums}.study-content{gap:var(--cm-space-xl);min-width:0;margin-block-start:var(--cm-space-xl);display:grid}.study-content>*{min-width:0}.study-section-heading{justify-content:space-between;align-items:center;gap:var(--cm-space-md);border-block-end:1px solid var(--cm-hairline);padding-block-end:var(--cm-space-sm);display:flex}.study-section-heading h2,.study-notice h2{overflow-wrap:anywhere;min-width:0;font-family:var(--cm-font-display);font-size:var(--cm-text-lg);margin:0;font-style:normal;line-height:1.15}.study-section-heading span{color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);white-space:nowrap}.source-code{width:100%;min-width:0;max-height:22rem;margin:var(--cm-space-md) 0 0;padding:var(--cm-space-sm) 0;border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground);color:var(--cm-ink);counter-reset:source-line;font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);tab-size:4;line-height:1.65;list-style:none;overflow:auto}.source-code li{min-width:max-content;padding-inline:var(--cm-space-sm) var(--cm-space-md);grid-template-columns:3.25rem minmax(0,1fr);display:grid}.source-code li:before{content:attr(data-line);color:var(--cm-ink-3);text-align:end;-webkit-user-select:none;user-select:none;padding-inline-end:var(--cm-space-md)}.source-code li:target{background:var(--cm-orbit-low)}.source-code code{font:inherit;white-space:pre}.grounded-explanation>p,.study-notice p,.study-loading,.evidence-list p{max-width:65ch;color:var(--cm-ink-2)}.grounded-explanation>p,.study-notice p,.study-loading{margin:var(--cm-space-md) 0 0}.grounded-explanation h3{margin:var(--cm-space-lg) 0 var(--cm-space-sm);font-family:var(--cm-font-display);font-size:var(--cm-text-base);font-style:normal}.source-citation{color:var(--cm-orbit);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);text-underline-offset:2px;text-decoration-thickness:1px}.source-citation:active{color:var(--cm-ink)}@media (hover:hover) and (pointer:fine){.source-citation:hover{color:var(--cm-ink)}}.source-citation--button{cursor:pointer;background:0 0;border:0;padding:0}.source-citation--button:disabled{color:var(--cm-ink-3);cursor:not-allowed}.evidence-list{gap:var(--cm-space-sm);margin:0;padding:0;list-style:none;display:grid}.evidence-list li,.study-notice{padding:var(--cm-space-md);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground)}.evidence-list p{margin:0 0 var(--cm-space-xs)}.evidence-list strong{overflow-wrap:anywhere;font-family:var(--cm-font-mono);font-size:var(--cm-text-sm);margin-block-end:var(--cm-space-xs);display:block}.lens-notes{margin-block-start:var(--cm-space-sm);display:grid}.lens-note{gap:var(--cm-space-sm);min-width:0;padding-block:var(--cm-space-md);border-block-end:1px solid var(--cm-hairline);display:grid}.lens-note h3{margin:0 0 var(--cm-space-xs);font-family:var(--cm-font-display);font-size:var(--cm-text-base);font-style:normal}.lens-note p{max-width:65ch;margin:0 0 var(--cm-space-sm);color:var(--cm-ink-2)}.lens-note code{min-width:0;padding:var(--cm-space-sm);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground);color:var(--cm-ink);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);white-space:pre;display:block;overflow-x:auto}.star-chart-screen{min-width:0;padding:var(--cm-space-lg) max(var(--cm-space-md), env(safe-area-inset-right)) var(--cm-space-xl) max(var(--cm-space-md), env(safe-area-inset-left));background:var(--cm-ground);overflow:auto}.star-chart-intro{max-width:72rem;margin-inline:auto;padding-block-end:var(--cm-space-xl);display:block}.star-chart-intro>p:first-child{margin:0 0 var(--cm-space-sm);color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.star-chart-intro h1{overflow-wrap:anywhere;min-width:0;font-family:var(--cm-font-display);font-size:var(--cm-text-display);letter-spacing:-.035em;margin:0;font-style:normal;line-height:1.08}.star-chart-intro>p:not(:first-child){max-width:65ch;margin:var(--cm-space-md) 0 0;color:var(--cm-ink-2)}.star-chart-intro dl{gap:var(--cm-space-md);margin:var(--cm-space-xl) 0 0;padding-block:var(--cm-space-md);border-block:1px solid var(--cm-hairline);grid-template-columns:repeat(3,minmax(0,1fr));display:grid}.star-chart-intro dt,.concept-row dt{color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.star-chart-intro dd{margin:var(--cm-space-xs) 0 0;font-family:var(--cm-font-display);font-size:var(--cm-text-lg);font-variant-numeric:tabular-nums}.concept-ledger{border-block-start:1px solid var(--cm-hairline);max-width:72rem;margin-inline:auto}.concept-row{align-items:center;gap:var(--cm-space-lg);min-width:0;padding-block:var(--cm-space-lg);border-block-end:1px solid var(--cm-hairline);grid-template-columns:minmax(10rem,.7fr) minmax(8rem,1.2fr) auto;display:grid}.concept-row h2{overflow-wrap:anywhere;min-width:0;font-family:var(--cm-font-display);font-size:var(--cm-text-lg);margin:0;font-style:normal}.concept-row>div:first-child>span{color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.concept-meter{background:var(--cm-hairline);height:2px;overflow:hidden}.concept-meter span{background:var(--cm-star);height:100%;display:block}.concept-row dl{align-items:center;gap:var(--cm-space-lg);margin:0;display:flex}.concept-row dl div{min-width:5rem}.concept-row dd{margin:var(--cm-space-xs) 0 0;font-variant-numeric:tabular-nums}.status-line{z-index:var(--cm-z-sticky);justify-content:space-between;gap:var(--cm-space-2xs) var(--cm-space-md);min-width:0;max-width:100%;padding:var(--cm-space-sm) max(var(--cm-space-md), env(safe-area-inset-right)) max(var(--cm-space-sm), env(safe-area-inset-bottom)) max(var(--cm-space-md), env(safe-area-inset-left));border-block-start:1px solid var(--cm-hairline);background:var(--cm-ground);color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);font-variant-numeric:tabular-nums;flex-wrap:wrap;grid-row:4;display:flex;position:relative}.status-line span{white-space:normal}.status-line span:nth-child(2){display:none}.load-state,.webgl-error{align-content:center;gap:var(--cm-space-md);min-height:100dvh;padding:var(--cm-space-lg);background:var(--cm-ground);color:var(--cm-ink);display:grid}.load-state p,.webgl-error p{max-width:65ch;color:var(--cm-ink-2);margin:0}@keyframes panel-enter{0%{opacity:0;transform:translateY(var(--cm-space-md))}to{opacity:1;transform:translateY(0)}}@keyframes entrypoint-enter{0%{opacity:0;transform:translateY(var(--cm-space-md))}to{opacity:1;transform:translateY(0)}}@keyframes illumination-pulse{0%{opacity:0;transform:translate(-50%,-50%)scale(.75)}45%{opacity:1;transform:translate(-50%,-50%)scale(1.08)}to{opacity:0;transform:translate(-50%,-50%)scale(1)}}@media (width>=40rem){.instrument-rail{grid-template-columns:minmax(0,1fr) auto minmax(0,1fr)}.rail-overflow{display:contents}.mobile-menu-trigger{display:none}.rail-overflow__panel{display:contents;position:static}.location{text-align:center;grid-area:1/2}.rail-action,.home-readout{justify-self:end}.rail-actions{grid-area:1/3}.rail-controls{grid-row:2;justify-self:center}.status-line span{white-space:nowrap;display:inline}.lens-note{gap:var(--cm-space-lg);grid-template-columns:minmax(8rem,.35fr) minmax(0,1fr)}.study-preview{border-block-start:0;border-inline-start:1px solid var(--cm-hairline);width:min(48rem,60%);max-height:none;inset-block:0;inset-inline-start:auto}.check-panel{border-block-start:0;border-inline-start:1px solid var(--cm-hairline);width:min(42rem,56%);max-height:none;inset-block:0;inset-inline-start:auto}}@media (width<=39.999rem){.study-preview,.check-panel{border-block-start:0;max-height:none;inset:0}}@media (width<=48rem){.illumination-pulse{inset-block-start:35%;inset-inline-start:50%}.star-chart-intro dl{grid-template-columns:1fr}.concept-row{gap:var(--cm-space-md);grid-template-columns:1fr}.concept-row dl{justify-content:space-between}}@media (width>=60rem){.map-legend:not([hidden]){display:grid}.chart-stage:has(.index-sidebar) .star-chart-screen{margin-inline-start:var(--cm-sidebar-width)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.15s!important;animation-duration:.15s!important;animation-iteration-count:1!important}.study-preview,.check-panel,.mode-gate{animation-name:panel-enter-reduced}.entrypoint-picker{animation-name:entrypoint-enter-reduced}.illumination-pulse{animation-name:illumination-pulse-reduced}}@keyframes panel-enter-reduced{0%{opacity:0}to{opacity:1}}@keyframes entrypoint-enter-reduced{0%{opacity:0}to{opacity:1}}@keyframes illumination-pulse-reduced{0%{opacity:0}50%{opacity:1}to{opacity:0}}.picker-screen{gap:var(--cm-space-lg);min-height:100vh;padding:var(--cm-space-2xl) var(--cm-space-xl);flex-direction:column;max-width:44rem;margin:0 auto;display:flex}.picker-wordmark{font-family:var(--cm-font-display);font-size:var(--cm-text-sm);letter-spacing:.2em;text-transform:uppercase;color:var(--cm-ink-3)}.picker-subtitle{color:var(--cm-ink-2);font-size:var(--cm-text-sm)}.picker-recents ul,.picker-browser ul{margin:var(--cm-space-sm) 0 0;gap:var(--cm-space-2xs);flex-direction:column;padding:0;list-style:none;display:flex}.picker-recents button,.picker-browser ul button{justify-content:space-between;gap:var(--cm-space-sm);width:100%;padding:var(--cm-space-xs) var(--cm-space-sm);background:var(--cm-ground-2);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);color:var(--cm-ink);font-family:var(--cm-font-mono);font-size:var(--cm-text-sm);text-align:left;cursor:pointer;display:flex}.picker-recents button:hover,.picker-recents button:focus-visible,.picker-browser ul button:hover,.picker-browser ul button:focus-visible{border-color:var(--cm-orbit)}.picker-recents button{flex-wrap:wrap}.picker-recent-path{overflow-wrap:anywhere;min-width:0}.picker-recent-lit{color:var(--cm-star);white-space:nowrap}.picker-path{font-family:var(--cm-font-mono);font-size:var(--cm-text-sm);color:var(--cm-ink-2);overflow-wrap:anywhere}.picker-scale,.picker-error{border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);padding:var(--cm-space-sm);font-size:var(--cm-text-sm)}.picker-error{color:var(--cm-error)}.picker-select{padding:var(--cm-space-xs) var(--cm-space-lg);background:var(--cm-orbit-low);border:1px solid var(--cm-orbit);border-radius:var(--cm-radius);color:var(--cm-ink);font-size:var(--cm-text-base);cursor:pointer;align-self:flex-start}.picker-select:disabled{opacity:.6;cursor:progress}.layer-switcher{border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);display:inline-flex;overflow:hidden}.layer-switcher button{padding:var(--cm-space-2xs) var(--cm-space-sm);color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);cursor:pointer;background:0 0;border:0}.layer-switcher button[aria-pressed=true]{background:var(--cm-orbit-low);color:var(--cm-ink)}.map-view{gap:var(--cm-space-sm);padding:var(--cm-space-md);background:var(--cm-ground);flex-direction:column;display:flex;position:absolute;inset:0;overflow:hidden}.map-tabs{gap:var(--cm-space-xs);flex:none;display:flex}.map-tabs button{padding:var(--cm-space-2xs) var(--cm-space-sm);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);cursor:pointer;background:0 0}.map-tabs button[aria-pressed=true]{border-color:var(--cm-orbit);color:var(--cm-ink)}.map-canvas{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative}.map-zoom{z-index:var(--cm-z-raised);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);background:var(--cm-ground-2);gap:1px;display:flex;position:absolute;inset-block-start:var(--cm-space-sm);inset-inline-end:var(--cm-space-sm)}.map-zoom button{min-width:34px;min-height:30px;color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);cursor:pointer;background:0 0;border:0}.map-zoom button:hover:not(:disabled),.map-zoom button:focus-visible{color:var(--cm-orbit)}.map-zoom button:disabled{color:var(--cm-ink-3);cursor:default}@media (width<=40rem){.map-tabs{flex-wrap:wrap;padding-inline-end:4.5rem}.map-zoom{align-self:flex-end;margin-block-end:var(--cm-space-xs);position:static}}.map-scroll{cursor:grab;flex:auto;overflow:auto}.map-scroll[data-panning]{cursor:grabbing;-webkit-user-select:none;user-select:none}.map-canvas__sized{min-width:100%}.architecture-map,.workflow-tree{min-height:0;display:block}.architecture-map__edges path{color:var(--cm-hairline);stroke:currentColor;fill:none;stroke-linecap:round;stroke-linejoin:round}.architecture-map__edges path.is-cycle{color:var(--cm-route-possible)}.architecture-map__arrow{color:var(--cm-hairline)}.architecture-map__arrow.is-cycle{color:var(--cm-route-possible)}.workflow-tree__edges path{stroke:var(--cm-hairline);stroke-width:1.2px;fill:none}.architecture-map__box rect{fill:var(--cm-ground-2);stroke:var(--cm-hairline)}.architecture-map__box .box-tint{stroke:none}.architecture-map__box text{fill:var(--cm-ink);font-family:var(--cm-font-mono);font-size:13px}.architecture-map__box .box-meta{fill:var(--cm-ink-3);font-size:11px}.architecture-map__box[data-partial=true] rect:first-of-type{stroke:var(--cm-route-possible)}.architecture-map__box .box-partial{fill:var(--cm-route-possible);stroke:none}.architecture-map__box[data-understood=true] rect:first-of-type{stroke:var(--cm-star-high)}.architecture-map__box[data-home=true] rect:first-of-type{stroke-width:2px}.architecture-map__box[data-reachable=false] rect:first-of-type{stroke-dasharray:4 3}.architecture-map__box:focus-visible rect:first-of-type,.workflow-tree__row:focus-visible circle{outline:2px solid var(--cm-orbit);outline-offset:2px}.architecture-map__box,.workflow-tree__row{cursor:pointer}.workflow-tree__row circle{fill:var(--cm-ink-3)}.workflow-tree__row[data-partial=true] circle,.workflow-tree__row[data-cut] circle{fill:var(--cm-route-possible)}.workflow-tree__row[data-understood=true] circle{fill:var(--cm-star-high)}.workflow-tree__row text{fill:var(--cm-ink);font-family:var(--cm-font-mono);font-size:13px}.workflow-tree__row .row-meta{fill:var(--cm-ink-3);font-size:11px}.architecture-map__box[data-selected=true] rect:first-of-type{outline:2px solid var(--cm-orbit);outline-offset:3px}.workflow-tree__row[data-selected=true] circle{stroke:var(--cm-orbit);stroke-width:2px}.map-note,.map-loading{color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);flex:none;margin:0}.map-state{text-align:center;max-width:46ch;color:var(--cm-ink-2);margin:auto}.hint-chip{z-index:var(--cm-z-raised);align-items:center;gap:var(--cm-space-sm);min-width:0;padding:var(--cm-space-sm) var(--cm-space-md);border-block:1px solid var(--cm-orbit);background:var(--cm-ground-2);color:var(--cm-ink);font-size:var(--cm-text-sm);flex-wrap:wrap;grid-row:3;display:flex;position:static}.hint-chip small{color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.hint-chip button{padding:var(--cm-space-2xs) var(--cm-space-sm);border:1px solid var(--cm-orbit);border-radius:var(--cm-radius);color:var(--cm-orbit);font-size:var(--cm-text-xs);cursor:pointer;background:0 0}.location button{color:var(--cm-orbit);font:inherit;cursor:pointer;background:0 0;border:0;padding:0}.location button[disabled]{color:var(--cm-ink-2);cursor:default}.location button:not([disabled]):hover{text-decoration:underline}.coach-marks{inset-block:auto var(--cm-space-lg);width:min(38ch, calc(100% - var(--cm-space-lg)));padding:var(--cm-space-md);border:1px solid var(--cm-orbit);border-radius:var(--cm-radius);background:var(--cm-ground-2);color:var(--cm-ink);margin-inline:auto;inset-inline:0}.coach-marks::backdrop{background:var(--cm-scrim)}.coach-marks__progress{margin:0 0 var(--cm-space-2xs);color:var(--cm-ink-3);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs)}.coach-marks h1{margin:0 0 var(--cm-space-xs);font-size:var(--cm-text-lg)}.coach-marks__actions{justify-content:space-between;gap:var(--cm-space-sm);margin-block-start:var(--cm-space-md);display:flex}.coach-skip{color:var(--cm-ink-3);cursor:pointer;background:0 0;border:0}.legend-tint{width:var(--cm-space-sm);height:var(--cm-space-sm);border-radius:2px;display:inline-block}.legend-tint--python{background:var(--cm-neb-python)}.legend-tint--javascript{background:var(--cm-neb-js)}.legend-tint--typescript{background:var(--cm-neb-ts)}.legend-route--dashed{border-block-start-style:dashed}.legend-size{width:var(--cm-space-md);height:var(--cm-space-sm);background:var(--cm-ink-3);border-radius:50%;display:inline-block}.legend-brightness{width:var(--cm-space-md);height:var(--cm-space-sm);background:linear-gradient(90deg, var(--cm-node-unlit), var(--cm-ink-2));display:inline-block}.app-shell[data-mode=easy]{--cm-text-xs:.8125rem;--cm-text-sm:.9375rem;--cm-text-base:1.125rem;--cm-text-lg:1.375rem;--cm-space-sm:1rem;--cm-space-md:1.375rem;--cm-space-lg:2.5rem;font-size:var(--cm-text-base);line-height:1.7}.app-shell[data-mode=easy] .orientation-copy p,.app-shell[data-mode=easy] .study-preview p,.app-shell[data-mode=easy] .grounded-explanation>p,.app-shell[data-mode=easy] .study-notice p,.app-shell[data-mode=easy] .evidence-list p,.app-shell[data-mode=easy] .lens-note p,.app-shell[data-mode=easy] .active-check fieldset>p,.app-shell[data-mode=easy] .check-state p,.app-shell[data-mode=easy] .check-complete p,.app-shell[data-mode=easy] .check-loading{max-width:46ch}.app-shell[data-mode=easy] .language-focus__label,.app-shell[data-mode=easy] .mode-toggle__label{text-transform:none;letter-spacing:normal}.loading-screen{gap:var(--cm-space-md);max-width:44rem;min-height:100vh;padding:var(--cm-space-lg) max(var(--cm-space-md), env(safe-area-inset-right)) var(--cm-space-lg) max(var(--cm-space-md), env(safe-area-inset-left));align-content:center;justify-items:start;margin:0 auto;display:grid}.loading-header h1{min-width:0;margin:var(--cm-space-2xs) 0 0;font-family:var(--cm-font-display);font-size:var(--cm-text-xl);overflow-wrap:anywhere;line-height:1.15}.loading-path{font-family:var(--cm-font-mono);font-size:var(--cm-text-sm);color:var(--cm-ink-2)}.loading-subtitle{margin:var(--cm-space-xs) 0 0;color:var(--cm-ink-2);font-size:var(--cm-text-sm)}.loading-stages{gap:var(--cm-space-2xs);width:100%;margin:0;padding:0;list-style:none;display:grid}.loading-stages li{justify-content:space-between;gap:var(--cm-space-sm);padding:var(--cm-space-xs) var(--cm-space-sm);border-left:2px solid var(--cm-hairline);color:var(--cm-ink-3);font-size:var(--cm-text-sm);flex-wrap:wrap;display:flex}.loading-stages li[data-state=active]{border-left-color:var(--cm-orbit);color:var(--cm-ink)}.loading-stages li[data-state=done]{border-left-color:var(--cm-orbit-deep);color:var(--cm-ink-2)}.loading-stages small{font-family:var(--cm-font-mono);color:var(--cm-ink-2)}.loading-meter{width:100%;height:var(--cm-space-xs);accent-color:var(--cm-orbit)}.loading-live{color:var(--cm-ink);font-size:var(--cm-text-sm)}.loading-error{border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);width:100%;padding:var(--cm-space-sm);color:var(--cm-ink-2);font-size:var(--cm-text-sm)}.loading-actions{gap:var(--cm-space-sm);flex-wrap:wrap;display:flex}.loading-actions button{padding:var(--cm-space-xs) var(--cm-space-md)}.picker-scale h2,.picker-failure h2{margin:0 0 var(--cm-space-xs);font-family:var(--cm-font-display);font-size:var(--cm-text-base)}.picker-scale-scopes{gap:var(--cm-space-xs);margin:var(--cm-space-sm) 0;flex-wrap:wrap;padding:0;list-style:none;display:flex}.picker-scale-scopes button{align-items:flex-start;gap:var(--cm-space-2xs);min-height:44px;padding:var(--cm-space-2xs) var(--cm-space-sm);background:var(--cm-ground-2);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);color:var(--cm-ink);font-family:var(--cm-font-mono);font-size:var(--cm-text-sm);cursor:pointer;flex-direction:column;display:flex}.picker-scale-scopes button:hover:not(:disabled),.picker-scale-scopes button:focus-visible{border-color:var(--cm-orbit)}.picker-scale-scopes button:disabled{opacity:.6;cursor:progress}.picker-scale-scopes small{color:var(--cm-ink-2)}.picker-path-entry{gap:var(--cm-space-xs);flex-wrap:wrap;align-items:center;display:flex}.picker-path-entry label{color:var(--cm-ink-2)}.picker-path-entry input{min-width:0;min-height:44px;padding:var(--cm-space-2xs) var(--cm-space-xs);background:var(--cm-ground);border:1px solid var(--cm-hairline);border-radius:var(--cm-radius);color:var(--cm-ink);font-family:var(--cm-font-mono);font-size:var(--cm-text-sm);flex:14rem}.picker-path-entry button{min-height:44px;padding:var(--cm-space-2xs) var(--cm-space-md);background:var(--cm-orbit-low);border:1px solid var(--cm-orbit);border-radius:var(--cm-radius);color:var(--cm-ink);font-size:var(--cm-text-sm);cursor:pointer}.picker-path-entry button:disabled{opacity:.6;cursor:not-allowed}.picker-failure{border:1px solid var(--cm-error);border-radius:var(--cm-radius);padding:var(--cm-space-sm);font-size:var(--cm-text-sm)}.picker-failure>p{margin:0 0 var(--cm-space-sm);color:var(--cm-ink-2)}.picker-failure__detail{gap:var(--cm-space-2xs);display:grid}.picker-failure__detail span{color:var(--cm-ink-3);font-size:var(--cm-text-xs);text-transform:uppercase;letter-spacing:.08em}.picker-failure__detail code{max-height:8rem;padding:var(--cm-space-xs);background:var(--cm-ground-2);border-radius:var(--cm-radius);color:var(--cm-ink-2);font-family:var(--cm-font-mono);font-size:var(--cm-text-xs);overflow-wrap:anywhere;display:block;overflow:auto}.picker-failure .picker-select{min-height:44px;font-size:var(--cm-text-sm);align-self:flex-start}.progress-reset{align-items:center;gap:var(--cm-space-sm);max-width:72rem;margin:var(--cm-space-lg) auto 0;padding-top:var(--cm-space-md);border-top:1px solid var(--cm-hairline);flex-wrap:wrap;display:flex}.progress-reset p{color:var(--cm-ink-2);font-size:var(--cm-text-sm);margin:0}.progress-reset button{min-height:44px;padding-inline:var(--cm-space-md);font-size:var(--cm-text-sm)}.progress-reset--confirming{justify-items:start;gap:var(--cm-space-sm);display:grid}.progress-reset--confirming>p[role=alert]{max-width:60ch;color:var(--cm-ink)}.progress-reset--confirming>div{gap:var(--cm-space-sm);flex-wrap:wrap;display:flex}.progress-reset__confirm{border-color:var(--cm-error);color:var(--cm-error)}.progress-reset__error{color:var(--cm-error)}