@conduction/docusaurus-preset 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/MISSING_COMPONENTS.md +109 -0
  2. package/README.md +171 -0
  3. package/package.json +59 -0
  4. package/src/components/AgentTrace/AgentTrace.jsx +128 -0
  5. package/src/components/AgentTrace/AgentTrace.module.css +115 -0
  6. package/src/components/AppMock/AppMock.jsx +86 -0
  7. package/src/components/AppMock/AppMock.module.css +629 -0
  8. package/src/components/AppMock/variants/DeciDeskMock.jsx +71 -0
  9. package/src/components/AppMock/variants/DocuDeskMock.jsx +69 -0
  10. package/src/components/AppMock/variants/LarpingAppMock.jsx +59 -0
  11. package/src/components/AppMock/variants/MyDashBiMock.jsx +135 -0
  12. package/src/components/AppMock/variants/MyDashMock.jsx +96 -0
  13. package/src/components/AppMock/variants/MyDashTilesMock.jsx +103 -0
  14. package/src/components/AppMock/variants/MyDashWidgetsMock.jsx +123 -0
  15. package/src/components/AppMock/variants/NLDesignMock.jsx +70 -0
  16. package/src/components/AppMock/variants/OpenCatalogiMock.jsx +61 -0
  17. package/src/components/AppMock/variants/OpenConnectorMock.jsx +83 -0
  18. package/src/components/AppMock/variants/OpenRegisterMock.jsx +100 -0
  19. package/src/components/AppMock/variants/OpenWooMock.jsx +61 -0
  20. package/src/components/AppMock/variants/PipelinQMock.jsx +88 -0
  21. package/src/components/AppMock/variants/ProcestMock.jsx +87 -0
  22. package/src/components/AppMock/variants/SoftwareCatalogMock.jsx +71 -0
  23. package/src/components/AppMock/variants/ZaakAfhandelAppMock.jsx +71 -0
  24. package/src/components/AppsGrid/AppsGrid.jsx +84 -0
  25. package/src/components/AppsGrid/AppsGrid.module.css +46 -0
  26. package/src/components/AppsPreview/AppsPreview.jsx +85 -0
  27. package/src/components/AppsPreview/AppsPreview.module.css +128 -0
  28. package/src/components/Clients/Clients.jsx +205 -0
  29. package/src/components/Clients/Clients.module.css +166 -0
  30. package/src/components/ComposeBlock/ComposeBlock.jsx +70 -0
  31. package/src/components/ComposeBlock/ComposeBlock.module.css +74 -0
  32. package/src/components/ConductionBg/ConductionBg.jsx +150 -0
  33. package/src/components/ConductionBg/ConductionBg.module.css +41 -0
  34. package/src/components/ContentCard/ContentCard.jsx +126 -0
  35. package/src/components/ContentCard/ContentCard.module.css +84 -0
  36. package/src/components/ContentDetailHero/ContentDetailHero.jsx +136 -0
  37. package/src/components/ContentDetailHero/ContentDetailHero.module.css +96 -0
  38. package/src/components/ContentTypeFilter/ContentTypeFilter.jsx +103 -0
  39. package/src/components/ContentTypeFilter/ContentTypeFilter.module.css +60 -0
  40. package/src/components/ContentTypeFilter/contentTypes.js +58 -0
  41. package/src/components/CookieCli/CookieCli.jsx +223 -0
  42. package/src/components/CookieCli/CookieCli.module.css +166 -0
  43. package/src/components/CtaBanner/CtaBanner.jsx +61 -0
  44. package/src/components/CtaBanner/CtaBanner.module.css +65 -0
  45. package/src/components/DetailHero/DetailHero.jsx +143 -0
  46. package/src/components/DetailHero/DetailHero.module.css +154 -0
  47. package/src/components/Diagrams/Diagrams.jsx +148 -0
  48. package/src/components/EmployeeCard/EmployeeCard.jsx +127 -0
  49. package/src/components/EmployeeCard/EmployeeCard.module.css +144 -0
  50. package/src/components/ExternalAppShelf/ExternalAppShelf.jsx +61 -0
  51. package/src/components/ExternalAppShelf/ExternalAppShelf.module.css +90 -0
  52. package/src/components/FAQ/FAQ.jsx +42 -0
  53. package/src/components/FAQ/FAQ.module.css +74 -0
  54. package/src/components/FacetedFilters/FacetedFilters.jsx +125 -0
  55. package/src/components/FacetedFilters/FacetedFilters.module.css +133 -0
  56. package/src/components/FeatureGrid/FeatureGrid.jsx +94 -0
  57. package/src/components/FeatureGrid/FeatureGrid.module.css +114 -0
  58. package/src/components/FeatureList/FeatureList.jsx +54 -0
  59. package/src/components/FeatureList/FeatureList.module.css +52 -0
  60. package/src/components/FeaturedCard/FeaturedCard.jsx +101 -0
  61. package/src/components/FeaturedCard/FeaturedCard.module.css +98 -0
  62. package/src/components/GameModal/GameModal.jsx +197 -0
  63. package/src/components/GameModal/GameModal.module.css +184 -0
  64. package/src/components/Hero/Hero.jsx +101 -0
  65. package/src/components/Hero/Hero.module.css +95 -0
  66. package/src/components/HexBackground/HexBackground.jsx +56 -0
  67. package/src/components/HexBackground/HexBackground.module.css +73 -0
  68. package/src/components/HexNetwork/HexNetwork.jsx +141 -0
  69. package/src/components/HexNetwork/HexNetwork.module.css +187 -0
  70. package/src/components/HexRain/HexRain.jsx +81 -0
  71. package/src/components/HowSteps/HowSteps.jsx +57 -0
  72. package/src/components/HowSteps/HowSteps.module.css +52 -0
  73. package/src/components/ManagedCommonGround/ManagedCommonGround.jsx +78 -0
  74. package/src/components/ManagedCommonGround/ManagedCommonGround.module.css +16 -0
  75. package/src/components/NewsletterCta/NewsletterCta.jsx +83 -0
  76. package/src/components/NewsletterCta/NewsletterCta.module.css +103 -0
  77. package/src/components/PairCard/PairCard.jsx +58 -0
  78. package/src/components/PairCard/PairCard.module.css +54 -0
  79. package/src/components/PartnerCard/PartnerCard.jsx +130 -0
  80. package/src/components/PartnerCard/PartnerCard.module.css +198 -0
  81. package/src/components/PartnerDirectory/PartnerDirectory.jsx +122 -0
  82. package/src/components/PartnerDirectory/PartnerDirectory.module.css +25 -0
  83. package/src/components/PartnerSidecard/PartnerSidecard.jsx +116 -0
  84. package/src/components/PartnerSidecard/PartnerSidecard.module.css +185 -0
  85. package/src/components/Pipeline/Pipeline.jsx +198 -0
  86. package/src/components/Pipeline/Pipeline.module.css +206 -0
  87. package/src/components/PlatformDiagram/PlatformDiagram.jsx +110 -0
  88. package/src/components/PlatformOverview/PlatformOverview.jsx +68 -0
  89. package/src/components/PlatformOverview/PlatformOverview.module.css +71 -0
  90. package/src/components/ReferenceCard/ReferenceCard.jsx +44 -0
  91. package/src/components/ReferenceCard/ReferenceCard.module.css +57 -0
  92. package/src/components/RelatedPosts/RelatedPosts.jsx +58 -0
  93. package/src/components/RelatedPosts/RelatedPosts.module.css +51 -0
  94. package/src/components/RotatingCards/RotatingCards.jsx +98 -0
  95. package/src/components/RotatingCards/RotatingCards.module.css +153 -0
  96. package/src/components/Showcase/Showcase.jsx +129 -0
  97. package/src/components/Showcase/Showcase.module.css +168 -0
  98. package/src/components/SolutionCard/SolutionCard.jsx +83 -0
  99. package/src/components/SolutionCard/SolutionCard.module.css +99 -0
  100. package/src/components/StatsStrip/StatsStrip.jsx +38 -0
  101. package/src/components/StatsStrip/StatsStrip.module.css +53 -0
  102. package/src/components/WidgetShelf/WidgetShelf.jsx +67 -0
  103. package/src/components/WidgetShelf/WidgetShelf.module.css +73 -0
  104. package/src/components/index.js +96 -0
  105. package/src/components/primitives/AuthorByline.jsx +85 -0
  106. package/src/components/primitives/AuthorByline.module.css +57 -0
  107. package/src/components/primitives/BrandCitation.jsx +71 -0
  108. package/src/components/primitives/Button.jsx +46 -0
  109. package/src/components/primitives/Button.module.css +88 -0
  110. package/src/components/primitives/Card.jsx +42 -0
  111. package/src/components/primitives/Card.module.css +42 -0
  112. package/src/components/primitives/Eyebrow.jsx +37 -0
  113. package/src/components/primitives/Eyebrow.module.css +19 -0
  114. package/src/components/primitives/HexBullet.jsx +37 -0
  115. package/src/components/primitives/HexBullet.module.css +16 -0
  116. package/src/components/primitives/HexThumbnail.jsx +70 -0
  117. package/src/components/primitives/HexThumbnail.module.css +45 -0
  118. package/src/components/primitives/Pill.jsx +42 -0
  119. package/src/components/primitives/Pill.module.css +30 -0
  120. package/src/components/primitives/Section.jsx +51 -0
  121. package/src/components/primitives/Section.module.css +31 -0
  122. package/src/components/primitives/SectionHead.jsx +36 -0
  123. package/src/components/primitives/SectionHead.module.css +43 -0
  124. package/src/components/primitives/index.js +22 -0
  125. package/src/css/brand.css +158 -0
  126. package/src/css/tokens.css +12 -0
  127. package/src/data/app-downloads.js +42 -0
  128. package/src/diagrams/README.md +74 -0
  129. package/src/diagrams/cn-domain-tree.js +105 -0
  130. package/src/diagrams/cn-hex-prism.js +163 -0
  131. package/src/diagrams/cn-hex.js +181 -0
  132. package/src/diagrams/cn-honeycomb-bg.js +135 -0
  133. package/src/diagrams/cn-pipeline.js +150 -0
  134. package/src/diagrams/cn-platform.js +156 -0
  135. package/src/diagrams/cn-side-box.js +104 -0
  136. package/src/diagrams/index.js +28 -0
  137. package/src/index.js +183 -0
  138. package/src/theme/Footer/index.jsx +516 -0
  139. package/src/theme/MDXPage/index.jsx +134 -0
  140. package/src/theme/Navbar/index.jsx +120 -0
  141. package/src/theme/Navbar/styles.module.css +114 -0
  142. package/src/theme/brand.jsx +63 -0
  143. package/src/theme.js +45 -0
  144. package/src/utils/lazyScript.js +37 -0
  145. package/static/img/favicon.svg +14 -0
  146. package/static/img/honeycomb-scatter.svg +23 -0
  147. package/static/img/honeycomb-watermark.svg +108 -0
  148. package/static/img/logo-dark.svg +11 -0
  149. package/static/img/logo.svg +14 -0
  150. package/static/img/nextcloud-logo.svg +5 -0
  151. package/static/lib/canal-footer.css +418 -0
  152. package/static/lib/canal-footer.js +499 -0
  153. package/static/lib/clients-flow.js +317 -0
  154. package/static/lib/conduction-bg.css +50 -0
  155. package/static/lib/conduction-bg.js +122 -0
  156. package/static/lib/hex-rain.css +128 -0
  157. package/static/lib/hex-rain.js +284 -0
  158. package/static/lib/kade-cyclist.css +264 -0
  159. package/static/lib/kade-cyclist.js +420 -0
  160. package/static/lib/logo-memory.css +219 -0
  161. package/static/lib/logo-memory.js +540 -0
  162. package/static/lib/platform-diagram.css +458 -0
  163. package/static/lib/platform-diagram.js +414 -0
@@ -0,0 +1,420 @@
1
+ /* Kade Cyclist: hidden minigame on the canal-footer kade strip. First
2
+ click on a drifting kade bike clears the ambient kade traffic, drops
3
+ the player cyclist under the blue Conduction house in the skyline,
4
+ and starts a slow-then-ramping dodge round. Hazards (bike → scooter
5
+ → car → bus → truck → tram) unlock progressively as the player racks
6
+ up dodges, and both spawn rate + drift speed creep up.
7
+
8
+ ↑/W and ↓/S switch between the top and bottom lane. Collision ends
9
+ the round, fires `connext:gameend` on window so the GameModal picks
10
+ it up, and the modal's "Play again" button fires `connext:gamereplay`
11
+ which we listen for to re-run the round. The ambient kade is
12
+ restored when the round ends. */
13
+ (function () {
14
+ const PLAYER_COLLISION_X_TOL = 24; // px tolerance around the player x
15
+ const HAZARD_BASE_MS = 6000; // very slow first hazards (drift duration)
16
+ const HAZARD_FLOOR_MS = 1900; // cap at hardest difficulty
17
+ const SPAWN_INITIAL_MS = 4500; // very slow first spawn interval
18
+ const SPAWN_FLOOR_MS = 700; // cap at hardest spawn interval
19
+
20
+ /* Difficulty tiers — kind unlocks happen at fixed dodge counts. The
21
+ speed/spawn multipliers grow continuously per dodge; tiers only
22
+ control which kinds are eligible to spawn. Vehicles get
23
+ progressively bigger and harder to read at speed. */
24
+ const KIND_TIERS = [
25
+ { atDodges: 0, kinds: ['bike'] },
26
+ { atDodges: 3, kinds: ['bike', 'scooter'] },
27
+ { atDodges: 6, kinds: ['bike', 'scooter', 'car'] },
28
+ { atDodges: 10, kinds: ['bike', 'scooter', 'car', 'bus'] },
29
+ { atDodges: 15, kinds: ['bike', 'scooter', 'car', 'bus', 'truck'] },
30
+ { atDodges: 22, kinds: ['bike', 'scooter', 'car', 'bus', 'truck', 'tram'] },
31
+ ];
32
+
33
+ function currentKinds(dodges) {
34
+ let active = KIND_TIERS[0].kinds;
35
+ for (let i = 0; i < KIND_TIERS.length; i++) {
36
+ if (dodges >= KIND_TIERS[i].atDodges) active = KIND_TIERS[i].kinds;
37
+ }
38
+ return active;
39
+ }
40
+
41
+ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
42
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
43
+
44
+ /* Inline SVG library. Kept in JS so the runtime doesn't depend on the
45
+ host page's <template> blocks. Sizes match the kade band's 16px
46
+ content height; viewBoxes leave a small bleed above for handlebars,
47
+ poles, and exhaust. */
48
+ function buildHazardSvg(kind) {
49
+ if (kind === 'bike') {
50
+ return (
51
+ '<svg class="kc-hazard-svg kc-hk-bike" width="22" height="16" viewBox="0 -2 22 18" aria-hidden="true">' +
52
+ '<g stroke="#0A172F" stroke-width="1.4" fill="none" stroke-linecap="round">' +
53
+ '<circle cx="4" cy="12" r="3"/>' +
54
+ '<circle cx="18" cy="12" r="3"/>' +
55
+ '<line x1="4" y1="12" x2="11" y2="6"/>' +
56
+ '<line x1="11" y1="6" x2="18" y2="12"/>' +
57
+ '<line x1="11" y1="6" x2="14" y2="12"/>' +
58
+ '<line x1="11" y1="6" x2="11" y2="3"/>' +
59
+ '<line x1="11" y1="3" x2="14" y2="6"/>' +
60
+ '</g>' +
61
+ '<circle cx="11" cy="1.5" r="1.6" fill="#0A172F"/>' +
62
+ '</svg>'
63
+ );
64
+ }
65
+ if (kind === 'scooter') {
66
+ return (
67
+ '<svg class="kc-hazard-svg kc-hk-scooter" width="22" height="16" viewBox="0 -2 22 18" aria-hidden="true">' +
68
+ '<g stroke="#3A3F4B" stroke-width="1.4" fill="none" stroke-linecap="round">' +
69
+ '<circle cx="5" cy="12" r="3"/>' +
70
+ '<circle cx="17" cy="12" r="3"/>' +
71
+ '<path d="M 5,12 L 11,12 L 14,5 L 17,12" stroke-width="1.6"/>' +
72
+ '<line x1="14" y1="5" x2="16" y2="2"/>' +
73
+ '<line x1="14" y1="5" x2="12" y2="2"/>' +
74
+ '</g>' +
75
+ '</svg>'
76
+ );
77
+ }
78
+ if (kind === 'car') {
79
+ return (
80
+ '<svg class="kc-hazard-svg kc-hk-car" width="38" height="16" viewBox="0 0 38 16" aria-hidden="true">' +
81
+ '<rect x="0" y="6" width="38" height="6" rx="2" fill="#0A172F"/>' +
82
+ '<path d="M 5,6 L 9,2 L 29,2 L 33,6 Z" fill="#0A172F"/>' +
83
+ '<rect x="11" y="3" width="6" height="3" fill="rgba(255,255,255,0.4)"/>' +
84
+ '<rect x="20" y="3" width="6" height="3" fill="rgba(255,255,255,0.4)"/>' +
85
+ '<circle cx="9" cy="13" r="2" fill="#C8482F"/>' +
86
+ '<circle cx="29" cy="13" r="2" fill="#C8482F"/>' +
87
+ '</svg>'
88
+ );
89
+ }
90
+ if (kind === 'bus') {
91
+ /* GVB-style city bus, side profile. Cobalt body, white windows. */
92
+ return (
93
+ '<svg class="kc-hazard-svg kc-hk-bus" width="58" height="16" viewBox="0 -1 58 17" aria-hidden="true">' +
94
+ '<rect x="0" y="2" width="58" height="11" rx="2" fill="#21468B"/>' +
95
+ '<rect x="2" y="4" width="6" height="4" fill="rgba(255,255,255,0.85)"/>' +
96
+ '<rect x="10" y="4" width="6" height="4" fill="rgba(255,255,255,0.85)"/>' +
97
+ '<rect x="18" y="4" width="6" height="4" fill="rgba(255,255,255,0.85)"/>' +
98
+ '<rect x="26" y="4" width="6" height="4" fill="rgba(255,255,255,0.85)"/>' +
99
+ '<rect x="34" y="4" width="6" height="4" fill="rgba(255,255,255,0.85)"/>' +
100
+ '<rect x="42" y="4" width="6" height="4" fill="rgba(255,255,255,0.85)"/>' +
101
+ '<rect x="50" y="4" width="6" height="4" fill="rgba(255,255,255,0.85)"/>' +
102
+ '<rect x="50" y="9" width="6" height="2" fill="#F77F0E"/>' +
103
+ '<circle cx="10" cy="13" r="2" fill="#0A172F"/>' +
104
+ '<circle cx="48" cy="13" r="2" fill="#0A172F"/>' +
105
+ '</svg>'
106
+ );
107
+ }
108
+ if (kind === 'truck') {
109
+ /* Cab + container, two sets of wheels. Cobalt cab, white box. */
110
+ return (
111
+ '<svg class="kc-hazard-svg kc-hk-truck" width="68" height="16" viewBox="0 0 68 16" aria-hidden="true">' +
112
+ '<rect x="0" y="3" width="42" height="9" fill="#FFFFFF" stroke="#0A172F" stroke-width="0.8"/>' +
113
+ '<rect x="42" y="5" width="20" height="7" fill="#21468B"/>' +
114
+ '<path d="M 62,5 L 66,8 L 62,8 Z" fill="#21468B"/>' +
115
+ '<rect x="46" y="6" width="6" height="3" fill="rgba(255,255,255,0.5)"/>' +
116
+ '<circle cx="8" cy="13" r="2" fill="#0A172F"/>' +
117
+ '<circle cx="22" cy="13" r="2" fill="#0A172F"/>' +
118
+ '<circle cx="50" cy="13" r="2" fill="#0A172F"/>' +
119
+ '<circle cx="60" cy="13" r="2" fill="#0A172F"/>' +
120
+ '</svg>'
121
+ );
122
+ }
123
+ /* tram — GVB Amsterdam, blue body with yellow band, overhead pole */
124
+ return (
125
+ '<svg class="kc-hazard-svg kc-hk-tram" width="80" height="16" viewBox="0 -3 80 19" aria-hidden="true">' +
126
+ '<line x1="40" y1="-3" x2="40" y2="2" stroke="#3A3F4B" stroke-width="0.8"/>' +
127
+ '<rect x="0" y="2" width="80" height="11" rx="1" fill="#21468B"/>' +
128
+ '<rect x="0" y="9" width="80" height="2" fill="#F4D04A"/>' +
129
+ '<rect x="2" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
130
+ '<rect x="9" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
131
+ '<rect x="16" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
132
+ '<rect x="23" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
133
+ '<rect x="32" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
134
+ '<rect x="39" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
135
+ '<rect x="46" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
136
+ '<rect x="53" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
137
+ '<rect x="60" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
138
+ '<rect x="67" y="4" width="5" height="4" fill="rgba(255,255,255,0.9)"/>' +
139
+ '<rect x="74" y="4" width="4" height="4" fill="rgba(255,255,255,0.9)"/>' +
140
+ '<circle cx="14" cy="13" r="1.6" fill="#0A172F"/>' +
141
+ '<circle cx="22" cy="13" r="1.6" fill="#0A172F"/>' +
142
+ '<circle cx="58" cy="13" r="1.6" fill="#0A172F"/>' +
143
+ '<circle cx="66" cy="13" r="1.6" fill="#0A172F"/>' +
144
+ '</svg>'
145
+ );
146
+ }
147
+
148
+ /* Player bike: like the ki-bike-1 outline but with an orange jersey
149
+ so it reads as the player tile, not ambient traffic. */
150
+ function buildPlayerSvg() {
151
+ return (
152
+ '<svg class="kc-player-svg" width="22" height="16" viewBox="0 -2 22 18" aria-hidden="true">' +
153
+ '<g stroke="#0A172F" stroke-width="1.4" fill="none" stroke-linecap="round">' +
154
+ '<circle cx="4" cy="12" r="3"/>' +
155
+ '<circle cx="18" cy="12" r="3"/>' +
156
+ '<line x1="4" y1="12" x2="11" y2="6"/>' +
157
+ '<line x1="11" y1="6" x2="18" y2="12"/>' +
158
+ '<line x1="11" y1="6" x2="14" y2="12"/>' +
159
+ '<line x1="11" y1="6" x2="11" y2="3"/>' +
160
+ '<line x1="11" y1="3" x2="14" y2="6"/>' +
161
+ '</g>' +
162
+ '<circle cx="11" cy="1.5" r="1.6" fill="var(--c-orange-knvb, #F77F0E)"/>' +
163
+ '<circle cx="4" cy="12" r="1.6" fill="var(--c-orange-knvb, #F77F0E)"/>' +
164
+ '<circle cx="18" cy="12" r="1.6" fill="var(--c-orange-knvb, #F77F0E)"/>' +
165
+ '</svg>'
166
+ );
167
+ }
168
+
169
+ function findRoot() { return document.querySelector('.canal-footer'); }
170
+
171
+ /* Per-root state stash so the connext:gamereplay listener can find
172
+ the right root and tear-down can restore the ambient items. */
173
+ const ACTIVE = new WeakMap();
174
+
175
+ function hydrate() {
176
+ const root = findRoot();
177
+ if (!root) return;
178
+ if (root.dataset.kadeHydrated === '1') return;
179
+ const kade = root.querySelector('.kade');
180
+ if (!kade) return;
181
+ root.dataset.kadeHydrated = '1';
182
+
183
+ const onBikeClick = function (e) {
184
+ if (root.dataset.kadeActive === '1') return;
185
+ const target = e.target.closest('.ki-bike-1, .ki-bike-2');
186
+ if (!target || !kade.contains(target)) return;
187
+ e.preventDefault();
188
+ e.stopPropagation();
189
+ startGame(root, kade);
190
+ };
191
+ kade.addEventListener('click', onBikeClick, true);
192
+
193
+ /* Modal "Play again" → re-run the round. */
194
+ window.addEventListener('connext:gamereplay', function (e) {
195
+ if (e.detail && e.detail.id === 'kade-cyclist') {
196
+ if (root.dataset.kadeActive === '1') tearDownActive(root, kade);
197
+ setTimeout(function () { startGame(root, kade); }, 80);
198
+ }
199
+ });
200
+ /* Modal "Close" → tear down the game stage and restore ambient. */
201
+ window.addEventListener('connext:gameclose', function (e) {
202
+ if (e.detail && e.detail.id === 'kade-cyclist') {
203
+ if (root.dataset.kadeActive === '1') tearDownActive(root, kade);
204
+ }
205
+ });
206
+ }
207
+
208
+ function startGame(root, kade) {
209
+ root.dataset.kadeActive = '1';
210
+
211
+ /* Clear the ambient kade — actually remove the items, not just
212
+ pause them, so the field reads as a clean playfield. We stash
213
+ the cleared markup on the kade-items container so tearDown can
214
+ restore it. */
215
+ const kadeItems = kade.querySelector('.kade-items');
216
+ const ambientHTML = kadeItems ? kadeItems.innerHTML : '';
217
+ if (kadeItems) kadeItems.innerHTML = '';
218
+
219
+ /* Compute player x: align under the blue Conduction house in the
220
+ skyline. The skyline is randomised at every page load by canal-
221
+ footer.js but always marks the centre house with .house-
222
+ conduction. Fall back to 14% of viewport if the marker isn't
223
+ present yet. */
224
+ const blueHouse = root.querySelector('.house-conduction');
225
+ const rootRect = root.getBoundingClientRect();
226
+ let playerCenterX;
227
+ if (blueHouse) {
228
+ const r = blueHouse.getBoundingClientRect();
229
+ playerCenterX = (r.left + r.width / 2) - rootRect.left;
230
+ } else {
231
+ playerCenterX = rootRect.width * 0.14 + 11;
232
+ }
233
+
234
+ const stage = document.createElement('div');
235
+ stage.className = 'kc-stage';
236
+ stage.innerHTML =
237
+ '<div class="kc-track" data-track>' +
238
+ '<div class="kc-lane kc-lane-top" data-lane="top"></div>' +
239
+ '<div class="kc-lane kc-lane-bottom" data-lane="bottom"></div>' +
240
+ '<div class="kc-player" data-player>' + buildPlayerSvg() + '</div>' +
241
+ '</div>' +
242
+ '<div class="kc-hud">' +
243
+ '<div class="kc-score-block"><span class="kc-score-num" data-score>0</span><span class="kc-score-label">Score</span></div>' +
244
+ '<div class="kc-controls" aria-hidden="true">' +
245
+ '<kbd>&uarr;</kbd>/<kbd>W</kbd> &middot; <kbd>&darr;</kbd>/<kbd>S</kbd>' +
246
+ '</div>' +
247
+ '<button type="button" class="kc-close" data-close aria-label="Stop spel">&times;</button>' +
248
+ '</div>';
249
+ kade.appendChild(stage);
250
+
251
+ const trackEl = stage.querySelector('[data-track]');
252
+ const playerEl = stage.querySelector('[data-player]');
253
+ const scoreSpan = stage.querySelector('[data-score]');
254
+ const closeBtn = stage.querySelector('[data-close]');
255
+
256
+ /* Position the player centred on the blue-house x. CSS has the
257
+ player at left: var(--kc-player-x). */
258
+ stage.style.setProperty('--kc-player-x', (playerCenterX - 11) + 'px');
259
+
260
+ let lane = 'bottom';
261
+ playerEl.dataset.lane = lane;
262
+
263
+ let score = 0;
264
+ let speedMul = 1;
265
+ let spawnTimer = null;
266
+ let rafId = null;
267
+ let over = false;
268
+ const hazards = [];
269
+ const startTime = performance.now();
270
+
271
+ function setLane(next) {
272
+ if (over) return;
273
+ if (lane === next) return;
274
+ lane = next;
275
+ playerEl.dataset.lane = next;
276
+ }
277
+
278
+ function spawnHazard() {
279
+ if (over) return;
280
+ /* Pick a kind from the currently-unlocked tier — heavier
281
+ vehicles are still possible at low scores, but only the
282
+ starter set. */
283
+ const kinds = currentKinds(score);
284
+ const kind = kinds[(Math.random() * kinds.length) | 0];
285
+ const hazardLane = Math.random() < 0.5 ? 'top' : 'bottom';
286
+ const hazard = document.createElement('div');
287
+ hazard.className = 'kc-hazard';
288
+ hazard.dataset.lane = hazardLane;
289
+ hazard.dataset.kind = kind;
290
+ hazard.innerHTML = buildHazardSvg(kind);
291
+ const dur = clamp(HAZARD_BASE_MS / speedMul, HAZARD_FLOOR_MS, HAZARD_BASE_MS);
292
+ hazard.style.animationDuration = dur + 'ms';
293
+ trackEl.appendChild(hazard);
294
+ const entry = {
295
+ el: hazard, lane: hazardLane, kind: kind,
296
+ startedAt: performance.now(), durationMs: dur, scored: false,
297
+ };
298
+ hazards.push(entry);
299
+ hazard.addEventListener('animationend', function () {
300
+ if (!entry.scored && !over) {
301
+ entry.scored = true;
302
+ score++;
303
+ scoreSpan.textContent = String(score);
304
+ /* Smooth ramp: per dodge bump speed and spawn rate. The
305
+ clamp in spawn / drift duration formulas caps it. */
306
+ speedMul = clamp(speedMul + 0.05, 1, 2.6);
307
+ }
308
+ hazard.remove();
309
+ const idx = hazards.indexOf(entry);
310
+ if (idx >= 0) hazards.splice(idx, 1);
311
+ });
312
+ }
313
+
314
+ function scheduleSpawn() {
315
+ if (over) return;
316
+ const interval = clamp(SPAWN_INITIAL_MS / speedMul, SPAWN_FLOOR_MS, SPAWN_INITIAL_MS);
317
+ spawnTimer = setTimeout(function () {
318
+ spawnHazard();
319
+ scheduleSpawn();
320
+ }, interval);
321
+ }
322
+
323
+ function checkCollisions() {
324
+ if (over) return;
325
+ const trackRect = trackEl.getBoundingClientRect();
326
+ const playerRect = playerEl.getBoundingClientRect();
327
+ const playerCx = playerRect.left + playerRect.width / 2 - trackRect.left;
328
+ for (let i = 0; i < hazards.length; i++) {
329
+ const h = hazards[i];
330
+ if (h.scored) continue;
331
+ if (h.lane !== lane) continue;
332
+ const hr = h.el.getBoundingClientRect();
333
+ const hazardCx = hr.left + hr.width / 2 - trackRect.left;
334
+ /* Use the hazard's actual width as the X tolerance so a tram
335
+ is harder to dodge late than a bike — generous gameplay
336
+ while still rewarding swap timing. */
337
+ const tol = Math.max(PLAYER_COLLISION_X_TOL, hr.width / 2 + 6);
338
+ if (Math.abs(hazardCx - playerCx) < tol) {
339
+ gameOver();
340
+ return;
341
+ }
342
+ }
343
+ rafId = requestAnimationFrame(checkCollisions);
344
+ }
345
+
346
+ function gameOver() {
347
+ if (over) return;
348
+ over = true;
349
+ if (spawnTimer) { clearTimeout(spawnTimer); spawnTimer = null; }
350
+ if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
351
+ hazards.forEach(function (h) { h.el.style.animationPlayState = 'paused'; });
352
+ playerEl.classList.add('crashed');
353
+ const elapsedSec = Math.round((performance.now() - startTime) / 1000);
354
+ /* Fire on window so the GameModal picks it up. The modal handles
355
+ the win/loss copy + replay button. */
356
+ window.dispatchEvent(new CustomEvent('connext:gameend', {
357
+ detail: {
358
+ id: 'kade-cyclist',
359
+ won: false,
360
+ score: score,
361
+ summary: score + ' dodges · ' + elapsedSec + 's',
362
+ title: 'Bots.',
363
+ subtitle: 'You crashed into a vehicle on the kade. Try again — start slow, watch the lane.',
364
+ }
365
+ }));
366
+ }
367
+
368
+ function tearDown() {
369
+ tearDownActive(root, kade);
370
+ }
371
+
372
+ function onKey(e) {
373
+ if (e.key === 'Escape') { tearDown(); return; }
374
+ if (over) return;
375
+ if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') { setLane('top'); e.preventDefault(); }
376
+ else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') { setLane('bottom'); e.preventDefault(); }
377
+ }
378
+ document.addEventListener('keydown', onKey);
379
+ closeBtn.addEventListener('click', tearDown);
380
+
381
+ ACTIVE.set(root, {
382
+ ambientHTML: ambientHTML,
383
+ stage: stage,
384
+ onKey: onKey,
385
+ cleanup: function () {
386
+ if (spawnTimer) clearTimeout(spawnTimer);
387
+ if (rafId) cancelAnimationFrame(rafId);
388
+ over = true;
389
+ },
390
+ });
391
+
392
+ if (reduceMotion) speedMul = 1.4;
393
+
394
+ /* Start the round. */
395
+ scheduleSpawn();
396
+ rafId = requestAnimationFrame(checkCollisions);
397
+ }
398
+
399
+ function tearDownActive(root, kade) {
400
+ const state = ACTIVE.get(root);
401
+ if (!state) return;
402
+ if (state.cleanup) state.cleanup();
403
+ if (state.onKey) document.removeEventListener('keydown', state.onKey);
404
+ if (state.stage && state.stage.parentNode) state.stage.parentNode.removeChild(state.stage);
405
+ /* Restore the ambient kade items. */
406
+ const kadeItems = kade.querySelector('.kade-items');
407
+ if (kadeItems && state.ambientHTML) kadeItems.innerHTML = state.ambientHTML;
408
+ delete root.dataset.kadeActive;
409
+ ACTIVE.delete(root);
410
+ }
411
+
412
+ window.KadeCyclist = window.KadeCyclist || {};
413
+ window.KadeCyclist.hydrate = hydrate;
414
+
415
+ if (document.readyState === 'loading') {
416
+ document.addEventListener('DOMContentLoaded', hydrate);
417
+ } else {
418
+ hydrate();
419
+ }
420
+ })();
@@ -0,0 +1,219 @@
1
+ /* Logo Memory in-place styles. Pairs with logo-memory.js.
2
+ The runtime uses the actual <a class="hex"> anchors that already
3
+ render in the marquee — no overlay div, no separately-built tiles.
4
+ This CSS handles the visual phases:
5
+ 1. .marquee.lm-active pause tracks + restore colour
6
+ 2. .marquee.lm-overflow let the 6-row cluster extend below
7
+ the marquee's original 3-row band
8
+ 3. .lm-tile / .lm-flipper 3D rotateY flip (front=logo, back=
9
+ cobalt + Conduction C-in-hex)
10
+ 4. .lm-back-up flipped to back (the face-down state
11
+ before the player reveals)
12
+ 5. .lm-matched orange ring + pulse on a matched pair
13
+ 6. .lm-hud score pill above the cluster
14
+ */
15
+
16
+ /* ============ Phase 1+2: pause + colourise + overflow ============ */
17
+
18
+ .marquee.lm-active .track,
19
+ [data-memory-marquee].lm-active .track,
20
+ [data-memory-marquee].lm-active > div > div {
21
+ animation-play-state: paused !important;
22
+ }
23
+ [data-memory-marquee].lm-active a img {
24
+ filter: none !important;
25
+ opacity: 1 !important;
26
+ transition: filter 220ms ease, opacity 220ms ease;
27
+ }
28
+
29
+ .marquee.lm-overflow,
30
+ [data-memory-marquee].lm-overflow {
31
+ overflow: visible !important;
32
+ -webkit-mask-image: none !important;
33
+ mask-image: none !important;
34
+ min-height: 760px;
35
+ position: relative;
36
+ }
37
+
38
+ /* ============ Phase 3: tile flip structure ============ */
39
+
40
+ .lm-tile {
41
+ perspective: 900px;
42
+ cursor: pointer;
43
+ }
44
+ /* The runtime moves the original img into .lm-front during enhance,
45
+ so the tile's only direct children are the .lm-flipper. The hex's
46
+ ::before (cobalt-50 ring) + ::after (white inner) still render
47
+ behind the flipper as the resting frame. */
48
+
49
+ .lm-flipper {
50
+ position: absolute;
51
+ inset: 0;
52
+ transform-style: preserve-3d;
53
+ transition: transform 600ms cubic-bezier(0.65, 0, 0.35, 1);
54
+ z-index: 3;
55
+ }
56
+ .lm-tile.lm-back-up .lm-flipper {
57
+ transform: rotateY(180deg);
58
+ }
59
+
60
+ .lm-face {
61
+ position: absolute;
62
+ inset: 0;
63
+ display: grid;
64
+ place-items: center;
65
+ -webkit-backface-visibility: hidden;
66
+ backface-visibility: hidden;
67
+ clip-path: var(--hex-pointy-top, polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%));
68
+ }
69
+
70
+ /* Front: transparent so the existing .hex::before (cobalt-50 ring) +
71
+ .hex::after (white interior) keep showing through. The img sits on
72
+ top. */
73
+ .lm-front {
74
+ background: transparent;
75
+ }
76
+ .lm-front img {
77
+ max-width: 60%;
78
+ max-height: 50%;
79
+ object-fit: contain;
80
+ filter: none;
81
+ opacity: 1;
82
+ transition: opacity 220ms ease;
83
+ }
84
+
85
+ /* Back: solid brand cobalt with the Conduction C-in-hex avatar. No
86
+ inset frame — the hex clip-path of .lm-face already shapes the
87
+ tile, and a darker outer ring read as a black border. */
88
+ .lm-back {
89
+ transform: rotateY(180deg);
90
+ background: var(--c-cobalt-500, #21468B);
91
+ }
92
+ .lm-back-svg {
93
+ width: 56%;
94
+ height: 56%;
95
+ }
96
+
97
+ /* ============ Matched state ============ */
98
+
99
+ /* Replace the cobalt-50 outer ring with KNVB orange when matched.
100
+ Inner white (::after, 1.5px inset) still covers the interior, so
101
+ the visible orange is just the rim — the same shape as the rest
102
+ of the hex pattern, no layout shift. */
103
+ .lm-tile.lm-matched::before {
104
+ background: var(--c-orange-knvb, #F77F0E) !important;
105
+ }
106
+ .lm-tile.lm-matched {
107
+ animation: lmMatchPulse 700ms cubic-bezier(0.34, 1.56, 0.64, 1) 1;
108
+ }
109
+ @keyframes lmMatchPulse {
110
+ 0% { transform: translateY(0) scale(1); }
111
+ 35% { transform: translateY(0) scale(1.08); }
112
+ 100% { transform: translateY(0) scale(1); }
113
+ }
114
+
115
+ /* Hover hint while face-down: NEVER touch the flipper's transform.
116
+ Animating between rotateY(180deg) and rotateY(180deg) <translate>
117
+ triggers quaternion-decomposition ambiguity at the 180° pole, and
118
+ browsers sometimes interpolate through rotateY(0) — which would
119
+ reveal the front face on hover and let the player cheat the game.
120
+ Use filter only; the tile is only "pushed" face-up via the click
121
+ handler, never via hover or focus. */
122
+ .lm-tile.lm-back-up:not(.lm-matched):hover {
123
+ filter: drop-shadow(0 4px 8px rgba(33, 70, 139, 0.28));
124
+ }
125
+ .lm-tile.lm-back-up:focus-visible {
126
+ outline: 2px solid var(--c-orange-knvb, #F77F0E);
127
+ outline-offset: 4px;
128
+ }
129
+
130
+ /* ============ Phase 6: HUD ============ */
131
+
132
+ .lm-hud {
133
+ position: absolute;
134
+ top: 12px;
135
+ left: 50%;
136
+ transform: translateX(-50%);
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 10px;
140
+ z-index: 20;
141
+ font-family: var(--conduction-typography-font-family-body, 'Figtree', system-ui, sans-serif);
142
+ pointer-events: auto;
143
+ animation: lmHudIn 360ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
144
+ }
145
+ @keyframes lmHudIn {
146
+ from { opacity: 0; transform: translateX(-50%) translateY(-12px); }
147
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
148
+ }
149
+
150
+ .lm-counter,
151
+ .lm-moves {
152
+ background: var(--c-cobalt-900, #0A172F);
153
+ color: white;
154
+ padding: 8px 16px;
155
+ border-radius: 999px;
156
+ font-size: 13px;
157
+ font-weight: 600;
158
+ letter-spacing: 0.02em;
159
+ }
160
+ .lm-counter [data-matched] {
161
+ color: var(--c-orange-knvb, #F77F0E);
162
+ font-weight: 700;
163
+ margin-right: 2px;
164
+ }
165
+ .lm-moves [data-moves] {
166
+ font-weight: 700;
167
+ margin-right: 2px;
168
+ }
169
+
170
+ .lm-close {
171
+ width: 36px;
172
+ height: 36px;
173
+ border-radius: 50%;
174
+ border: 1px solid var(--c-cobalt-900, #0A172F);
175
+ background: white;
176
+ color: var(--c-cobalt-900, #0A172F);
177
+ font-size: 22px;
178
+ line-height: 1;
179
+ cursor: pointer;
180
+ display: grid;
181
+ place-items: center;
182
+ font-family: inherit;
183
+ transition: background 150ms ease, color 150ms ease, transform 150ms ease;
184
+ }
185
+ .lm-close:hover,
186
+ .lm-close:focus-visible {
187
+ background: var(--c-cobalt-900, #0A172F);
188
+ color: white;
189
+ transform: scale(1.05);
190
+ }
191
+
192
+ /* ============ Reduced motion ============ */
193
+
194
+ @media (prefers-reduced-motion: reduce) {
195
+ .lm-flipper,
196
+ .lm-tile.lm-matched,
197
+ .lm-hud,
198
+ .marquee.lm-active .hexLogo,
199
+ .marquee.lm-active a.hex img,
200
+ [data-memory-marquee].lm-active .hexLogo,
201
+ [data-memory-marquee].lm-active a.hex img {
202
+ transition: none !important;
203
+ animation: none !important;
204
+ }
205
+ }
206
+
207
+ /* ============ Responsive: keep the cluster legible on small screens ============ */
208
+
209
+ @media (max-width: 720px) {
210
+ .marquee.lm-overflow,
211
+ [data-memory-marquee].lm-overflow {
212
+ min-height: 600px;
213
+ }
214
+ .lm-counter,
215
+ .lm-moves {
216
+ padding: 6px 12px;
217
+ font-size: 12px;
218
+ }
219
+ }