@humanforest/nuxt-layer 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 (324) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +208 -0
  3. package/app.config.ts +10 -0
  4. package/assets/icons/forest/bike-asterisk.svg +21 -0
  5. package/assets/icons/forest/bike-down.svg +19 -0
  6. package/assets/icons/forest/bike-off.svg +20 -0
  7. package/assets/icons/forest/bike-up.svg +19 -0
  8. package/assets/icons/forest/bike-x.svg +19 -0
  9. package/assets/icons/forest/bike.svg +18 -0
  10. package/assets/icons/forest/mark-fill.svg +6 -0
  11. package/assets/icons/forest/mark.svg +6 -0
  12. package/assets/icons/forest/parking.svg +3 -0
  13. package/assets/icons/forest/star-fill.svg +4 -0
  14. package/assets/icons/forest/star.svg +4 -0
  15. package/assets/icons/forest/traffic-light-caution.svg +8 -0
  16. package/assets/icons/forest/traffic-light-go.svg +8 -0
  17. package/assets/icons/forest/traffic-light-lit.svg +8 -0
  18. package/assets/icons/forest/traffic-light-stop.svg +8 -0
  19. package/assets/icons/forest/traffic-light.svg +8 -0
  20. package/nuxt.config.ts +81 -0
  21. package/package.json +35 -0
  22. package/packages/charts/src/FBoroughShape.vue +52 -0
  23. package/packages/charts/src/FCalendarGrid.vue +332 -0
  24. package/packages/charts/src/FCellLegend.vue +54 -0
  25. package/packages/charts/src/FChartFrame.vue +201 -0
  26. package/packages/charts/src/FChartFrameSingle.vue +91 -0
  27. package/packages/charts/src/FChartLegend.vue +26 -0
  28. package/packages/charts/src/FDistributionBar.vue +224 -0
  29. package/packages/charts/src/FDottedMap.vue +335 -0
  30. package/packages/charts/src/FLondonMap.vue +399 -0
  31. package/packages/charts/src/FSparkline.vue +198 -0
  32. package/packages/charts/src/FStatusTrack.vue +151 -0
  33. package/packages/charts/src/cellSize.ts +46 -0
  34. package/packages/charts/src/distribution.ts +99 -0
  35. package/packages/charts/src/engine.ts +15 -0
  36. package/packages/charts/src/forestTooltip.ts +110 -0
  37. package/packages/charts/src/fromCategories.ts +43 -0
  38. package/packages/charts/src/index.ts +87 -0
  39. package/packages/charts/src/londonAreas.ts +75 -0
  40. package/packages/charts/src/londonGrid.ts +67 -0
  41. package/packages/charts/src/motionDuration.ts +49 -0
  42. package/packages/charts/src/presets.ts +143 -0
  43. package/packages/charts/src/resolveVar.ts +21 -0
  44. package/packages/charts/src/snapTooltip.ts +154 -0
  45. package/packages/charts/src/tooltipStandIn.ts +58 -0
  46. package/packages/charts/src/useCellScale.ts +155 -0
  47. package/packages/charts/src/useChartPalette.ts +61 -0
  48. package/packages/charts/src/useChartRepaintKey.ts +22 -0
  49. package/packages/charts/src/useThemeVersion.ts +17 -0
  50. package/packages/frames/src/FBrowserFrame.vue +53 -0
  51. package/packages/frames/src/FDeviceFrame.vue +242 -0
  52. package/packages/frames/src/FFitFrame.vue +83 -0
  53. package/packages/frames/src/art/iphone-11.svg +124 -0
  54. package/packages/frames/src/art/iphone-12.svg +601 -0
  55. package/packages/frames/src/art/iphone-13-pro.svg +820 -0
  56. package/packages/frames/src/art/iphone-13.svg +849 -0
  57. package/packages/frames/src/art/iphone-14.svg +681 -0
  58. package/packages/frames/src/art/iphone-15-pro.svg +633 -0
  59. package/packages/frames/src/art/iphone-15.svg +713 -0
  60. package/packages/frames/src/art/iphone-16-plus.svg +685 -0
  61. package/packages/frames/src/art/iphone-16-pro-max.svg +611 -0
  62. package/packages/frames/src/art/iphone-16-pro.svg +631 -0
  63. package/packages/frames/src/art/iphone-16.svg +713 -0
  64. package/packages/frames/src/art/iphone-17-pro-max.svg +355 -0
  65. package/packages/frames/src/art/iphone-17.svg +305 -0
  66. package/packages/frames/src/art/iphone-air.svg +389 -0
  67. package/packages/frames/src/art/iphone-se-3.svg +473 -0
  68. package/packages/frames/src/art/iphone-x.svg +652 -0
  69. package/packages/frames/src/art/pixel-10-pro-xl.svg +32 -0
  70. package/packages/frames/src/art/pixel-10-pro.svg +32 -0
  71. package/packages/frames/src/art/pixel-10.svg +32 -0
  72. package/packages/frames/src/art/pixel-8-pro.svg +208 -0
  73. package/packages/frames/src/art/pixel-8.svg +208 -0
  74. package/packages/frames/src/art/pixel-8a.svg +8 -0
  75. package/packages/frames/src/art/pixel-9-pro-xl.svg +32 -0
  76. package/packages/frames/src/art/pixel-9-pro.svg +34 -0
  77. package/packages/frames/src/art/pixel-9a.svg +25 -0
  78. package/packages/frames/src/art.ts +29 -0
  79. package/packages/frames/src/devices.ts +337 -0
  80. package/packages/frames/src/index.ts +15 -0
  81. package/packages/tokens/geo/london-boroughs.json +1 -0
  82. package/packages/tokens/geo/london-dots-coarse.json +1 -0
  83. package/packages/tokens/geo/london-dots-fine.json +1 -0
  84. package/packages/tokens/geo/london-dots-medium.json +1 -0
  85. package/packages/tokens/geo/london-dots-ultra.json +1 -0
  86. package/packages/tokens/geo/london-wards.json +1 -0
  87. package/packages/tokens/glyphs/glyphs.json +1 -0
  88. package/packages/tokens/logos/f-rot.svg +4 -0
  89. package/packages/tokens/logos/f.svg +4 -0
  90. package/packages/tokens/logos/favicon-c2w.svg +9 -0
  91. package/packages/tokens/logos/favicon-console.svg +9 -0
  92. package/packages/tokens/logos/favicon-ds.svg +9 -0
  93. package/packages/tokens/logos/favicon-fleet.svg +9 -0
  94. package/packages/tokens/logos/favicon-flex.svg +9 -0
  95. package/packages/tokens/logos/favicon-plus.svg +9 -0
  96. package/packages/tokens/logos/favicon-radar.svg +9 -0
  97. package/packages/tokens/logos/favicon-tower.svg +9 -0
  98. package/packages/tokens/logos/lockup-horizontal.svg +17 -0
  99. package/packages/tokens/logos/lockup-vertical.svg +17 -0
  100. package/packages/tokens/logos/mark-mono.svg +4 -0
  101. package/packages/tokens/logos/mark.svg +7 -0
  102. package/packages/tokens/logos/wordmark.svg +9 -0
  103. package/packages/tokens/src/colourEngine.ts +604 -0
  104. package/packages/tokens/src/glyphs.ts +127 -0
  105. package/packages/tokens/src/logo.ts +50 -0
  106. package/packages/tokens/src/subbrands.ts +44 -0
  107. package/packages/tokens/src/sublogo.ts +80 -0
  108. package/packages/ui/icons/forest/map/forest-place-count.svg +21 -0
  109. package/packages/ui/icons/forest/map/forest-place-marker.svg +17 -0
  110. package/packages/ui/icons/forest/map/forest-vehicle-marker.svg +73 -0
  111. package/packages/ui/icons/forest/map/manifest.json +373 -0
  112. package/packages/ui/icons/forest/map/place-count-manifest.json +83 -0
  113. package/packages/ui/icons/forest/map/place-manifest.json +74 -0
  114. package/packages/ui/src/address/address.ts +125 -0
  115. package/packages/ui/src/address/autofill.ts +78 -0
  116. package/packages/ui/src/address/geocode.ts +55 -0
  117. package/packages/ui/src/address/index.ts +31 -0
  118. package/packages/ui/src/address/validate.ts +91 -0
  119. package/packages/ui/src/canvas/FCanvasCard.vue +97 -0
  120. package/packages/ui/src/canvas/FCanvasControl.vue +121 -0
  121. package/packages/ui/src/canvas/FCanvasControlGroup.vue +100 -0
  122. package/packages/ui/src/canvas/FCanvasFlyout.vue +216 -0
  123. package/packages/ui/src/canvas/FCanvasHud.vue +402 -0
  124. package/packages/ui/src/canvas/canvasSurface.ts +39 -0
  125. package/packages/ui/src/canvas/index.ts +17 -0
  126. package/packages/ui/src/contexts/ForestContext.vue +53 -0
  127. package/packages/ui/src/contexts/contexts.ts +153 -0
  128. package/packages/ui/src/contexts/index.ts +23 -0
  129. package/packages/ui/src/contexts/marketing-theme.ts +214 -0
  130. package/packages/ui/src/contexts/mobile-theme.ts +402 -0
  131. package/packages/ui/src/filter/FFilterBar.vue +72 -0
  132. package/packages/ui/src/filter/FFilterChip.vue +362 -0
  133. package/packages/ui/src/filter/FFilterClauses.vue +104 -0
  134. package/packages/ui/src/filter/FFilterTrigger.vue +183 -0
  135. package/packages/ui/src/filter/filter.theme.ts +27 -0
  136. package/packages/ui/src/filter/filterClause.ts +265 -0
  137. package/packages/ui/src/filter/filterPredicate.ts +134 -0
  138. package/packages/ui/src/filter/filterSchema.ts +82 -0
  139. package/packages/ui/src/filter/index.ts +36 -0
  140. package/packages/ui/src/forest-preset.ts +68 -0
  141. package/packages/ui/src/icons.ts +79 -0
  142. package/packages/ui/src/index.ts +17 -0
  143. package/packages/ui/src/kpi/FKpi.vue +253 -0
  144. package/packages/ui/src/kpi/FKpiGroup.vue +161 -0
  145. package/packages/ui/src/kpi/index.ts +20 -0
  146. package/packages/ui/src/kpi/kpi.theme.ts +168 -0
  147. package/packages/ui/src/kpi/kpiDelta.ts +82 -0
  148. package/packages/ui/src/kpi/kpiFormat.ts +72 -0
  149. package/packages/ui/src/kpi/kpiGroupState.ts +63 -0
  150. package/packages/ui/src/logo/FLogo.vue +52 -0
  151. package/packages/ui/src/logo/FSubLogo.vue +214 -0
  152. package/packages/ui/src/logo/index.ts +8 -0
  153. package/packages/ui/src/map/FMap.vue +355 -0
  154. package/packages/ui/src/map/FMapCompass.vue +154 -0
  155. package/packages/ui/src/map/FMapCoordinates.vue +134 -0
  156. package/packages/ui/src/map/FMapDrawTools.vue +116 -0
  157. package/packages/ui/src/map/FMapFullscreen.vue +92 -0
  158. package/packages/ui/src/map/FMapGeolocate.vue +208 -0
  159. package/packages/ui/src/map/FMapLayers.vue +75 -0
  160. package/packages/ui/src/map/FMapLegend.vue +45 -0
  161. package/packages/ui/src/map/FMapPopup.vue +129 -0
  162. package/packages/ui/src/map/FMapRamp.vue +69 -0
  163. package/packages/ui/src/map/FMapRegion.vue +77 -0
  164. package/packages/ui/src/map/FMapScale.vue +107 -0
  165. package/packages/ui/src/map/FMapSearch.vue +362 -0
  166. package/packages/ui/src/map/FMapTilt.vue +87 -0
  167. package/packages/ui/src/map/FMapTools.vue +61 -0
  168. package/packages/ui/src/map/FMapZoom.vue +81 -0
  169. package/packages/ui/src/map/FMarker.vue +218 -0
  170. package/packages/ui/src/map/FTrackScrubber.vue +94 -0
  171. package/packages/ui/src/map/FVehicleLegend.vue +72 -0
  172. package/packages/ui/src/map/FVehicleMark.vue +166 -0
  173. package/packages/ui/src/map/bin.ts +196 -0
  174. package/packages/ui/src/map/coordinates.ts +83 -0
  175. package/packages/ui/src/map/density.ts +401 -0
  176. package/packages/ui/src/map/draw.ts +356 -0
  177. package/packages/ui/src/map/ease.ts +67 -0
  178. package/packages/ui/src/map/fullscreen.ts +59 -0
  179. package/packages/ui/src/map/heat.ts +160 -0
  180. package/packages/ui/src/map/index.ts +351 -0
  181. package/packages/ui/src/map/liveMarkers.ts +60 -0
  182. package/packages/ui/src/map/map.css +116 -0
  183. package/packages/ui/src/map/mapColor.ts +31 -0
  184. package/packages/ui/src/map/mapContext.ts +12 -0
  185. package/packages/ui/src/map/mapLayers.ts +55 -0
  186. package/packages/ui/src/map/mapLegend.ts +70 -0
  187. package/packages/ui/src/map/mapSearch.theme.ts +79 -0
  188. package/packages/ui/src/map/mapSearch.ts +279 -0
  189. package/packages/ui/src/map/mapUrl.ts +117 -0
  190. package/packages/ui/src/map/markerAsset.ts +3 -0
  191. package/packages/ui/src/map/padding.ts +39 -0
  192. package/packages/ui/src/map/place.ts +192 -0
  193. package/packages/ui/src/map/ramp.ts +95 -0
  194. package/packages/ui/src/map/regionSelect.ts +163 -0
  195. package/packages/ui/src/map/route.ts +167 -0
  196. package/packages/ui/src/map/scale.ts +329 -0
  197. package/packages/ui/src/map/scaleBar.ts +67 -0
  198. package/packages/ui/src/map/selectionModel.ts +50 -0
  199. package/packages/ui/src/map/sources/coordinates.ts +58 -0
  200. package/packages/ui/src/map/sources/mapboxPlaces.ts +71 -0
  201. package/packages/ui/src/map/style.ts +14 -0
  202. package/packages/ui/src/map/track.ts +279 -0
  203. package/packages/ui/src/map/useFeatureSelect.ts +230 -0
  204. package/packages/ui/src/map/useLiveMarkers.ts +84 -0
  205. package/packages/ui/src/map/useMap.ts +150 -0
  206. package/packages/ui/src/map/useMapDraw.ts +776 -0
  207. package/packages/ui/src/map/useMapOverlay.ts +421 -0
  208. package/packages/ui/src/map/useMapTableSelection.ts +101 -0
  209. package/packages/ui/src/map/useMapUrlState.ts +155 -0
  210. package/packages/ui/src/map/useMarkerSource.ts +163 -0
  211. package/packages/ui/src/map/useRegionSelect.ts +309 -0
  212. package/packages/ui/src/map/useTokenMode.ts +61 -0
  213. package/packages/ui/src/map/useTrackPlayback.ts +131 -0
  214. package/packages/ui/src/map/useVehicleFilter.ts +51 -0
  215. package/packages/ui/src/map/useVisibleFeatures.ts +124 -0
  216. package/packages/ui/src/map/vehicle.ts +807 -0
  217. package/packages/ui/src/map/vehicleFilter.ts +284 -0
  218. package/packages/ui/src/map/vehicleLegend.ts +155 -0
  219. package/packages/ui/src/map/visibleFeatures.ts +192 -0
  220. package/packages/ui/src/map/zone.ts +291 -0
  221. package/packages/ui/src/shell/FAppNavbar.vue +68 -0
  222. package/packages/ui/src/shell/FAppShell.vue +163 -0
  223. package/packages/ui/src/shell/FAppSidebar.vue +215 -0
  224. package/packages/ui/src/shell/FPageToolbar.vue +126 -0
  225. package/packages/ui/src/shell/FScrollShadow.vue +43 -0
  226. package/packages/ui/src/shell/FSidePanel.vue +258 -0
  227. package/packages/ui/src/shell/FToolbarActions.vue +32 -0
  228. package/packages/ui/src/shell/index.ts +33 -0
  229. package/packages/ui/src/shell/nav.ts +37 -0
  230. package/packages/ui/src/shell/pageToolbar.ts +69 -0
  231. package/packages/ui/src/shell/shell.ts +74 -0
  232. package/packages/ui/src/shell/sidePanel.ts +115 -0
  233. package/packages/ui/src/themes/accordion.ts +36 -0
  234. package/packages/ui/src/themes/alert.ts +16 -0
  235. package/packages/ui/src/themes/auth-form.ts +19 -0
  236. package/packages/ui/src/themes/avatar-group.ts +9 -0
  237. package/packages/ui/src/themes/avatar.ts +18 -0
  238. package/packages/ui/src/themes/badge.ts +51 -0
  239. package/packages/ui/src/themes/banner.ts +21 -0
  240. package/packages/ui/src/themes/breadcrumb.ts +10 -0
  241. package/packages/ui/src/themes/button.mechanics.ts +191 -0
  242. package/packages/ui/src/themes/button.ts +136 -0
  243. package/packages/ui/src/themes/calendar.ts +29 -0
  244. package/packages/ui/src/themes/card.ts +67 -0
  245. package/packages/ui/src/themes/carousel.ts +41 -0
  246. package/packages/ui/src/themes/checkbox-group.ts +20 -0
  247. package/packages/ui/src/themes/checkbox.ts +47 -0
  248. package/packages/ui/src/themes/chip.ts +50 -0
  249. package/packages/ui/src/themes/collapsible.ts +26 -0
  250. package/packages/ui/src/themes/color-picker.ts +13 -0
  251. package/packages/ui/src/themes/command-palette.ts +37 -0
  252. package/packages/ui/src/themes/container.ts +14 -0
  253. package/packages/ui/src/themes/context-menu.ts +38 -0
  254. package/packages/ui/src/themes/dashboard-group.ts +11 -0
  255. package/packages/ui/src/themes/dashboard-navbar.ts +10 -0
  256. package/packages/ui/src/themes/dashboard-panel.ts +18 -0
  257. package/packages/ui/src/themes/dashboard-resize-handle.ts +35 -0
  258. package/packages/ui/src/themes/dashboard-sidebar-collapse.ts +7 -0
  259. package/packages/ui/src/themes/dashboard-sidebar-toggle.ts +7 -0
  260. package/packages/ui/src/themes/dashboard-sidebar.ts +58 -0
  261. package/packages/ui/src/themes/dashboard-toolbar.ts +15 -0
  262. package/packages/ui/src/themes/drawer.ts +45 -0
  263. package/packages/ui/src/themes/dropdown-menu.ts +38 -0
  264. package/packages/ui/src/themes/empty.ts +43 -0
  265. package/packages/ui/src/themes/field-group.ts +52 -0
  266. package/packages/ui/src/themes/file-upload.ts +38 -0
  267. package/packages/ui/src/themes/footer-columns.ts +13 -0
  268. package/packages/ui/src/themes/footer.ts +9 -0
  269. package/packages/ui/src/themes/form-field.ts +25 -0
  270. package/packages/ui/src/themes/form.ts +22 -0
  271. package/packages/ui/src/themes/header.ts +12 -0
  272. package/packages/ui/src/themes/icon.ts +61 -0
  273. package/packages/ui/src/themes/index.ts +99 -0
  274. package/packages/ui/src/themes/input-date.ts +73 -0
  275. package/packages/ui/src/themes/input-menu.ts +66 -0
  276. package/packages/ui/src/themes/input-number.ts +58 -0
  277. package/packages/ui/src/themes/input-rating.ts +25 -0
  278. package/packages/ui/src/themes/input-tags.ts +63 -0
  279. package/packages/ui/src/themes/input-time.ts +70 -0
  280. package/packages/ui/src/themes/input.ts +148 -0
  281. package/packages/ui/src/themes/kbd.ts +27 -0
  282. package/packages/ui/src/themes/link.ts +27 -0
  283. package/packages/ui/src/themes/listbox.ts +48 -0
  284. package/packages/ui/src/themes/main.ts +6 -0
  285. package/packages/ui/src/themes/marquee.ts +45 -0
  286. package/packages/ui/src/themes/modal.ts +46 -0
  287. package/packages/ui/src/themes/navigation-menu.ts +80 -0
  288. package/packages/ui/src/themes/page-anchors.ts +7 -0
  289. package/packages/ui/src/themes/page-aside.ts +7 -0
  290. package/packages/ui/src/themes/page-card.ts +22 -0
  291. package/packages/ui/src/themes/page-cta.ts +18 -0
  292. package/packages/ui/src/themes/page-feature.ts +13 -0
  293. package/packages/ui/src/themes/page-header.ts +16 -0
  294. package/packages/ui/src/themes/page-hero.ts +15 -0
  295. package/packages/ui/src/themes/page-links.ts +12 -0
  296. package/packages/ui/src/themes/page-section.ts +14 -0
  297. package/packages/ui/src/themes/page.ts +7 -0
  298. package/packages/ui/src/themes/pagination.ts +17 -0
  299. package/packages/ui/src/themes/pin-input.ts +73 -0
  300. package/packages/ui/src/themes/popover.ts +30 -0
  301. package/packages/ui/src/themes/progress.ts +29 -0
  302. package/packages/ui/src/themes/prose.ts +45 -0
  303. package/packages/ui/src/themes/radio-group.ts +39 -0
  304. package/packages/ui/src/themes/scroll-area.ts +23 -0
  305. package/packages/ui/src/themes/select-menu.ts +71 -0
  306. package/packages/ui/src/themes/select.ts +72 -0
  307. package/packages/ui/src/themes/separator.ts +28 -0
  308. package/packages/ui/src/themes/sidebar.ts +127 -0
  309. package/packages/ui/src/themes/skeleton.ts +15 -0
  310. package/packages/ui/src/themes/slideover.ts +87 -0
  311. package/packages/ui/src/themes/slider.ts +48 -0
  312. package/packages/ui/src/themes/stepper.ts +41 -0
  313. package/packages/ui/src/themes/switch.ts +63 -0
  314. package/packages/ui/src/themes/table.ts +34 -0
  315. package/packages/ui/src/themes/tabs.ts +74 -0
  316. package/packages/ui/src/themes/textarea.ts +77 -0
  317. package/packages/ui/src/themes/timeline.ts +25 -0
  318. package/packages/ui/src/themes/toast.ts +29 -0
  319. package/packages/ui/src/themes/toaster.ts +66 -0
  320. package/packages/ui/src/themes/tooltip.ts +31 -0
  321. package/packages/ui/src/themes/tree.ts +41 -0
  322. package/packages/ui/src/themes/user.ts +28 -0
  323. package/styles.cdn.gen.css +2047 -0
  324. package/theme.gen.json +1 -0
@@ -0,0 +1,2047 @@
1
+ /* GENERATED by scripts/layer.ts — do not edit. Run `bun run build:layer`.
2
+ Fonts from the CDN. The build npm ships — import as @humanforest/nuxt-layer/styles.cdn.gen.css. */
3
+
4
+ /* Forest brand fonts — CDN build (absolute urls — host must send Access-Control-Allow-Origin).
5
+ GENERATED by scripts/fonts.ts from packages/fonts/files/*. Do not edit by hand.
6
+ Licence: Mohr = Black + UPPERCASE only; GT Haptik unrestricted; JetBrains Mono OFL. */
7
+
8
+ /* Mohr's caps sit above its own box centre; these re-hang the face so the box matches
9
+ GT Haptik's (ascent + descent) and the cap band lands centred (ascent − descent = cap height). */
10
+ @font-face {
11
+ font-family: 'MohrAlt';
12
+ src: url('https://assets.forest.bike/fonts/MohrAlt-Black.woff2') format('woff2'),
13
+ url('https://assets.forest.bike/fonts/MohrAlt-Black.woff') format('woff');
14
+ font-weight: 900;
15
+ font-style: normal;
16
+ ascent-override: 98.7549%;
17
+ descent-override: 26.7334%;
18
+ line-gap-override: 0%;
19
+ font-display: swap;
20
+ }
21
+
22
+ /* Mohr's caps sit above its own box centre; these re-hang the face so the box matches
23
+ GT Haptik's (ascent + descent) and the cap band lands centred (ascent − descent = cap height). */
24
+ @font-face {
25
+ font-family: 'Mohr-BlackIt';
26
+ src: url('https://assets.forest.bike/fonts/Mohr-BlackIt.woff2') format('woff2'),
27
+ url('https://assets.forest.bike/fonts/Mohr-BlackIt.woff') format('woff');
28
+ font-weight: 900;
29
+ font-style: italic;
30
+ ascent-override: 98.7549%;
31
+ descent-override: 26.7334%;
32
+ line-gap-override: 0%;
33
+ font-display: swap;
34
+ }
35
+
36
+ @font-face {
37
+ font-family: 'GT Haptik';
38
+ src: url('https://assets.forest.bike/fonts/GTHaptik-Regular.woff2') format('woff2'),
39
+ url('https://assets.forest.bike/fonts/GTHaptik-Regular.woff') format('woff');
40
+ font-weight: normal;
41
+ font-style: normal;
42
+ font-display: swap;
43
+ }
44
+
45
+ @font-face {
46
+ font-family: 'GT Haptik';
47
+ src: url('https://assets.forest.bike/fonts/GTHaptik-RegularOblique.woff2') format('woff2'),
48
+ url('https://assets.forest.bike/fonts/GTHaptik-RegularOblique.woff') format('woff');
49
+ font-weight: normal;
50
+ font-style: italic;
51
+ font-display: swap;
52
+ }
53
+
54
+ @font-face {
55
+ font-family: 'GT Haptik';
56
+ src: url('https://assets.forest.bike/fonts/GTHaptik-Bold.woff2') format('woff2'),
57
+ url('https://assets.forest.bike/fonts/GTHaptik-Bold.woff') format('woff');
58
+ font-weight: bold;
59
+ font-style: normal;
60
+ font-display: swap;
61
+ }
62
+
63
+ @font-face {
64
+ font-family: 'GT Haptik';
65
+ src: url('https://assets.forest.bike/fonts/GTHaptik-BoldOblique.woff2') format('woff2'),
66
+ url('https://assets.forest.bike/fonts/GTHaptik-BoldOblique.woff') format('woff');
67
+ font-weight: bold;
68
+ font-style: italic;
69
+ font-display: swap;
70
+ }
71
+
72
+ @font-face {
73
+ font-family: 'JetBrains Mono';
74
+ src: url('https://assets.forest.bike/fonts/JetBrainsMono-Regular.woff2') format('woff2');
75
+ font-weight: 400;
76
+ font-style: normal;
77
+ font-display: swap;
78
+ }
79
+
80
+ @font-face {
81
+ font-family: 'JetBrains Mono';
82
+ src: url('https://assets.forest.bike/fonts/JetBrainsMono-Bold.woff2') format('woff2');
83
+ font-weight: 700;
84
+ font-style: normal;
85
+ font-display: swap;
86
+ }
87
+
88
+
89
+ /* Forest 50–950 ramps + named brand tokens — generated by scripts/scales.ts (OKLCH, v2 engine).
90
+ Byte-for-byte the docs' buildPCHIP output. Display-P3 primary + sRGB hex fallback. Brand naming
91
+ : forest / maple / river / warm / amber / acid / neutral. Additive to scales.css. Do not edit. */
92
+ :root {
93
+ --forest-0: #F4FDF7;
94
+ --forest-0: color(display-p3 0.9617 0.9904 0.9692);
95
+ --forest-50: #E8FBEE;
96
+ --forest-50: color(display-p3 0.9221 0.9812 0.9381);
97
+ --forest-100: #CEF5DD;
98
+ --forest-100: color(display-p3 0.8366 0.9550 0.8714);
99
+ --forest-200: #99E9BC;
100
+ --forest-200: color(display-p3 0.6692 0.9055 0.7508);
101
+ --forest-300: #3BD996;
102
+ --forest-300: color(display-p3 0.4365 0.8410 0.6100);
103
+ --forest-400: #0ABB7F;
104
+ --forest-400: color(display-p3 0.2782 0.7321 0.5042);
105
+ --forest-500: #019F6D;
106
+ --forest-500: color(display-p3 0.1537 0.6308 0.4214);
107
+ --forest-600: #00845C;
108
+ --forest-600: color(display-p3 0.2237 0.5096 0.3721);
109
+ --forest-700: #046C4C;
110
+ --forest-700: color(display-p3 0.1815 0.4169 0.3069);
111
+ --forest-800: #075640;
112
+ --forest-800: color(display-p3 0.1446 0.3337 0.2565);
113
+ --forest-900: #074235;
114
+ --forest-900: color(display-p3 0.1092 0.2548 0.2097);
115
+ --forest-950: #002A24;
116
+ --forest-950: color(display-p3 0.0519 0.1616 0.1416);
117
+ --maple-0: #FFF8F8;
118
+ --maple-0: color(display-p3 1.0000 0.9720 0.9730);
119
+ --maple-50: #FFF1F2;
120
+ --maple-50: color(display-p3 0.9985 0.9449 0.9465);
121
+ --maple-100: #FFE1E2;
122
+ --maple-100: color(display-p3 1.0000 0.8781 0.8806);
123
+ --maple-200: #FFC2C4;
124
+ --maple-200: color(display-p3 0.9973 0.7533 0.7546);
125
+ --maple-300: #FF9A9B;
126
+ --maple-300: color(display-p3 0.9961 0.5849 0.5840);
127
+ --maple-400: #F26F70;
128
+ --maple-400: color(display-p3 0.8993 0.4544 0.4449);
129
+ --maple-500: #D35A59;
130
+ --maple-500: color(display-p3 0.7916 0.3578 0.3444);
131
+ --maple-600: #AA514E;
132
+ --maple-600: color(display-p3 0.6214 0.3379 0.3175);
133
+ --maple-700: #8C4340;
134
+ --maple-700: color(display-p3 0.5112 0.2789 0.2593);
135
+ --maple-800: #6E3633;
136
+ --maple-800: color(display-p3 0.4027 0.2236 0.2061);
137
+ --maple-900: #532A27;
138
+ --maple-900: color(display-p3 0.3056 0.1743 0.1597);
139
+ --maple-950: #3B1C1A;
140
+ --maple-950: color(display-p3 0.2159 0.1166 0.1048);
141
+ --river-0: #F6FBFF;
142
+ --river-0: color(display-p3 0.9669 0.9829 1.0000);
143
+ --river-50: #EDF6FF;
144
+ --river-50: color(display-p3 0.9341 0.9657 1.0000);
145
+ --river-100: #D8ECFF;
146
+ --river-100: color(display-p3 0.8580 0.9250 1.0000);
147
+ --river-200: #B2D9FF;
148
+ --river-200: color(display-p3 0.7169 0.8475 1.0000);
149
+ --river-300: #81C0FF;
150
+ --river-300: color(display-p3 0.5424 0.7476 1.0000);
151
+ --river-400: #43A2FF;
152
+ --river-400: color(display-p3 0.3493 0.6273 0.9890);
153
+ --river-500: #3588DF;
154
+ --river-500: color(display-p3 0.2668 0.5237 0.8840);
155
+ --river-600: #3C72B2;
156
+ --river-600: color(display-p3 0.2865 0.4405 0.6806);
157
+ --river-700: #355D93;
158
+ --river-700: color(display-p3 0.2436 0.3601 0.5609);
159
+ --river-800: #2D4974;
160
+ --river-800: color(display-p3 0.2015 0.2837 0.4416);
161
+ --river-900: #253858;
162
+ --river-900: color(display-p3 0.1613 0.2166 0.3341);
163
+ --river-950: #19263E;
164
+ --river-950: color(display-p3 0.1098 0.1474 0.2378);
165
+ --warm-0: #FDFAF6;
166
+ --warm-0: color(display-p3 0.9904 0.9785 0.9624);
167
+ --warm-50: #FFF9F2;
168
+ --warm-50: color(display-p3 0.9959 0.9773 0.9519);
169
+ --warm-100: #FCE6CB;
170
+ --warm-100: color(display-p3 0.9861 0.9003 0.7854);
171
+ --warm-200: #FFD6A6;
172
+ --warm-200: color(display-p3 0.9740 0.8452 0.6748);
173
+ --warm-300: #FEAF59;
174
+ --warm-300: color(display-p3 0.9642 0.6946 0.3782);
175
+ --warm-400: #F48519;
176
+ --warm-400: color(display-p3 0.8990 0.5431 0.2243);
177
+ --warm-500: #DB6300;
178
+ --warm-500: color(display-p3 0.8006 0.4157 0.1565);
179
+ --warm-600: #A75633;
180
+ --warm-600: color(display-p3 0.6145 0.3552 0.2294);
181
+ --warm-700: #8A462E;
182
+ --warm-700: color(display-p3 0.5077 0.2898 0.2006);
183
+ --warm-800: #6D3828;
184
+ --warm-800: color(display-p3 0.4006 0.2302 0.1713);
185
+ --warm-900: #532B22;
186
+ --warm-900: color(display-p3 0.3046 0.1778 0.1408);
187
+ --warm-950: #44231C;
188
+ --warm-950: color(display-p3 0.2491 0.1436 0.1165);
189
+ --amber-0: #FBFAF3;
190
+ --amber-0: color(display-p3 0.9856 0.9819 0.9552);
191
+ --amber-50: #FAF7E9;
192
+ --amber-50: color(display-p3 0.9780 0.9698 0.9198);
193
+ --amber-100: #F8EFCB;
194
+ --amber-100: color(display-p3 0.9701 0.9381 0.8003);
195
+ --amber-200: #F6DC99;
196
+ --amber-200: color(display-p3 0.9475 0.8648 0.6327);
197
+ --amber-300: #F7BD41;
198
+ --amber-300: color(display-p3 0.9381 0.7477 0.3311);
199
+ --amber-400: #E59E00;
200
+ --amber-400: color(display-p3 0.8716 0.6255 0.1299);
201
+ --amber-500: #C78100;
202
+ --amber-500: color(display-p3 0.7654 0.5049 0.0000);
203
+ --amber-600: #A56819;
204
+ --amber-600: color(display-p3 0.6118 0.4173 0.1721);
205
+ --amber-700: #875219;
206
+ --amber-700: color(display-p3 0.5015 0.3318 0.1466);
207
+ --amber-800: #6A3F18;
208
+ --amber-800: color(display-p3 0.3908 0.2530 0.1231);
209
+ --amber-900: #502F16;
210
+ --amber-900: color(display-p3 0.2948 0.1895 0.1031);
211
+ --amber-950: #391F0E;
212
+ --amber-950: color(display-p3 0.2095 0.1264 0.0666);
213
+ --acid-0: #F8FCF1;
214
+ --acid-0: color(display-p3 0.9758 0.9875 0.9485);
215
+ --acid-50: #F2F8E5;
216
+ --acid-50: color(display-p3 0.9535 0.9727 0.9050);
217
+ --acid-100: #E5EFC9;
218
+ --acid-100: color(display-p3 0.9041 0.9369 0.8019);
219
+ --acid-200: #CFDD90;
220
+ --acid-200: color(display-p3 0.8226 0.8663 0.5989);
221
+ --acid-300: #B4BD11;
222
+ --acid-300: color(display-p3 0.7123 0.7400 0.2471);
223
+ --acid-400: #A2A707;
224
+ --acid-400: color(display-p3 0.6420 0.6544 0.1046);
225
+ --acid-500: #8C8C00;
226
+ --acid-500: color(display-p3 0.5529 0.5517 0.0000);
227
+ --acid-600: #767502;
228
+ --acid-600: color(display-p3 0.4628 0.4579 0.1356);
229
+ --acid-700: #626006;
230
+ --acid-700: color(display-p3 0.3837 0.3754 0.1127);
231
+ --acid-800: #4E4B0C;
232
+ --acid-800: color(display-p3 0.3054 0.2964 0.0993);
233
+ --acid-900: #3C3A0E;
234
+ --acid-900: color(display-p3 0.2347 0.2264 0.0854);
235
+ --acid-950: #2A2808;
236
+ --acid-950: color(display-p3 0.1634 0.1553 0.0508);
237
+ --neutral-0: #FEFBFC;
238
+ --neutral-0: color(display-p3 0.9945 0.9834 0.9863);
239
+ --neutral-50: #F9F6F7;
240
+ --neutral-50: color(display-p3 0.9748 0.9637 0.9666);
241
+ --neutral-100: #F3EFF0;
242
+ --neutral-100: color(display-p3 0.9486 0.9375 0.9405);
243
+ --neutral-200: #E7E3E4;
244
+ --neutral-200: color(display-p3 0.9031 0.8922 0.8951);
245
+ --neutral-300: #D5D1D2;
246
+ --neutral-300: color(display-p3 0.8325 0.8217 0.8246);
247
+ --neutral-400: #A29FA0;
248
+ --neutral-400: color(display-p3 0.6336 0.6233 0.6261);
249
+ --neutral-500: #757273;
250
+ --neutral-500: color(display-p3 0.4570 0.4474 0.4500);
251
+ --neutral-600: #545252;
252
+ --neutral-600: color(display-p3 0.3293 0.3202 0.3227);
253
+ --neutral-700: #423F40;
254
+ --neutral-700: color(display-p3 0.2554 0.2467 0.2491);
255
+ --neutral-800: #282626;
256
+ --neutral-800: color(display-p3 0.1558 0.1478 0.1500);
257
+ --neutral-900: #191617;
258
+ --neutral-900: color(display-p3 0.0956 0.0882 0.0902);
259
+ --neutral-950: #0C090A;
260
+ --neutral-950: color(display-p3 0.0441 0.0372 0.0391);
261
+
262
+ --forest-brand-forest: var(--forest-600);
263
+ --forest-dark-forest: var(--forest-700);
264
+ --forest-darkest-forest: var(--forest-950);
265
+ --warm-ice-cream: var(--warm-50);
266
+ --warm-peach-cream: var(--warm-200);
267
+ --warm-acer-orange: var(--warm-500);
268
+ --warm-bark-brown: var(--warm-950);
269
+ --acid-acid-green: var(--acid-300);
270
+ }
271
+
272
+
273
+ /* Forest ramps → Tailwind/Nuxt UI colours — generated by scripts/scales.ts. Do not edit.
274
+ @theme: literal hex registers each family as a Tailwind colour (generates bg-forest-* etc.).
275
+ :root: re-points --color-<fam>-* to the P3-capable ramp var, and bridges the raw families to
276
+ Nuxt UI's --ui-color-<family>-* so `color="forest"` works. Semantic roles auto-derive. */
277
+ @theme static {
278
+ --color-forest-0: #F4FDF7;
279
+ --color-forest-50: #E8FBEE;
280
+ --color-forest-100: #CEF5DD;
281
+ --color-forest-200: #99E9BC;
282
+ --color-forest-300: #3BD996;
283
+ --color-forest-400: #0ABB7F;
284
+ --color-forest-500: #019F6D;
285
+ --color-forest-600: #00845C;
286
+ --color-forest-700: #046C4C;
287
+ --color-forest-800: #075640;
288
+ --color-forest-900: #074235;
289
+ --color-forest-950: #002A24;
290
+ --color-maple-0: #FFF8F8;
291
+ --color-maple-50: #FFF1F2;
292
+ --color-maple-100: #FFE1E2;
293
+ --color-maple-200: #FFC2C4;
294
+ --color-maple-300: #FF9A9B;
295
+ --color-maple-400: #F26F70;
296
+ --color-maple-500: #D35A59;
297
+ --color-maple-600: #AA514E;
298
+ --color-maple-700: #8C4340;
299
+ --color-maple-800: #6E3633;
300
+ --color-maple-900: #532A27;
301
+ --color-maple-950: #3B1C1A;
302
+ --color-river-0: #F6FBFF;
303
+ --color-river-50: #EDF6FF;
304
+ --color-river-100: #D8ECFF;
305
+ --color-river-200: #B2D9FF;
306
+ --color-river-300: #81C0FF;
307
+ --color-river-400: #43A2FF;
308
+ --color-river-500: #3588DF;
309
+ --color-river-600: #3C72B2;
310
+ --color-river-700: #355D93;
311
+ --color-river-800: #2D4974;
312
+ --color-river-900: #253858;
313
+ --color-river-950: #19263E;
314
+ --color-warm-0: #FDFAF6;
315
+ --color-warm-50: #FFF9F2;
316
+ --color-warm-100: #FCE6CB;
317
+ --color-warm-200: #FFD6A6;
318
+ --color-warm-300: #FEAF59;
319
+ --color-warm-400: #F48519;
320
+ --color-warm-500: #DB6300;
321
+ --color-warm-600: #A75633;
322
+ --color-warm-700: #8A462E;
323
+ --color-warm-800: #6D3828;
324
+ --color-warm-900: #532B22;
325
+ --color-warm-950: #44231C;
326
+ --color-amber-0: #FBFAF3;
327
+ --color-amber-50: #FAF7E9;
328
+ --color-amber-100: #F8EFCB;
329
+ --color-amber-200: #F6DC99;
330
+ --color-amber-300: #F7BD41;
331
+ --color-amber-400: #E59E00;
332
+ --color-amber-500: #C78100;
333
+ --color-amber-600: #A56819;
334
+ --color-amber-700: #875219;
335
+ --color-amber-800: #6A3F18;
336
+ --color-amber-900: #502F16;
337
+ --color-amber-950: #391F0E;
338
+ --color-acid-0: #F8FCF1;
339
+ --color-acid-50: #F2F8E5;
340
+ --color-acid-100: #E5EFC9;
341
+ --color-acid-200: #CFDD90;
342
+ --color-acid-300: #B4BD11;
343
+ --color-acid-400: #A2A707;
344
+ --color-acid-500: #8C8C00;
345
+ --color-acid-600: #767502;
346
+ --color-acid-700: #626006;
347
+ --color-acid-800: #4E4B0C;
348
+ --color-acid-900: #3C3A0E;
349
+ --color-acid-950: #2A2808;
350
+ --color-neutral-0: #FEFBFC;
351
+ --color-neutral-50: #F9F6F7;
352
+ --color-neutral-100: #F3EFF0;
353
+ --color-neutral-200: #E7E3E4;
354
+ --color-neutral-300: #D5D1D2;
355
+ --color-neutral-400: #A29FA0;
356
+ --color-neutral-500: #757273;
357
+ --color-neutral-600: #545252;
358
+ --color-neutral-700: #423F40;
359
+ --color-neutral-800: #282626;
360
+ --color-neutral-900: #191617;
361
+ --color-neutral-950: #0C090A;
362
+ }
363
+
364
+ :root {
365
+ --color-forest-0: var(--forest-0);
366
+ --color-forest-50: var(--forest-50);
367
+ --color-forest-100: var(--forest-100);
368
+ --color-forest-200: var(--forest-200);
369
+ --color-forest-300: var(--forest-300);
370
+ --color-forest-400: var(--forest-400);
371
+ --color-forest-500: var(--forest-500);
372
+ --color-forest-600: var(--forest-600);
373
+ --color-forest-700: var(--forest-700);
374
+ --color-forest-800: var(--forest-800);
375
+ --color-forest-900: var(--forest-900);
376
+ --color-forest-950: var(--forest-950);
377
+ --color-maple-0: var(--maple-0);
378
+ --color-maple-50: var(--maple-50);
379
+ --color-maple-100: var(--maple-100);
380
+ --color-maple-200: var(--maple-200);
381
+ --color-maple-300: var(--maple-300);
382
+ --color-maple-400: var(--maple-400);
383
+ --color-maple-500: var(--maple-500);
384
+ --color-maple-600: var(--maple-600);
385
+ --color-maple-700: var(--maple-700);
386
+ --color-maple-800: var(--maple-800);
387
+ --color-maple-900: var(--maple-900);
388
+ --color-maple-950: var(--maple-950);
389
+ --color-river-0: var(--river-0);
390
+ --color-river-50: var(--river-50);
391
+ --color-river-100: var(--river-100);
392
+ --color-river-200: var(--river-200);
393
+ --color-river-300: var(--river-300);
394
+ --color-river-400: var(--river-400);
395
+ --color-river-500: var(--river-500);
396
+ --color-river-600: var(--river-600);
397
+ --color-river-700: var(--river-700);
398
+ --color-river-800: var(--river-800);
399
+ --color-river-900: var(--river-900);
400
+ --color-river-950: var(--river-950);
401
+ --color-warm-0: var(--warm-0);
402
+ --color-warm-50: var(--warm-50);
403
+ --color-warm-100: var(--warm-100);
404
+ --color-warm-200: var(--warm-200);
405
+ --color-warm-300: var(--warm-300);
406
+ --color-warm-400: var(--warm-400);
407
+ --color-warm-500: var(--warm-500);
408
+ --color-warm-600: var(--warm-600);
409
+ --color-warm-700: var(--warm-700);
410
+ --color-warm-800: var(--warm-800);
411
+ --color-warm-900: var(--warm-900);
412
+ --color-warm-950: var(--warm-950);
413
+ --color-amber-0: var(--amber-0);
414
+ --color-amber-50: var(--amber-50);
415
+ --color-amber-100: var(--amber-100);
416
+ --color-amber-200: var(--amber-200);
417
+ --color-amber-300: var(--amber-300);
418
+ --color-amber-400: var(--amber-400);
419
+ --color-amber-500: var(--amber-500);
420
+ --color-amber-600: var(--amber-600);
421
+ --color-amber-700: var(--amber-700);
422
+ --color-amber-800: var(--amber-800);
423
+ --color-amber-900: var(--amber-900);
424
+ --color-amber-950: var(--amber-950);
425
+ --color-acid-0: var(--acid-0);
426
+ --color-acid-50: var(--acid-50);
427
+ --color-acid-100: var(--acid-100);
428
+ --color-acid-200: var(--acid-200);
429
+ --color-acid-300: var(--acid-300);
430
+ --color-acid-400: var(--acid-400);
431
+ --color-acid-500: var(--acid-500);
432
+ --color-acid-600: var(--acid-600);
433
+ --color-acid-700: var(--acid-700);
434
+ --color-acid-800: var(--acid-800);
435
+ --color-acid-900: var(--acid-900);
436
+ --color-acid-950: var(--acid-950);
437
+ --color-neutral-0: var(--neutral-0);
438
+ --color-neutral-50: var(--neutral-50);
439
+ --color-neutral-100: var(--neutral-100);
440
+ --color-neutral-200: var(--neutral-200);
441
+ --color-neutral-300: var(--neutral-300);
442
+ --color-neutral-400: var(--neutral-400);
443
+ --color-neutral-500: var(--neutral-500);
444
+ --color-neutral-600: var(--neutral-600);
445
+ --color-neutral-700: var(--neutral-700);
446
+ --color-neutral-800: var(--neutral-800);
447
+ --color-neutral-900: var(--neutral-900);
448
+ --color-neutral-950: var(--neutral-950);
449
+
450
+ --ui-color-forest-0: var(--forest-0);
451
+ --ui-color-forest-50: var(--forest-50);
452
+ --ui-color-forest-100: var(--forest-100);
453
+ --ui-color-forest-200: var(--forest-200);
454
+ --ui-color-forest-300: var(--forest-300);
455
+ --ui-color-forest-400: var(--forest-400);
456
+ --ui-color-forest-500: var(--forest-500);
457
+ --ui-color-forest-600: var(--forest-600);
458
+ --ui-color-forest-700: var(--forest-700);
459
+ --ui-color-forest-800: var(--forest-800);
460
+ --ui-color-forest-900: var(--forest-900);
461
+ --ui-color-forest-950: var(--forest-950);
462
+ --ui-forest: var(--forest-500);
463
+ --ui-color-maple-0: var(--maple-0);
464
+ --ui-color-maple-50: var(--maple-50);
465
+ --ui-color-maple-100: var(--maple-100);
466
+ --ui-color-maple-200: var(--maple-200);
467
+ --ui-color-maple-300: var(--maple-300);
468
+ --ui-color-maple-400: var(--maple-400);
469
+ --ui-color-maple-500: var(--maple-500);
470
+ --ui-color-maple-600: var(--maple-600);
471
+ --ui-color-maple-700: var(--maple-700);
472
+ --ui-color-maple-800: var(--maple-800);
473
+ --ui-color-maple-900: var(--maple-900);
474
+ --ui-color-maple-950: var(--maple-950);
475
+ --ui-maple: var(--maple-500);
476
+ --ui-color-river-0: var(--river-0);
477
+ --ui-color-river-50: var(--river-50);
478
+ --ui-color-river-100: var(--river-100);
479
+ --ui-color-river-200: var(--river-200);
480
+ --ui-color-river-300: var(--river-300);
481
+ --ui-color-river-400: var(--river-400);
482
+ --ui-color-river-500: var(--river-500);
483
+ --ui-color-river-600: var(--river-600);
484
+ --ui-color-river-700: var(--river-700);
485
+ --ui-color-river-800: var(--river-800);
486
+ --ui-color-river-900: var(--river-900);
487
+ --ui-color-river-950: var(--river-950);
488
+ --ui-river: var(--river-500);
489
+ --ui-color-warm-0: var(--warm-0);
490
+ --ui-color-warm-50: var(--warm-50);
491
+ --ui-color-warm-100: var(--warm-100);
492
+ --ui-color-warm-200: var(--warm-200);
493
+ --ui-color-warm-300: var(--warm-300);
494
+ --ui-color-warm-400: var(--warm-400);
495
+ --ui-color-warm-500: var(--warm-500);
496
+ --ui-color-warm-600: var(--warm-600);
497
+ --ui-color-warm-700: var(--warm-700);
498
+ --ui-color-warm-800: var(--warm-800);
499
+ --ui-color-warm-900: var(--warm-900);
500
+ --ui-color-warm-950: var(--warm-950);
501
+ --ui-warm: var(--warm-500);
502
+ --ui-color-acid-0: var(--acid-0);
503
+ --ui-color-acid-50: var(--acid-50);
504
+ --ui-color-acid-100: var(--acid-100);
505
+ --ui-color-acid-200: var(--acid-200);
506
+ --ui-color-acid-300: var(--acid-300);
507
+ --ui-color-acid-400: var(--acid-400);
508
+ --ui-color-acid-500: var(--acid-500);
509
+ --ui-color-acid-600: var(--acid-600);
510
+ --ui-color-acid-700: var(--acid-700);
511
+ --ui-color-acid-800: var(--acid-800);
512
+ --ui-color-acid-900: var(--acid-900);
513
+ --ui-color-acid-950: var(--acid-950);
514
+ --ui-acid: var(--acid-500);
515
+ --ui-color-neutral-0: var(--neutral-0);
516
+ --ui-color-neutral-50: var(--neutral-50);
517
+ --ui-color-neutral-100: var(--neutral-100);
518
+ --ui-color-neutral-200: var(--neutral-200);
519
+ --ui-color-neutral-300: var(--neutral-300);
520
+ --ui-color-neutral-400: var(--neutral-400);
521
+ --ui-color-neutral-500: var(--neutral-500);
522
+ --ui-color-neutral-600: var(--neutral-600);
523
+ --ui-color-neutral-700: var(--neutral-700);
524
+ --ui-color-neutral-800: var(--neutral-800);
525
+ --ui-color-neutral-900: var(--neutral-900);
526
+ --ui-color-neutral-950: var(--neutral-950);
527
+ --ui-neutral: var(--neutral-500);
528
+ }
529
+
530
+
531
+ /* Forest typography core — generated by scripts/type.ts (size ramp = Tailwind's default scale). Do not edit.
532
+ Parallels packages/tokens/scales/scales.css: a systematic ramp + named roles. Families resolve via the shared
533
+ @theme vars in packages/ui/src/styles/forest.css (--font-display / --font-sans / --font-mono). */
534
+ :root {
535
+ /* size ramp = Tailwind's default font-size scale, named xs…7xl (md = 16px base). Namespaced so it
536
+ never collides with Tailwind's own --text-* scale. Use as var(--type-text-lg) etc. For real
537
+ text-<name> Tailwind utilities, import the opt-in packages/tokens/type/type.theme.css. */
538
+ --type-text-xs: 0.75rem;
539
+ --type-text-sm: 0.875rem;
540
+ --type-text-md: 1rem; /* md · base */
541
+ --type-text-lg: 1.125rem;
542
+ --type-text-xl: 1.25rem;
543
+ --type-text-2xl: 1.5rem;
544
+ --type-text-3xl: 1.875rem;
545
+ --type-text-4xl: 2.25rem;
546
+ --type-text-5xl: 3rem;
547
+ --type-text-6xl: 3.75rem;
548
+ --type-text-7xl: 4.5rem;
549
+ --type-text-8xl: 6rem;
550
+ --type-text-9xl: 8rem;
551
+
552
+ /* role bundles — size · leading · weight · tracking · family */
553
+ --type-display-size: 3rem;
554
+ --type-display-line-height: 1;
555
+ --type-display-weight: 900;
556
+ --type-display-tracking: 0.01em;
557
+ --type-display-family: var(--font-display);
558
+ --type-display-transform: uppercase;
559
+ --type-display-size-fluid: clamp(2.25rem, 1.5rem + 3vw, 4.5rem);
560
+ --type-headline-size: 1.875rem;
561
+ --type-headline-line-height: 1.1;
562
+ --type-headline-weight: 900;
563
+ --type-headline-tracking: 0.01em;
564
+ --type-headline-family: var(--font-display);
565
+ --type-headline-transform: uppercase;
566
+ --type-title-size: 1.5rem;
567
+ --type-title-line-height: 1.18;
568
+ --type-title-weight: 700;
569
+ --type-title-tracking: 0em;
570
+ --type-title-family: var(--font-sans);
571
+ --type-title-transform: none;
572
+ --type-heading-sm-size: 1.125rem;
573
+ --type-heading-sm-line-height: 1.3;
574
+ --type-heading-sm-weight: 700;
575
+ --type-heading-sm-tracking: 0em;
576
+ --type-heading-sm-family: var(--font-sans);
577
+ --type-heading-sm-transform: none;
578
+ --type-heading-xs-size: 1rem;
579
+ --type-heading-xs-line-height: 1.3;
580
+ --type-heading-xs-weight: 700;
581
+ --type-heading-xs-tracking: 0em;
582
+ --type-heading-xs-family: var(--font-sans);
583
+ --type-heading-xs-transform: none;
584
+ --type-subtitle-size: 1.125rem;
585
+ --type-subtitle-line-height: 1.5;
586
+ --type-subtitle-weight: 400;
587
+ --type-subtitle-tracking: 0em;
588
+ --type-subtitle-family: var(--font-sans);
589
+ --type-subtitle-transform: none;
590
+ --type-body-size: 1rem;
591
+ --type-body-line-height: 1.6;
592
+ --type-body-weight: 400;
593
+ --type-body-tracking: 0em;
594
+ --type-body-family: var(--font-sans);
595
+ --type-body-transform: none;
596
+ --type-body-sm-size: 0.875rem;
597
+ --type-body-sm-line-height: 1.5;
598
+ --type-body-sm-weight: 400;
599
+ --type-body-sm-tracking: 0em;
600
+ --type-body-sm-family: var(--font-sans);
601
+ --type-body-sm-transform: none;
602
+ --type-label-size: 0.875rem;
603
+ --type-label-line-height: 1.2;
604
+ --type-label-weight: 700;
605
+ --type-label-tracking: 0.01em;
606
+ --type-label-family: var(--font-sans);
607
+ --type-label-transform: none;
608
+ --type-caption-size: 0.75rem;
609
+ --type-caption-line-height: 1.4;
610
+ --type-caption-weight: 400;
611
+ --type-caption-tracking: 0em;
612
+ --type-caption-family: var(--font-sans);
613
+ --type-caption-transform: none;
614
+ --type-overline-size: 0.75rem;
615
+ --type-overline-line-height: 1.1;
616
+ --type-overline-weight: 700;
617
+ --type-overline-tracking: 0.08em;
618
+ --type-overline-family: var(--font-sans);
619
+ --type-overline-transform: uppercase;
620
+ }
621
+
622
+ /* role utility classes — set the whole bundle in one class (text-transform baked in for Mohr/overline) */
623
+ .type-display {
624
+ font-family: var(--type-display-family);
625
+ font-size: var(--type-display-size);
626
+ line-height: var(--type-display-line-height);
627
+ font-weight: var(--type-display-weight);
628
+ letter-spacing: var(--type-display-tracking);
629
+ text-transform: var(--type-display-transform);
630
+ }
631
+ .type-headline {
632
+ font-family: var(--type-headline-family);
633
+ font-size: var(--type-headline-size);
634
+ line-height: var(--type-headline-line-height);
635
+ font-weight: var(--type-headline-weight);
636
+ letter-spacing: var(--type-headline-tracking);
637
+ text-transform: var(--type-headline-transform);
638
+ }
639
+ .type-title {
640
+ font-family: var(--type-title-family);
641
+ font-size: var(--type-title-size);
642
+ line-height: var(--type-title-line-height);
643
+ font-weight: var(--type-title-weight);
644
+ letter-spacing: var(--type-title-tracking);
645
+ text-transform: var(--type-title-transform);
646
+ }
647
+ .type-heading-sm {
648
+ font-family: var(--type-heading-sm-family);
649
+ font-size: var(--type-heading-sm-size);
650
+ line-height: var(--type-heading-sm-line-height);
651
+ font-weight: var(--type-heading-sm-weight);
652
+ letter-spacing: var(--type-heading-sm-tracking);
653
+ text-transform: var(--type-heading-sm-transform);
654
+ }
655
+ .type-heading-xs {
656
+ font-family: var(--type-heading-xs-family);
657
+ font-size: var(--type-heading-xs-size);
658
+ line-height: var(--type-heading-xs-line-height);
659
+ font-weight: var(--type-heading-xs-weight);
660
+ letter-spacing: var(--type-heading-xs-tracking);
661
+ text-transform: var(--type-heading-xs-transform);
662
+ }
663
+ .type-subtitle {
664
+ font-family: var(--type-subtitle-family);
665
+ font-size: var(--type-subtitle-size);
666
+ line-height: var(--type-subtitle-line-height);
667
+ font-weight: var(--type-subtitle-weight);
668
+ letter-spacing: var(--type-subtitle-tracking);
669
+ text-transform: var(--type-subtitle-transform);
670
+ }
671
+ .type-body {
672
+ font-family: var(--type-body-family);
673
+ font-size: var(--type-body-size);
674
+ line-height: var(--type-body-line-height);
675
+ font-weight: var(--type-body-weight);
676
+ letter-spacing: var(--type-body-tracking);
677
+ text-transform: var(--type-body-transform);
678
+ }
679
+ .type-body-sm {
680
+ font-family: var(--type-body-sm-family);
681
+ font-size: var(--type-body-sm-size);
682
+ line-height: var(--type-body-sm-line-height);
683
+ font-weight: var(--type-body-sm-weight);
684
+ letter-spacing: var(--type-body-sm-tracking);
685
+ text-transform: var(--type-body-sm-transform);
686
+ }
687
+ .type-label {
688
+ font-family: var(--type-label-family);
689
+ font-size: var(--type-label-size);
690
+ line-height: var(--type-label-line-height);
691
+ font-weight: var(--type-label-weight);
692
+ letter-spacing: var(--type-label-tracking);
693
+ text-transform: var(--type-label-transform);
694
+ }
695
+ .type-caption {
696
+ font-family: var(--type-caption-family);
697
+ font-size: var(--type-caption-size);
698
+ line-height: var(--type-caption-line-height);
699
+ font-weight: var(--type-caption-weight);
700
+ letter-spacing: var(--type-caption-tracking);
701
+ text-transform: var(--type-caption-transform);
702
+ }
703
+ .type-overline {
704
+ font-family: var(--type-overline-family);
705
+ font-size: var(--type-overline-size);
706
+ line-height: var(--type-overline-line-height);
707
+ font-weight: var(--type-overline-weight);
708
+ letter-spacing: var(--type-overline-tracking);
709
+ text-transform: var(--type-overline-transform);
710
+ }
711
+
712
+ /* fluid display — marketing/hero: scales with the viewport up to the top of the ramp */
713
+ .type-display--fluid {
714
+ font-size: var(--type-display-size-fluid);
715
+ }
716
+
717
+ /* treatments — orthogonal to size, compose onto any role. Family/figures, never a size. */
718
+ .type-tabular {
719
+ /* Mohr ships no tnum feature — measured, its "1" is 13.01px against "0" at 23.66px (30px/700), and
720
+ font-variant-numeric moves neither. So aligned digits need the family too, not just the figure set:
721
+ --font-sans goes flat at 0 spread. Composing this onto a display role therefore swaps the family.
722
+ That is the trade — a metric that must align cannot be set in the display face. Note --font-sans
723
+ caps at 700, so a display role's 900 renders as bold here, lighter than an adjacent Mohr heading. */
724
+ font-family: var(--font-sans);
725
+ font-variant-numeric: tabular-nums; /* aligned digits: tables, metrics */
726
+ /* Display roles uppercase, which is correct for a heading and wrong for a measured value: unit
727
+ letters are case-significant, so "4m 12s" must not render as "4M 12S" (mega, siemens). */
728
+ text-transform: none;
729
+ }
730
+ .type-mono {
731
+ font-family: var(--font-mono); /* code, token names, hex, IDs at whatever size the role sets */
732
+ font-size: 0.875em; /* mono runs optically large; the shrink matches sans x-height at the same step */
733
+ }
734
+
735
+ /* bare <code>/<kbd>/<samp> default: Tailwind Preflight already sets font-family: var(--font-mono) at
736
+ 1em, so this only adds the optical shrink — making a bare tag identical to .type-mono. <pre> keeps
737
+ its own size so Shiki code blocks are untouched. */
738
+ @layer base {
739
+ code,
740
+ kbd,
741
+ samp {
742
+ font-size: 0.875em;
743
+ }
744
+ /* Inline <code> renders as a boxed chip — the system's inline-code treatment, distinct from the bare
745
+ .type-mono treatment (which is for data: hex, token names, numbers, never boxed). Use <code> for
746
+ actual code/syntax in prose. Shiki blocks (pre code) opt out below. box-decoration-break keeps the
747
+ box intact when inline code wraps across lines. */
748
+ :not(pre) > code {
749
+ background: var(--ui-bg-elevated);
750
+ border: 1px solid var(--ui-border-muted);
751
+ border-radius: 0.25rem;
752
+ padding: 0.05em 0.35em;
753
+ box-decoration-break: clone;
754
+ -webkit-box-decoration-break: clone;
755
+ }
756
+ pre code {
757
+ font-size: inherit;
758
+ background: none;
759
+ border: 0;
760
+ padding: 0;
761
+ }
762
+ }
763
+
764
+
765
+ /* Forest motion ladder — generated by scripts/motion.ts. Do not edit.
766
+
767
+ The source of truth for animation across the system, the same "name it once, derive everywhere"
768
+ model as --ui-radius. Components reference these via the (--motion-*) arbitrary shorthand
769
+ (e.g. duration-(--motion-base), ease-(--motion-ease-out)); nothing hard-codes a millisecond value or
770
+ a bezier curve. Documented at /foundations/motion.
771
+
772
+ This file is framework-agnostic — plain custom properties and one media query, no at-rules a bundler
773
+ has to understand — so a non-Tailwind surface (the Slidev deck theme runs UnoCSS) imports the same
774
+ ladder the product does instead of restating it. The Tailwind half lives in motion.theme.css.
775
+
776
+ DURATIONS — bucketed by element class, all ≤ 300 ms (the UI ceiling: past ~300 ms a control feels
777
+ sluggish rather than smooth). Forest sits on the snappy end of every bucket on purpose — a faster
778
+ animation reads as a faster app.
779
+ instant press feedback, the keycap lift/press (state, not decoration)
780
+ fast small / near elements — tooltips, input focus, card & slider feedback, overlay EXITS
781
+ base overlays that scale in — popover, dropdown, menus, tabs indicator, switch
782
+ slow larger travel — collapsible & accordion height, nav viewport resize, sidebar collapse, slideover panel, progress
783
+
784
+ EASINGS — strong CUSTOM curves. The built-in CSS easings (ease-out / ease-in) are too weak to read as
785
+ intentional, and ease-in is banned outright: it starts slow, which makes the interface feel unresponsive
786
+ at the exact moment the user is watching. Enter AND exit both use ease-out (exits just run shorter).
787
+ ease-out the default — enters, exits, presses, colour fades
788
+ ease-in-out on-screen movement that both accelerates and settles — tabs indicator, nav resize, the sidebar collapse
789
+ ease-drawer iOS-style sheet/drawer curve (Ionic) — long, weighted pull for bottom sheets. Unreferenced by choice; vaul already ships this exact curve.
790
+
791
+ ★ ease-drawer is unreferenced BY CHOICE, and it costs nothing. MEASURED: UDrawer's sheet opens on a
792
+ keyframe animation from vaul's own injected stylesheet (`slideFromBottom`, 500ms,
793
+ cubic-bezier(0.32, 0.72, 0, 1)) — the identical curve this token names, so the shipped sheet already
794
+ runs it. Attaching the token would mean overriding animation-duration and animation-timing-function
795
+ against a four-attribute selector vaul owns, to arrive at the same motion. That 500ms sits past the
796
+ ≤300ms ceiling above; the ceiling is about controls, and a bottom sheet is a weighted pull (iOS ships
797
+ ~500ms too). It is animation, NOT transition: `transition-duration` on that element governs only the
798
+ drag-follow, so overriding it changes a real property and moves nothing.
799
+
800
+ REDUCED MOTION is handled below, once, for the whole system (WCAG 2.3.3). The token values themselves
801
+ stay unconditional — a component that wants to say something about its own reduced-motion behaviour
802
+ still writes `motion-reduce:` (the button keycap in themes/button.mechanics.ts does, so its lift/press
803
+ snaps while the colour step survives). But a per-component gate can only ever cover components Forest
804
+ overrides, and most motion in the app is stock: the skeleton pulse, indeterminate progress, the table
805
+ loader, vaul's drawer. A global floor covers those too, so it is the floor that carries the contract
806
+ and the per-component gates are the exception on top of it. */
807
+ :root {
808
+ --motion-instant: 80ms;
809
+ --motion-fast: 140ms;
810
+ --motion-base: 180ms;
811
+ --motion-slow: 240ms;
812
+
813
+ /* DWELL — how long a pointer has to REST before a hover-only affordance appears, so a cursor crossing
814
+ the region on its way somewhere else never summons it. Not a duration and so not bound by the ≤300ms
815
+ ceiling above: the motion itself still runs at `fast`, this is only the wait before it starts. It is
816
+ one-way — the affordance leaves immediately, because a control that lingers after the pointer has
817
+ gone reads as stuck. */
818
+ --motion-dwell: 500ms;
819
+
820
+ --motion-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
821
+ --motion-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
822
+ --motion-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
823
+ }
824
+
825
+ /* ── Reduced motion — the system-wide floor ────────────────────────────────────────────────────────
826
+ The contract from the doctrine above: fewer and gentler, NOT zero. WCAG 2.3.3 is about motion, not
827
+ about change, so this removes movement and keeps the colour and opacity fades that carry meaning.
828
+
829
+ Two deliberate choices, both load-bearing:
830
+
831
+ 1. `animation-duration: 1ms` rather than `animation: none`. Reka drives overlay teardown off the
832
+ exit keyframe — a UPopover/UModal/UDropdown with `data-[state=closed]` unmounts when its
833
+ animation finishes — and a suppressed animation is the classic way to strand a closed overlay in
834
+ the DOM, invisible but still focusable. Measured on Reka 2.x here: BOTH forms tear down cleanly,
835
+ because its Presence primitive falls back to `animation-name: none`. 1ms is kept anyway — it does
836
+ not depend on that fallback staying, and it costs nothing. `animation-iteration-count: 1` is the
837
+ part that matters unconditionally: it stops the infinite loaders (skeleton pulse, indeterminate
838
+ progress, the table loading bar) from looping forever at 1ms.
839
+
840
+ 2. `transition-property` is re-declared as an ALLOW-LIST of non-motion properties rather than
841
+ zeroing the duration. An element that was only transitioning `transform` is left with nothing to
842
+ transition, so it snaps; an element transitioning `background-color` keeps its fade at its own
843
+ duration. Zeroing the duration instead would have flattened both, which is the "zero" the
844
+ doctrine rules out.
845
+
846
+ ★ UNLAYERED, deliberately. Do not wrap this in `@layer`: for `!important` declarations the cascade
847
+ reverses layer order, so a layered `!important` beats an unlayered one. Wrapping the floor would
848
+ silently re-rank it against any layered `!important` a consumer ships.
849
+
850
+ This is the floor, and it covers what a per-component gate structurally cannot: stock motion Forest
851
+ never overrides. Components may still add their own `motion-reduce:` where they want to say
852
+ something more specific — the button keycap does. */
853
+ @media (prefers-reduced-motion: reduce) {
854
+ *,
855
+ *::before,
856
+ *::after {
857
+ animation-duration: 1ms !important;
858
+ animation-delay: 0ms !important;
859
+ animation-iteration-count: 1 !important;
860
+ transition-property:
861
+ color, background-color, border-color, outline-color, text-decoration-color, fill, stroke,
862
+ opacity, box-shadow !important;
863
+ }
864
+
865
+ html {
866
+ scroll-behavior: auto !important;
867
+ }
868
+ }
869
+
870
+
871
+ /* Forest motion — the Tailwind v4 half — generated by scripts/motion.ts. Do not edit.
872
+
873
+ A bare `transition` (no duration, no easing) is the single most common motion utility in the stock
874
+ recipes — the hover affordance on a card, a calendar cell, a rating star. Left alone it runs on
875
+ Tailwind's own defaults, 150ms and the weak built-in `ease`, which is a hard-coded value the
876
+ doctrine in motion.css doesn't reach. Re-pointing the two Tailwind defaults at the tokens brings every
877
+ undurationed `transition` onto the ladder at once, so a component only needs an explicit
878
+ duration-/ease- pair when it wants something OTHER than the near-element default.
879
+
880
+ ★ Import this WITH motion.css, not instead of it — the values it points at are declared there. And
881
+ unlike type.theme.css this one is not optional: dropping it does not remove a convenience, it hands
882
+ every bare `transition` in the framework back to Tailwind's 150ms/`ease`. */
883
+ @theme {
884
+ --default-transition-duration: var(--motion-fast);
885
+ --default-transition-timing-function: var(--motion-ease-out);
886
+ }
887
+
888
+
889
+ /* Forest brand layer for Nuxt UI — the NON-colour brand config: fonts, radius, the coarse variant.
890
+ The colour ramps are owned by the engine: packages/tokens/scales/ramps.css emits the --<family>-* values and
891
+ packages/tokens/scales/theme.css registers them as Tailwind/Nuxt UI colours (--color-<family>-*). Do not add
892
+ a colour ramp here — it would shadow the generated one and drift (this file used to, and did).
893
+ Import order in an app: tailwindcss → @nuxt/ui → @humanforest/fonts → ramps.css → theme.css → this file. */
894
+
895
+ /* Brand fonts. GT Haptik for everything; Mohr (display) for headings via .font-display;
896
+ JetBrains Mono (self-hosted in @humanforest/fonts) for code/tokens. */
897
+ @theme {
898
+ --font-sans: 'GT Haptik', system-ui, sans-serif;
899
+ --font-display: 'MohrAlt', 'GT Haptik', system-ui, sans-serif;
900
+ /* The italic display cut, named so a theme fragment never has to spell the family out. It is a
901
+ token of its own rather than a re-point of --font-display, because that re-point is keyed on
902
+ the .forest-context-mobile CLASS (below) and only ForestContext stamps it: a fragment reading
903
+ var(--font-display) resolves to the UPRIGHT cut anywhere the class is absent, which is how
904
+ mobile would quietly render marketing's face. This one always means the italic cut. */
905
+ --font-display-italic: 'Mohr-BlackIt', 'GT Haptik', system-ui, sans-serif;
906
+ --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
907
+
908
+ /* Two-weight system: every family ships regular + bold only (Mohr is display-black 900). No 500
909
+ or 600 cut exists, so font-medium and font-semibold resolve to the same faces as normal/bold —
910
+ these re-maps make the tokens say so, and keep the fallback stack (which HAS real 500/600
911
+ cuts) from flashing a weight the brand fonts can't render during font-display: swap. */
912
+ --font-weight-medium: 400;
913
+ --font-weight-semibold: 700;
914
+ }
915
+
916
+ /* Forest reads softer than Nuxt UI's default sharp radius — base bumped 0.25→0.5rem. This is the
917
+ only lever: the rounded-* utilities inline calc(var(--ui-radius) * N) at build, so the whole scale
918
+ derives from this one var. Per-:root --radius-* overrides are NOT read by the utilities (an
919
+ @theme override is the only way to bend an individual step). */
920
+ :root {
921
+ --ui-radius: 0.5rem;
922
+ }
923
+
924
+ /* The header band. Stock is 4rem; Forest runs 3rem, which clears the 32px control it holds with 8px
925
+ either side.
926
+ ★ It is not only a height. UDashboardNavbar root, UDashboardSidebar header and UHeader root +
927
+ container take it as a box; USidebar, UModal and USlideover headers take it as a floor; UMain and
928
+ UError subtract it from the viewport; UPageAside and ProseH1–H4 read it as a sticky offset and a
929
+ scroll margin. Thirteen recipes in all, so the value also decides where an anchored heading rests. */
930
+ :root {
931
+ --ui-header-height: 3rem;
932
+ }
933
+
934
+ /* ── The well ──────────────────────────────────────────────────────────────────────────────────────
935
+ The WELL is the capped, centred content column — UContainer — and it is the only thing in the
936
+ system that caps page width. A page never sets max-w-*. See LAYOUT-SPEC.md.
937
+
938
+ `--ui-container` (Nuxt UI's own token, 80rem) is the lever, exactly as --ui-radius is for radius:
939
+ one var, everything derives. UContainer reads it, so using UContainer IS the fix for a shell that
940
+ would otherwise be uncapped.
941
+
942
+ ★ SCOPE OF THAT CLAIM. None of the DASHBOARD primitives consume --ui-container, which is why an app
943
+ shell is uncapped by default. The PAGE-CHROME family is the opposite: UHeader, UFooter, UBanner,
944
+ UPageHero, UPageSection and UPageCTA each embed a UContainer of their own, so they self-cap and
945
+ nesting one inside a well double-applies the gutters. There are also two further width mechanisms
946
+ that have nothing to do with this token — USidebar declares its own --sidebar-width (16rem, fixed
947
+ positioning) and UDashboardSidebar negotiates a PERCENTAGE with its group (default 15, persisted).
948
+ The well is the only cap a PAGE sets; it is not the only cap in the system. /layout/well carries
949
+ the register.
950
+
951
+ THREE RUNGS, by what the content is:
952
+ .well-reading 46rem — a single column of prose or one form. The reading measure.
953
+ (default) 80rem — the working surface: lists, detail, settings, mixed content
954
+ .well-full uncapped — maps, boards, timelines, canvases only
955
+ .well-flush drops the gutters, opt-in ON TOP of well-full
956
+
957
+ The rungs are named for content, not density, because density does not predict a width. There is
958
+ deliberately no rung above `working`: no measured cap supports one. Every cap anyone publishes sits
959
+ BELOW it — Vercel's Geist works at 1200px, Notion at 720px, and Forest's own shipped product layout
960
+ caps at 48rem — and caps track product class and column count rather than a house maximum.
961
+
962
+ Releasing the cap and removing the gutters are separate levers, matching the two systems that
963
+ publish theirs: Primer's containerWidth="full" leaves padding alone (a separate `padding` prop
964
+ owns it), and Carbon's .cds--grid--full-width is defined solely as max-inline-size: 100%. So most
965
+ `full` content keeps its gutters; only content that must touch the panel edge adds .well-flush. */
966
+ :root {
967
+ --forest-well-reading: 46rem;
968
+ }
969
+
970
+ @utility well-reading {
971
+ --ui-container: var(--forest-well-reading);
972
+ }
973
+ @utility well-full {
974
+ --ui-container: 100%;
975
+ }
976
+
977
+ /* well-flush is NOT an @utility, deliberately. It has to beat UContainer's own gutters
978
+ (px-4 sm:px-6 lg:px-8), and those live in Tailwind's `utilities` layer inside media queries — a
979
+ same-layer rule loses to them on position no matter how it's written. Unlayered CSS outranks every
980
+ layer, so a plain rule wins cleanly and no !important is needed. It also reads correctly: this is a
981
+ deliberate opt-out of a component's padding, not another utility competing with it. */
982
+ .well-flush {
983
+ padding-inline: 0;
984
+ }
985
+
986
+ /* ── The canvas HUD ────────────────────────────────────────────────────────────────────────────────
987
+ The second layout pattern: chrome OVER a full-bleed canvas (map, camera) rather than around
988
+ content. The canvas reaches the physical edge and runs under the status bar; chrome never does.
989
+
990
+ Safe area is chrome's problem, and it arrives two ways. A real device reports it through
991
+ `env(safe-area-inset-*)`. A device MOCK — an SVG phone drawn inside a desktop page — has no notch
992
+ for the browser to report, so the frame supplies measured numbers by setting these vars on the
993
+ screen element. One read order covers both: the var wins where a frame set it, `env()` elsewhere.
994
+ No `isDocs` branch, and the shipped component is the same component in either place.
995
+
996
+ Left as `initial` rather than 0 so the `env()` fallback is what actually resolves off a device;
997
+ a var declared here as a length would shadow it everywhere. */
998
+ :root {
999
+ --forest-chrome-gutter: 1rem; /* inset between chrome and the safe edge — never the canvas's */
1000
+ /* The gutter the dashboard panel floats in — its own margin (themes/dashboard-panel.ts) and the
1001
+ app shell's bands both read it, so a band's edges track the card's rather than restating a
1002
+ number that has to agree. */
1003
+ --forest-panel-inset: 1rem;
1004
+ /* ONE CONTROL — a FAB, a square icon button. The canvas HUD's areas are all measured in these rather
1005
+ than in hand-picked heights, so a diagram of the shell derives from the shell.
1006
+
1007
+ ★ IT IS NOT ONE SIZE, because it is not one job. 2.75rem is the TOUCH target floor (44px), and on a
1008
+ pointer surface it reads as oversized against every map tool a console operator has used — Mapbox's
1009
+ own control measures 32px, Apple's about 30. 2.25rem (36px) is the pointer size: comfortably past
1010
+ the 24px WCAG 2.2 minimum, and in the band the market actually uses.
1011
+
1012
+ ★ TWO signals raise it, and both are needed. `pointer: coarse` catches a real touch device. The
1013
+ mobile CONTEXT catches a phone rendered on a desktop — a device mock in these docs has a fine
1014
+ pointer and would otherwise draw thumb controls at desk size. Neither alone is sufficient; that
1015
+ distinction is the one the contexts layer exists to make. */
1016
+ --forest-canvas-control: 2.25rem;
1017
+ /* ★ Reserved for the BASEMAP, not for taste. Mapbox's terms require its wordmark (bottom-left) and
1018
+ its attribution (bottom-right) to stay visible, so the whole bottom strip of a map canvas belongs
1019
+ to the basemap and no chrome may float over it. 0 by default — a camera or a board canvas owes
1020
+ nothing — and the canvas HUD adds it to its bottom inset when a surface sets it. */
1021
+ --forest-canvas-basemap-strip: 0px;
1022
+ /* ★ The chrome fill, NAMED. The utility below reads it, and so does anything that has to restate the
1023
+ surface under a variant — a disabled control, whose recipe sets its own `disabled:bg-*` and wins on
1024
+ source order, so it must be handed this value rather than a second copy of the colour-mix. */
1025
+ --forest-canvas-chrome-fill: color-mix(in oklab, var(--ui-bg) 85%, transparent);
1026
+ /* ★ The DISABLED chrome fill. A dimmed glyph alone was not enough — measured, a switched-off control
1027
+ still read as pressable because its surface was identical to a live one. This moves the surface
1028
+ itself toward the middle: `--ui-bg-elevated` is greyer than the base in light and lighter than it in
1029
+ dark, so the tile recedes in BOTH modes without either becoming a different hue. Same 85% and the
1030
+ same blur, so it stays the same MATERIAL — a quieter instance of it, not another object. */
1031
+ --forest-canvas-chrome-fill-disabled: color-mix(in oklab, var(--ui-bg-elevated) 85%, transparent);
1032
+ }
1033
+
1034
+ /* The touch floor, from either signal — a real coarse pointer, or a surface declaring itself a phone. */
1035
+ @media (pointer: coarse) {
1036
+ :root {
1037
+ --forest-canvas-control: 2.75rem;
1038
+ }
1039
+ }
1040
+ :where(.forest-context-mobile) {
1041
+ --forest-canvas-control: 2.75rem;
1042
+ }
1043
+
1044
+ /* ── CHROME OVER A CANVAS ──────────────────────────────────────────────────────────────────────────
1045
+ The surface every piece of chrome takes when it floats ON a canvas rather than beside it — a HUD
1046
+ panel, a control cluster, a filter column, a legend. It is one treatment because it answers one
1047
+ question, and the answer has to be the same everywhere it is asked: how much of the map does the
1048
+ operator keep while something sits on top of it?
1049
+
1050
+ Semi-opaque rather than solid, so the canvas underneath stays legible as CONTEXT — the thing being
1051
+ filtered, measured or dispatched is still visible through its own controls. Blurred rather than
1052
+ merely translucent, because a basemap is high-frequency: street lines and labels read straight
1053
+ through a flat 85% fill and turn the text on top of it to noise. The blur is what buys the
1054
+ translucency its legibility, so the two ship together or not at all.
1055
+
1056
+ 85%, one value. It was authored five ways (80, 85, 90 × two blur radii) before this existed, which
1057
+ is exactly the drift a shared treatment removes: chrome on one map read differently from chrome on
1058
+ the next with nothing naming the difference.
1059
+
1060
+ ★ An @utility, not a plain class, so ordinary utilities still beat it — `hover:bg-default` on a
1061
+ pressable HUD tile has to win, and a plain class outside the utilities layer would take that away
1062
+ (the reason `well-flush` above is deliberately NOT an @utility is the mirror image of this one).
1063
+
1064
+ ★ NEVER ON A CONTROL THAT CARRIES ITS OWN SOLID FILL. This replaces background-color, so on a solid
1065
+ button it removes the very fill the label colour was chosen against and leaves that ink sitting on a
1066
+ translucent surface — unreadable in both modes (a primary FAB on the canvas specimen shipped exactly
1067
+ that). Solid controls are already opaque over a basemap and need nothing from this; it is for the
1068
+ neutral, outline and ghost chrome whose surface would otherwise be a flat card over a moving map.
1069
+
1070
+ ★ color-mix at the ELEMENT, not a var precomputed on :root. --ui-bg re-points inside .light/.dark
1071
+ islands — a browser frame drawing a dark screen inside a light page is one — and a derived value
1072
+ computed once at the root would stay the outer mode's colour inside them. */
1073
+ @utility forest-canvas-chrome {
1074
+ /* ★ Read through VARS so a parent can switch the surface off on its children. A merged control group
1075
+ draws ONE surface and needs its members bare, and it cannot win that by selector: this rule is
1076
+ doubled (below), so a child selector on the group loses to it. Declaring the var on the child wins
1077
+ without any specificity contest, because it is the same rule reading a different value. The
1078
+ defaults are the surface itself, so an unqualified use is unchanged. */
1079
+ /* ★ Doubled selector, for a fight a single class loses. Chrome over a canvas is often a COMPONENT —
1080
+ a UBadge, a UButton — whose own recipe already ships `bg-default`, and that recipe's utility sits
1081
+ later in the same layer, so a one-class rule here is overruled and the tile renders opaque
1082
+ (measured: the HUD's badges lost their translucency to exactly this). Two classes outrank it.
1083
+ Variants still win, which is the point of staying in the utilities layer: `hover:bg-default` is
1084
+ class + pseudo-class, the same specificity, and Tailwind emits variants after base utilities. */
1085
+ &.forest-canvas-chrome {
1086
+ background-color: var(--forest-canvas-chrome-fill);
1087
+ -webkit-backdrop-filter: var(--forest-canvas-chrome-blur, blur(12px));
1088
+ backdrop-filter: var(--forest-canvas-chrome-blur, blur(12px));
1089
+ }
1090
+ }
1091
+
1092
+ /* ── CANVAS SHELL · THE SURFACE REFLOW ─────────────────────────────────────────────────────────────
1093
+ FCanvasHud adapts to the width of its own container, so ONE instance is correct on a phone
1094
+ browser and on a console. Two things change together, and they have to:
1095
+
1096
+ narrow top rails 9rem (a card), bottom rails one control wide (a FAB strip) sitting ABOVE a
1097
+ full-width dock — the only place a rail can go above a CTA that spans the thumb zone.
1098
+ wide top rails 17rem and bottom rails 19rem (panels), and the bottom band mirrors the top
1099
+ one: rails in the CORNERS with the dock between them, because a centred dock leaves
1100
+ those corners free and a detail panel belongs in one.
1101
+
1102
+ ★ CONTAINER queries, not media queries. The shell is often NOT the viewport: on the console it is
1103
+ the interior of an inset dashboard panel, several hundred pixels narrower than the window and
1104
+ narrower again when the sidebar expands. A media query would reflow it on the wrong measurement.
1105
+ The shell's root declares `container: forest-canvas / inline-size`.
1106
+
1107
+ ★ The threshold is a LITERAL 60rem and cannot be a token: a container query condition may not read
1108
+ a custom property. 60rem is where the wide arrangement starts to fit — two 19rem rails are 38rem of
1109
+ it, and below that the dock has no room left between them.
1110
+
1111
+ Pinning: `[data-surface]` overrides the query in both directions, for a specimen that has to draw a
1112
+ given surface regardless of how wide it happens to be rendered.
1113
+
1114
+ ★ THESE ARE THE ONLY COPIES of the rail measures. The component applies them as `var(...)` rather than as
1115
+ JS values, a diagram sizes its chips from the same properties, and documentation reads them back off a
1116
+ hidden probe instead of restating them — so changing a number here changes it everywhere it is claimed. */
1117
+ .forest-hud {
1118
+ /* Ceiling on a rail, not its width. A rail HUGS its control — `fit-content(cap)` sizes to content
1119
+ and stops here — so a 36px button leaves 36px and the hub keeps the rest. A fixed track left a
1120
+ control floating in dead space, and the card beside it indented by space nothing occupied. The
1121
+ cap still earns its keep: an oversized child clamps here instead of starving the hub. */
1122
+ --forest-canvas-rail-max: 9rem;
1123
+ /* The card measure. This is the number the top rails used to carry, moved to the areas that hold
1124
+ cards — which is the whole point of splitting the two. Inert on a phone, where the card spans the
1125
+ measure between its band's rails instead. */
1126
+ --forest-canvas-card: 9rem;
1127
+ /* Ceiling on a band's height: half the canvas, so a card in each band still leaves the map its middle.
1128
+ `cqh` against the chrome layer's size container, like the bottom rail's ceiling. */
1129
+ --forest-canvas-card-max: 50cqh;
1130
+ }
1131
+
1132
+ /* ★ ONE GRID PER BAND, not one for the HUD.
1133
+ A single grid means one set of columns for the whole chrome layer, and every coupling that caused
1134
+ came with it: an empty top rail could not free its column because the BOTTOM rail was holding it
1135
+ open; a card in each band was pinned to the same track whatever its band wanted. Bands sized
1136
+ independently have none of it, and nothing outside a band can move what is inside it. */
1137
+ .forest-hud-band {
1138
+ display: grid;
1139
+ grid-template-columns:
1140
+ fit-content(var(--forest-canvas-rail-max))
1141
+ fit-content(var(--forest-canvas-card))
1142
+ minmax(0, 1fr)
1143
+ fit-content(var(--forest-canvas-card))
1144
+ fit-content(var(--forest-canvas-rail-max));
1145
+ /* Top band: everything hangs from the top edge, so a short control sits level with a tall card's
1146
+ top rather than dangling at its bottom. The bottom band flips this — see --dock. */
1147
+ align-items: start;
1148
+ pointer-events: none;
1149
+ /* ★ THE CAP IS ON THE TRACK, NOT ON THE ITEM. A `max-height` on a grid item clamps what it PAINTS and
1150
+ nothing else: the auto row still sizes to the item's content, so a 2000px card left the area drawn at
1151
+ its 383px ceiling inside a 2116px band, and the band pushed the dock out of the layer exactly as if
1152
+ there were no cap at all.
1153
+ `fit-content(cap)` and not `minmax(0, cap)`: a fixed `0` minimum makes the base size 0 and the track
1154
+ resolves to its growth limit, so a band holding a 165px rail claimed the whole half and starved the
1155
+ other band of the room it was owed. fit-content is content-sized up to the cap — which is the rule
1156
+ being stated. It only binds because the card area carries `min-height: 0`; without it the scroll
1157
+ wrapper's min-content contribution is the card's full height and no track can clamp below it. */
1158
+ grid-template-rows: fit-content(var(--forest-canvas-card-max));
1159
+ /* ★ THE TOP BAND YIELDS, THE BOTTOM ONE NEVER DOES. Half a canvas per band plus the hub row plus the
1160
+ dock is more than a canvas, so something has to give, and it is never the primary action. */
1161
+ min-height: 0;
1162
+ }
1163
+
1164
+ /* The card fills the measure between its band's rails on a phone; on a console it takes its own
1165
+ track and stops at the card measure. `fit-content` is what lets the unused card track collapse to
1166
+ nothing rather than claim width — and it cannot be wrapped in `minmax()`, which is not valid and
1167
+ silently drops the whole declaration to `auto`. */
1168
+ .forest-hud-card {
1169
+ grid-column: 2 / 5;
1170
+ }
1171
+
1172
+ /* The dock rides its own full-width row under the band on a phone, so the rails sit on its top edge.
1173
+ `end` here and `start` on the base: the bottom band grows UPWARD from the dock, so a short control
1174
+ sits level with a tall card's BOTTOM. */
1175
+ .forest-hud-band--dock {
1176
+ grid-template-rows: fit-content(var(--forest-canvas-card-max)) auto;
1177
+ align-items: end;
1178
+ flex-shrink: 0;
1179
+ }
1180
+ .forest-hud-dock {
1181
+ grid-row: 2;
1182
+ grid-column: 1 / -1;
1183
+ }
1184
+
1185
+ @container forest-canvas (min-width: 60rem) {
1186
+ .forest-hud:not([data-surface='phone']) {
1187
+ --forest-canvas-rail-max: 17rem;
1188
+ /* ★ A PROPORTION, not a measure, and only where the card takes a track of its own. A third across
1189
+ and a half down is the card's shape wherever the canvas is; a fixed 17rem was a third of one
1190
+ particular panel and a different share of every other. It lands within 10px of the number it
1191
+ replaces at a typical console width, and keeps that share when the panel is not typical.
1192
+ `cqi` against the chrome layer, matching the `cqh` the height ceiling already uses. */
1193
+ --forest-canvas-card: calc(100cqi / 3);
1194
+ }
1195
+ .forest-hud:not([data-surface='phone']) .forest-hud-card--start {
1196
+ grid-column: 2 / 3;
1197
+ }
1198
+ .forest-hud:not([data-surface='phone']) .forest-hud-card--end {
1199
+ grid-column: 4 / 5;
1200
+ }
1201
+ /* Console: the dock joins the band's single row, spanning it so it centres on the CANVAS rather
1202
+ than on whatever the rails and cards left over. */
1203
+ .forest-hud:not([data-surface='phone']) .forest-hud-band--dock {
1204
+ grid-template-rows: fit-content(var(--forest-canvas-card-max));
1205
+ }
1206
+ .forest-hud:not([data-surface='phone']) .forest-hud-dock {
1207
+ grid-row: 1;
1208
+ }
1209
+ }
1210
+
1211
+ .forest-hud[data-surface='web'] {
1212
+ --forest-canvas-rail-max: 17rem;
1213
+ --forest-canvas-card: calc(100cqi / 3);
1214
+ }
1215
+ .forest-hud[data-surface='web'] .forest-hud-card--start {
1216
+ grid-column: 2 / 3;
1217
+ }
1218
+ .forest-hud[data-surface='web'] .forest-hud-card--end {
1219
+ grid-column: 4 / 5;
1220
+ }
1221
+ .forest-hud[data-surface='web'] .forest-hud-band--dock {
1222
+ grid-template-rows: fit-content(var(--forest-canvas-card-max));
1223
+ }
1224
+ .forest-hud[data-surface='web'] .forest-hud-dock {
1225
+ grid-row: 1;
1226
+ }
1227
+
1228
+ /* Kill Nuxt UI's pure-white literals so EVERY surface/text/border rides the warm Forest neutral ramp
1229
+ (neutral-0 = #FEFBFC, a hair warm) instead of flat #fff. Nuxt hardcodes white on the max-contrast /
1230
+ inverted slots; we redirect each to neutral-0. Light and dark hit different tokens (white lands on
1231
+ different slots per mode). The mid-range tokens already derive from --ui-color-neutral-* — untouched.
1232
+ Note: dark bg-default stays Nuxt's neutral-900 (the card) so it lifts off the neutral-950 shell. */
1233
+ :root:not(.dark), .light {
1234
+ --ui-bg: var(--ui-color-neutral-0); /* page/card base — was pure white */
1235
+ --ui-text-inverted: var(--ui-color-neutral-0); /* text on inverted surfaces — was pure white */
1236
+ }
1237
+
1238
+ .dark {
1239
+ --ui-bg-inverted: var(--ui-color-neutral-0); /* inverted surface (solid tooltips/cards) */
1240
+ --ui-text-highlighted: var(--ui-color-neutral-0); /* strongest text (headings) — was pure white */
1241
+ --ui-border-inverted: var(--ui-color-neutral-0); /* border on inverted surfaces — was pure white */
1242
+ }
1243
+
1244
+ /* ── Card nesting — depth-stepped surface + concentric corners ─────────────────────────────────────
1245
+ Drives `.forest-card` (set on UCard's root by forest-preset; soft/subtle fill from --forest-card-bg).
1246
+ The neutral SURFACE ramp is compressed (in dark bg-muted == bg-elevated), so translucent fills can't
1247
+ separate nested cards. Instead each nesting level steps one NEUTRAL-RAMP level — the standard surface
1248
+ ladder (Material 3 surface-containers · Apple system-background levels · Radix gray 1/2/3): surfaces
1249
+ step AWAY from the page — light DARKER, dark LIGHTER. Corners stay concentric: nested radius =
1250
+ parent radius − padding, floored at 8px (std 24→8, held · mkt/mobile 32→16→8 — a larger outer
1251
+ radius buys one more step). Capped at 3 levels (deeper reuses L3). Scoped to `.forest-card`
1252
+ so it can't leak into other components' data-slot=root. Coupled to Nuxt UI's card DOM by design. */
1253
+ :root {
1254
+ --forest-card-pad: 1rem; /* p-4 — the inset between a card and its nested child */
1255
+ --forest-card-radius: 1.5rem; /* xl / 24px — outer radius, the base for the concentric calc */
1256
+ --forest-drawer-radius: 1.5rem; /* drawer edge radius — xl/24 standard; mobile/marketing → 2rem */
1257
+ }
1258
+ /* Drawer edge radius per context: mobile + marketing round to 2xl/32. Set on the context MARKER CLASS
1259
+ (forest-context-*, stamped by ForestContext.vue on the same wrapper as the diagnostic data-context
1260
+ attr) so it INHERITS down into the drawer content (which reads rounded-*-[var(--forest-drawer-radius)]).
1261
+ The var route, not a slot class: the radius lives in the preset's compoundVariants (stock sets it
1262
+ there per direction+inset, beating any base slot), and a context UTheme can only add slots.
1263
+ The marker class is the sanctioned hook here; data-context stays diagnostic-only (test-asserted). */
1264
+ :where(.forest-context-mobile, .forest-context-marketing) { --forest-drawer-radius: 2rem; }
1265
+ /* Mobile's display face is Mohr-BlackIt — the italic, always-uppercase cut, licensed for THIS context
1266
+ only (web/marketing keep upright MohrAlt). Rather than make every author remember the swap, the
1267
+ context re-points --font-display, so the plain `font-display` utility (and anything else reading the
1268
+ token) resolves to the italic cut inside a mobile subtree. Family rides the var; slant and case
1269
+ can't, so they're pinned on the utility itself — Mohr is ALWAYS uppercase, the two travel together.
1270
+ Scoped to the context MARKER CLASS, which reaches portaled Reka content the way a theme slot can't. */
1271
+ .forest-context-mobile {
1272
+ --font-display: 'Mohr-BlackIt', 'GT Haptik', system-ui, sans-serif;
1273
+ }
1274
+ :where(.forest-context-mobile) .font-display {
1275
+ font-style: italic;
1276
+ text-transform: uppercase;
1277
+ }
1278
+ /* Mobile + marketing drawer: roomier horizontal padding (24px vs the standard 16px p-4). Vertical
1279
+ stays. One CSS rule on the context marker class rather than the same container slot restated in
1280
+ both context themes. */
1281
+ :where(.forest-context-mobile, .forest-context-marketing) [data-vaul-drawer] [data-slot="container"] {
1282
+ padding-inline: 1.5rem;
1283
+ }
1284
+ /* Level surfaces = the page mixed with a little ink, in small EVEN steps (5/10/15 %). color-mix
1285
+ paints for background (unlike box-shadow, where the ring must stay rgb). Auto mode-aware: both
1286
+ --ui-bg and --ui-text-highlighted flip by mode (light = page + a little black; dark = page + a
1287
+ little white). Small even steps keep surfaces close to the page so text contrast holds even at
1288
+ L3 — the neutral ramp's uneven 100→200 jump drifted too far toward mid-grey.
1289
+ Declared on EVERY mode-boundary selector (:root, .light, .dark), NOT just :root: the formula
1290
+ derives from --ui-bg/--ui-text-highlighted, and a custom property computes ONCE where it's declared
1291
+ then inherits that fixed colour. On :root alone, a nested force-light island (marketing) or
1292
+ force-dark island re-lights --ui-bg but inherits :root's already-resolved surface — so a .light
1293
+ marketing card stayed DARK on a dark page. Re-declaring here makes the island recompute with its
1294
+ own --ui-bg. */
1295
+ :root, .light, .dark {
1296
+ --forest-card-l1: color-mix(in oklab, var(--ui-bg), var(--ui-text-highlighted) 3%);
1297
+ --forest-card-l2: color-mix(in oklab, var(--ui-bg), var(--ui-text-highlighted) 8%);
1298
+ --forest-card-l3: color-mix(in oklab, var(--ui-bg), var(--ui-text-highlighted) 13%);
1299
+ }
1300
+ /* Map — WHICH PALETTE OWNS WHICH REGION (José, 2026-07-26). Two rules, and they do not overlap:
1301
+ the DISC and the BADGE take the VIVID set (plus neutrals). They are categorical: they say WHICH
1302
+ kind of thing this is, and vivid separates further than the CVD-gated set does at marker size.
1303
+ the RING and the DOT take SEMANTIC colours. They say how BAD something is — charge and
1304
+ connectivity — so they must move with --ui-warning / --ui-error / --ui-success like every other
1305
+ warning in the system.
1306
+ Aliased here rather than used raw: `--dataviz-categorical-vivid-*` names a position in a chart palette,
1307
+ which tells a layer nothing about meaning, and re-pointing one would mean editing every call site.
1308
+ ⚠ Measured against the real resolved tokens (canvas readback, both modes). Two collisions are live and
1309
+ accepted — see MAPS-SPEC, "accepted collisions": functional-disc vs the high-charge ring (ΔE 17 light / 24 dark, so the
1310
+ border goes quiet on a charged functional bike), and damage-disc vs the critical ring and the offline dot
1311
+ (ΔE 7 in dark — same red; the dot's own outline is what keeps it readable). */
1312
+ :root {
1313
+ --forest-map-functional: var(--dataviz-categorical-vivid-green);
1314
+ --forest-map-damage: var(--dataviz-categorical-vivid-red);
1315
+ /* ⚠ AMBER, AND THE RING OWNS AMBER (José, 2026-07-26, with the numbers in front of him). Measured against
1316
+ --ui-warning: ΔE 9 light / 15 dark. A hold-family vehicle on a LOW charge therefore reads as ONE amber
1317
+ shape — the ring merges into the disc and the charge signal is lost on that group. This is a deliberate
1318
+ accepted collision, not an oversight; `pink` (ΔE 37) was the measured alternative and was declined. */
1319
+ --forest-map-hold: var(--dataviz-categorical-vivid-amber);
1320
+ --forest-map-lost: var(--dataviz-categorical-vivid-purple);
1321
+ /* `parked` is the one disc that is NOT vivid: off the street, accounted for, nobody looking for it — and a
1322
+ categorical palette has no neutral. Its OWN token rather than `--ui-text-dimmed`, which is tuned for type
1323
+ on a page: a disc is a surface carrying a glyph, and borrowing a text role tied it to a paragraph
1324
+ decision. The step flips by mode because the ramp does not, and a 500 grey that reads as inert on a light
1325
+ basemap disappears into a dark one. */
1326
+ /* `parked` is inventory — off the street and accounted for — and it takes SKY rather than the old neutral:
1327
+ with six groups the neutral was the only one that did not look like a decision. */
1328
+ --forest-map-parked: var(--dataviz-categorical-vivid-sky);
1329
+ /* ★ `retired` is its OWN group and its own value: the inverted colour, so a dead vehicle is the one mark that
1330
+ is not a hue at all. It flips with the mode, which is the point — it stays maximally separated from the page
1331
+ in both. ⚠ ΔE 0 against the locked pause mark, which is also the inverted colour; harmless only because a
1332
+ retired vehicle cannot be on a rental. */
1333
+ --forest-map-retired: var(--ui-bg-inverted);
1334
+ /* ★ A PLACE'S COLOUR IS ITS CONDITION, NOT ITS KIND (José). The glyph says what it is — the Mohr P, or a
1335
+ house — and the colour says what state it is in. A condition is a SEVERITY, so it takes semantic roles for
1336
+ the same reason the battery ring does, and colour-by-kind was making the colour and the glyph say the same
1337
+ thing twice with nowhere left to put "suspended".
1338
+ ★ It also keeps the two palettes apart by construction: vehicle discs are CATEGORICAL and take the vivid
1339
+ set; place squares are SEVERITY and take these. Before this, sky was both a bay and a `parked` vehicle. */
1340
+ /* ★ A WAREHOUSE IS OFF THE SEVERITY SCALE, not at the good end of it (José). It takes the inverted colour,
1341
+ which is not a semantic role at all — so it does not claim a condition it cannot have. Same move as
1342
+ `retired` on the vehicle disc: the one value that is not a hue is how the set says "this is not on the
1343
+ scale the others are on". A bay's colour is its condition; a warehouse's colour says it has none. */
1344
+ --forest-map-warehouse: var(--ui-bg-inverted);
1345
+ --forest-map-place-ok: var(--ui-info);
1346
+ --forest-map-place-suspended: var(--ui-warning);
1347
+ --forest-map-place-removed: var(--ui-error);
1348
+ /* Badges: measured as the two vivid hues furthest from everything already on the mark — sky is 46/49
1349
+ from the nearest disc and 92/98 from the ring; pink is 37/38 and 37/42. */
1350
+ /* ⚠ The badge has NO tokens of its own any more. It draws from the vivid set plus the inverted colour, by
1351
+ name, through BADGE_COLOURS in map/vehicle.ts — five `--forest-map-badge-N` aliases named positions in a
1352
+ palette rather than meanings, and three of them pointed outside the categorical set entirely. */
1353
+
1354
+ /* IN USE — the rental axis, and the only disc that is a GRADIENT. Vivid amber into vivid green: chosen by
1355
+ searching 824 ramp pairs on the WORST distance to everything else on the mark (five discs, three ring
1356
+ bands), where this pair scores ΔE 42.7 against a floor of 33. The winners are all OLIVE means, because
1357
+ an olive sits between the green cluster and the amber/red ring cluster — a position no pure green and no
1358
+ pure warm can hold at once.
1359
+ ⚠ Brightness here is capped by the GLYPH, not by the palette: the pine is painted in the background
1360
+ colour, so above roughly L 60 the disc's mean drops under 3:1 against it and the mark reads as a blank
1361
+ chip with a ghost in it. This pair sits at L 54.7 / 3.70.
1362
+ ★ VERIFIED on the published style (mapbox-gl 3.24.1): the gradient renders and substitution reaches
1363
+ stop-color. -inuse-flat is now an OPTION rather than a fallback — point BOTH stops at it for a flat
1364
+ in-use disc, which measures better on separation (44.7) and changes nothing else. */
1365
+ --forest-map-inuse-from: var(--dataviz-categorical-vivid-amber);
1366
+ --forest-map-inuse-to: var(--dataviz-categorical-vivid-green);
1367
+ --forest-map-inuse-flat: var(--acid-700);
1368
+
1369
+ /* The pause mark a locked rental puts in the bottom-right corner. INK: the rental axis is neither a
1370
+ category among the state groups nor a severity, so it takes neither palette — and ink is the strongest
1371
+ value against every disc colour, including the gradient it sits on. */
1372
+ --forest-map-locked: var(--ui-bg-inverted);
1373
+ }
1374
+
1375
+
1376
+ /* Zones — a region, not a mark, so these are a FILL plus a LINE (see map/zone.ts).
1377
+ ★ The fill is faint by design: a zone covers a large part of the screen, and the vehicles inside it are
1378
+ what the map is for. The LINE carries the boundary, which is the part an operator actually reads —
1379
+ "where does the rule stop" — so it takes the saturated value and the fill takes 10–12% alpha of it.
1380
+ ★ COLOUR IS WHAT THE RULE DOES TO A RENTAL (José, 2026-07-26), and it is a SEVERITY, so it takes the
1381
+ semantic roles rather than the categorical set. Green permits, amber limits, red forbids. Riding is not
1382
+ the axis — parking is: a low-speed zone limits the ride and leaves parking alone, which is why it is
1383
+ amber and not red.
1384
+ ★ The BUSINESS ZONE IS INK, not a hue. It is the container every other zone nests in, and it makes no
1385
+ claim about behaviour inside itself — the map is the service area, and its line is where the service
1386
+ stops. Ink flips with the mode, so the outermost boundary stays maximally separated from the page in
1387
+ both. Same move as `retired` on the vehicle disc and `warehouse` on the place square.
1388
+ ★ The EXCLUSION IS GREEN, and green is correct rather than ironic: the pocket PERMITS parking inside a
1389
+ zone that forbids it. It is dashed and operations-only, so it cannot be read as a business boundary.
1390
+ ★ The EVENT ZONE IS PINK — next to red in hue, because an event zone IS a no-parking zone, and one step
1391
+ away from it, because it is temporary. The dash carries the schedule; the hue carries the kinship. Blue
1392
+ was the alternative and was declined: it reads permissive, and this zone forbids.
1393
+ ⚠ Three of these are spoken for elsewhere on a map: --ui-warning is a hold-family disc and a low charge,
1394
+ --ui-error is damage and a critical charge, vivid pink is a badge. All of those are MARKS ON TOP of a
1395
+ faint field and shape resolves before colour — but the zone LINE is saturated, so the pairs are measured
1396
+ on /maps/zones rather than assumed. */
1397
+ :root {
1398
+ --forest-map-zone: var(--ui-bg-inverted);
1399
+ --forest-map-zone-low-speed: var(--ui-warning);
1400
+ --forest-map-zone-no-parking: var(--ui-error);
1401
+ --forest-map-zone-event: var(--dataviz-categorical-vivid-pink);
1402
+ --forest-map-zone-exclusion: var(--ui-primary);
1403
+ }
1404
+ /* Drawing — the shape a reader is authoring right now, which is a different kind of thing from every
1405
+ other mark on the map: it is not data, it is an unfinished sentence.
1406
+ ★ It is PRIMARY, not a zone hue. A draft has no kind yet — the operator has not said whether they are
1407
+ drawing a no-parking zone or an event — so painting it in one would be a claim the geometry does not
1408
+ make. Primary says "this is the thing you are doing", which is exactly what it is.
1409
+ ★ The HANDLE IS THE PAGE COLOUR, ringed in the draw colour. A grabbable disc has to read as an object
1410
+ sitting above the map rather than a dot painted on it, and a filled disc in the draw colour reads as a
1411
+ vertex of the shape instead of a control over it. Flipping with the mode keeps that separation in both.
1412
+ ★ INVALID IS ERROR, and the ring keeps painting in it. A self-crossing draft that vanished at the moment
1413
+ of the mistake would take the reader's work with it. */
1414
+ :root {
1415
+ --forest-map-draw: var(--ui-primary);
1416
+ --forest-map-draw-invalid: var(--ui-error);
1417
+ --forest-map-draw-handle: var(--ui-bg);
1418
+ }
1419
+ /* Balance and density paint — see map/density.ts and map/heat.ts.
1420
+ ★ THE SEQUENTIAL RAMP IS NOT ALIASED HERE, DELIBERATELY. A bin map samples the engine's own
1421
+ `--dataviz-sequential-forest-*` through map/ramp.ts, which also reverses the order after dark. This
1422
+ block used to restate five of those positions; a seven-bucket map then asked for a sixth that did not
1423
+ exist, and an undefined var in a GL paint property falls back to the inherited colour instead of
1424
+ failing — London's densest cells painted grey under a green legend. Sampling cannot miss a stop.
1425
+ The DIVERGING set below stays aliased, because its dark centre is not a position on any ramp. */
1426
+ /* Balance — a SIGNED gap against an expectation. The ARMS are sampled from the engine's diverging
1427
+ families by map/ramp.ts (blueOrange by default, and it is a prop). Only the CENTRE lives here.
1428
+ ★ THE CENTRE IS THE ONE COLOUR NOT ON ANY RAMP. Stop 4 means "no meaningful gap", so it has to
1429
+ vanish against the basemap in BOTH modes. Every family's own centre is a pale neutral — right on a
1430
+ light ground, wrong on a dark one, where it would make "nothing to report" the loudest thing on the
1431
+ map. --ui-bg-muted is near-white in light and a dark neutral in dark, so it disappears in both.
1432
+ ⚠ NOT --ui-bg: measured on the running app, that resolves to the same near-white in BOTH modes, so a
1433
+ dark centre pointed at it paints white over the night basemap. */
1434
+ :root {
1435
+ --forest-map-balance-centre: var(--ui-bg-muted);
1436
+ }
1437
+ /* subtle's ring/divide: RELATIVE translucent hairline (not a fixed ramp token, which collides with
1438
+ the stepped fill). rgb, NOT color-mix — color-mix doesn't paint in box-shadow/ring here. */
1439
+ :root:not(.dark), .light { --forest-card-ring: rgb(0 0 0 / 0.09); }
1440
+ .dark { --forest-card-ring: rgb(255 255 255 / 0.12); }
1441
+ /* Input recess line — the solid inset top edge that makes a field read as pressed INTO the page (see
1442
+ themes/input.ts). It must always be DARKER than the field, in both modes: a light neutral ramp step
1443
+ works on a white field but glares on a dark one. rgb, NOT color-mix — color-mix doesn't paint in
1444
+ box-shadow here. Light keeps the neutral-100 tone; dark drops to a translucent black. */
1445
+ :root:not(.dark), .light { --forest-input-recess: rgb(0 0 0 / 0.06); }
1446
+ .dark { --forest-input-recess: rgb(0 0 0 / 0.35); }
1447
+ /* Input focus halo — the soft 3px outer glow a focused field gains behind its ring (themes/input.ts).
1448
+ The numbered ramp steps do NOT flip by mode (only the semantic roles like --ui-primary do), so a
1449
+ fixed 200 step swung from 1.39:1 against the light page to 12.61:1 against the dark one — louder
1450
+ than the ring it sits behind, inverting the hierarchy. Each mode therefore picks its own step:
1451
+ light stays at 200 (~1.3-1.5:1), dark drops to 800 (~1.9-2.1:1) — a hair more present than light,
1452
+ which a glow needs on a dark surface, but still soft. Same soft-tint-light / deep-tone-dark shape
1453
+ the --forest-card-* accents use. NEUTRAL is the exception: its ladder is the deep near-black one
1454
+ (see COLOUR-DECISIONS.md), so 800 collapses onto the page (1.19:1) — it takes 600 (2.32:1). */
1455
+ :root:not(.dark), .light {
1456
+ --forest-halo-primary: var(--ui-color-primary-200);
1457
+ --forest-halo-secondary: var(--ui-color-secondary-200);
1458
+ --forest-halo-success: var(--ui-color-success-200);
1459
+ --forest-halo-info: var(--ui-color-info-200);
1460
+ --forest-halo-warning: var(--ui-color-warning-200);
1461
+ --forest-halo-error: var(--ui-color-error-200);
1462
+ --forest-halo-forest: var(--ui-color-forest-200);
1463
+ --forest-halo-maple: var(--ui-color-maple-200);
1464
+ --forest-halo-river: var(--ui-color-river-200);
1465
+ --forest-halo-warm: var(--ui-color-warm-200);
1466
+ --forest-halo-acid: var(--ui-color-acid-200);
1467
+ --forest-halo-neutral: var(--ui-color-neutral-200);
1468
+ }
1469
+ .dark {
1470
+ --forest-halo-primary: var(--ui-color-primary-800);
1471
+ --forest-halo-secondary: var(--ui-color-secondary-800);
1472
+ --forest-halo-success: var(--ui-color-success-800);
1473
+ --forest-halo-info: var(--ui-color-info-800);
1474
+ --forest-halo-warning: var(--ui-color-warning-800);
1475
+ --forest-halo-error: var(--ui-color-error-800);
1476
+ --forest-halo-forest: var(--ui-color-forest-800);
1477
+ --forest-halo-maple: var(--ui-color-maple-800);
1478
+ --forest-halo-river: var(--ui-color-river-800);
1479
+ --forest-halo-warm: var(--ui-color-warm-800);
1480
+ --forest-halo-acid: var(--ui-color-acid-800);
1481
+ --forest-halo-neutral: var(--ui-color-neutral-600);
1482
+ }
1483
+ /* Coloured recess — for an EXPLICIT colour intent the recessed depth line is the intent's own ring
1484
+ colour (the 500 step, same hue the focus ring / border uses) at low alpha, so the line reads as a
1485
+ translucent tint of the field's own accent rather than a separate tone. Only applied while the ring
1486
+ is coloured (the `highlight` state, in themes/input*.ts) — a plain rest field keeps the neutral line.
1487
+ Relative-colour `oklch(from … l c h / a)` resolves to a concrete colour that PAINTS in box-shadow (a
1488
+ bare `color-mix()` does not). Dark carries a touch more alpha so the tint holds against the darker
1489
+ field. neutral is omitted — its highlight ring is the inverted (near-ink) tone, which the default
1490
+ neutral recess line already matches. */
1491
+ :root:not(.dark), .light {
1492
+ --forest-recess-primary: oklch(from var(--ui-color-primary-500) l c h / 0.22);
1493
+ --forest-recess-secondary: oklch(from var(--ui-color-secondary-500) l c h / 0.22);
1494
+ --forest-recess-success: oklch(from var(--ui-color-success-500) l c h / 0.22);
1495
+ --forest-recess-info: oklch(from var(--ui-color-info-500) l c h / 0.22);
1496
+ --forest-recess-warning: oklch(from var(--ui-color-warning-500) l c h / 0.22);
1497
+ --forest-recess-error: oklch(from var(--ui-color-error-500) l c h / 0.22);
1498
+ --forest-recess-forest: oklch(from var(--ui-color-forest-500) l c h / 0.22);
1499
+ --forest-recess-maple: oklch(from var(--ui-color-maple-500) l c h / 0.22);
1500
+ --forest-recess-river: oklch(from var(--ui-color-river-500) l c h / 0.22);
1501
+ --forest-recess-warm: oklch(from var(--ui-color-warm-500) l c h / 0.22);
1502
+ --forest-recess-acid: oklch(from var(--ui-color-acid-500) l c h / 0.22);
1503
+ }
1504
+ .dark {
1505
+ --forest-recess-primary: oklch(from var(--ui-color-primary-500) l c h / 0.35);
1506
+ --forest-recess-secondary: oklch(from var(--ui-color-secondary-500) l c h / 0.35);
1507
+ --forest-recess-success: oklch(from var(--ui-color-success-500) l c h / 0.35);
1508
+ --forest-recess-info: oklch(from var(--ui-color-info-500) l c h / 0.35);
1509
+ --forest-recess-warning: oklch(from var(--ui-color-warning-500) l c h / 0.35);
1510
+ --forest-recess-error: oklch(from var(--ui-color-error-500) l c h / 0.35);
1511
+ --forest-recess-forest: oklch(from var(--ui-color-forest-500) l c h / 0.35);
1512
+ --forest-recess-maple: oklch(from var(--ui-color-maple-500) l c h / 0.35);
1513
+ --forest-recess-river: oklch(from var(--ui-color-river-500) l c h / 0.35);
1514
+ --forest-recess-warm: oklch(from var(--ui-color-warm-500) l c h / 0.35);
1515
+ --forest-recess-acid: oklch(from var(--ui-color-acid-500) l c h / 0.35);
1516
+ }
1517
+ /* Expressiveness traits (--fx-*) — the per-context loudness knobs, documented in
1518
+ EXPRESSIVENESS-MODES-RESEARCH.md. Defaults = the restrained web voice, so an unwrapped tree renders
1519
+ exactly as before these existed. A context (mobile/marketing — see the *-theme.ts deltas) re-points
1520
+ them on SLOT classes inside its <UTheme> delta, never on a wrapper element: UTheme rides Vue
1521
+ provide/inject so slot-attached values survive teleported overlays; a DOM-scoped var would not. */
1522
+ :root {
1523
+ --fx-title-family: var(--font-sans); /* card/section titles — GT Haptik; marketing flips to Mohr */
1524
+ --fx-title-transform: none; /* marketing pairs Mohr with uppercase */
1525
+ /* KPI figures. GT Haptik at rest because Mohr ships no tnum — measured, its "1" is 13.01px against
1526
+ its "0" at 23.66px — so a column of figures set in it never lines up and a figure updating in
1527
+ place jitters as it changes width. The expressive contexts (mobile, marketing) flip it back:
1528
+ there the number is a statement, not something read down a column. */
1529
+ --fx-metric-family: var(--font-sans);
1530
+ --fx-metric-transform: none; /* Mohr is always uppercase; the sans cut is not */
1531
+ }
1532
+
1533
+ /* Marketing accent surfaces — tinted cards off the brand ramps (featured/promo/marketing), NOT part
1534
+ of the neutral nesting ladder. Each hue mode-flips like the inverted surface: light = a soft tint
1535
+ (ramp-50 lifted ~20% toward ramp-300), dark = the deep ramp-950 tone — a light tint would glare on
1536
+ a dark page. The tint is fixed across contexts — the soft tint IS the accents' character; a louder
1537
+ context reads bolder through shape + the Mohr title, not a deeper tint. Ring = a translucent
1538
+ ramp-500/300 hairline. Neutral ink reads on both (dark text on the light tint, near-white title on
1539
+ the deep tone). Hues: peach=warm · mint=forest · sky=river · amber=amber. Add a hue by cloning one
1540
+ --forest-card-<hue>-* pair + a preset card variant. */
1541
+ :root:not(.dark), .light {
1542
+ --forest-card-peach-bg: color-mix(in oklab, var(--warm-50), var(--warm-300) 20%);
1543
+ --forest-card-peach-ring: color-mix(in oklab, var(--warm-500), transparent 82%);
1544
+ --forest-card-mint-bg: color-mix(in oklab, var(--forest-50), var(--forest-300) 20%);
1545
+ --forest-card-mint-ring: color-mix(in oklab, var(--forest-500), transparent 82%);
1546
+ --forest-card-sky-bg: color-mix(in oklab, var(--river-50), var(--river-300) 20%);
1547
+ --forest-card-sky-ring: color-mix(in oklab, var(--river-500), transparent 82%);
1548
+ --forest-card-amber-bg: color-mix(in oklab, var(--amber-50), var(--amber-300) 20%);
1549
+ --forest-card-amber-ring: color-mix(in oklab, var(--amber-500), transparent 82%);
1550
+ /* Tone-on-tone ink: title = the deep ramp step, description = a step lighter — both hue-aligned to
1551
+ the tint and readable on the light surface. Mode-flipped in .dark below. */
1552
+ --forest-card-peach-text: var(--warm-900); --forest-card-peach-muted: var(--warm-700);
1553
+ /* Handle + edge on a peach surface (mobile drawer): ONE solid brand step, no mix and no alpha —
1554
+ Bark Brown, the warm ramp's deep anchor. Both marks read as drawn ON the sheet rather than as a
1555
+ wash of it, which is why this is not the peach CARD hairline (a translucent mix at 82%): a card
1556
+ sits in the page and can whisper, a sheet floats over it. Dark flips to Acer Orange — Bark Brown
1557
+ IS the dark fill, so the mark has to come up the ramp instead of down. */
1558
+ --forest-card-peach-mark: var(--warm-bark-brown);
1559
+ --forest-card-mint-text: var(--forest-900); --forest-card-mint-muted: var(--forest-700);
1560
+ --forest-card-sky-text: var(--river-900); --forest-card-sky-muted: var(--river-700);
1561
+ --forest-card-amber-text: var(--amber-900); --forest-card-amber-muted: var(--amber-700);
1562
+ }
1563
+ .dark {
1564
+ --forest-card-peach-bg: var(--warm-950);
1565
+ --forest-card-peach-ring: color-mix(in oklab, var(--warm-300), transparent 84%);
1566
+ --forest-card-mint-bg: var(--forest-950);
1567
+ --forest-card-mint-ring: color-mix(in oklab, var(--forest-300), transparent 84%);
1568
+ --forest-card-sky-bg: var(--river-950);
1569
+ --forest-card-sky-ring: color-mix(in oklab, var(--river-300), transparent 84%);
1570
+ --forest-card-amber-bg: var(--amber-950);
1571
+ --forest-card-amber-ring: color-mix(in oklab, var(--amber-300), transparent 84%);
1572
+ /* On the deep ramp-950 tone: light hue-aligned ink. */
1573
+ --forest-card-peach-text: var(--warm-100); --forest-card-peach-muted: var(--warm-300);
1574
+ --forest-card-peach-mark: var(--warm-acer-orange);
1575
+ --forest-card-mint-text: var(--forest-100); --forest-card-mint-muted: var(--forest-300);
1576
+ --forest-card-sky-text: var(--river-100); --forest-card-sky-muted: var(--river-300);
1577
+ --forest-card-amber-text: var(--amber-100); --forest-card-amber-muted: var(--amber-300);
1578
+ }
1579
+ .forest-card { --forest-card-bg: var(--forest-card-l1); }
1580
+ .forest-card .forest-card {
1581
+ --forest-card-bg: var(--forest-card-l2);
1582
+ border-radius: calc(var(--forest-card-radius) - var(--forest-card-pad)); /* std 24→8 · mkt 32→16 */
1583
+ }
1584
+ /* One more concentric step, FLOORED at 8px. A bolder context starts from a larger outer radius
1585
+ (marketing/mobile set --forest-card-radius:2rem), so it has room for a third distinct corner
1586
+ (32→16→8) before the floor. Standard (1.5rem) already hits the floor at L2 (24→8), so max() holds
1587
+ it at 8px and nothing changes. The floor stops a 3rd step reaching 0px (square = broken). L4+ reuse
1588
+ this rule (more specific than L2), so they hold the floor too — only the fill keeps stepping. */
1589
+ /* A KPI tile joined into a divided row squares off: the group draws the corners, and a rounded
1590
+ fill inside a joined strip leaves the selected tile's underline detached at both ends. Declared
1591
+ here rather than as a utility on the tile because this file is unlayered — the concentric rule
1592
+ above outranks any `rounded-none` a component could carry. */
1593
+ .forest-card.forest-kpi-joined {
1594
+ border-radius: 0;
1595
+ }
1596
+
1597
+ .forest-card .forest-card .forest-card {
1598
+ --forest-card-bg: var(--forest-card-l3);
1599
+ border-radius: max(calc(var(--forest-card-radius) - 2 * var(--forest-card-pad)), 0.5rem);
1600
+ }
1601
+
1602
+ /* A CANVAS card opts out of the concentric ramp entirely. The ramp models a surface nested INSIDE
1603
+ another surface — each step is a card sitting on the card that contains it. Chrome floating over a
1604
+ map is not that: it sits on the CANVAS, and the fact that the canvas is itself inside a card (a map
1605
+ panel on a dashboard, or a specimen in these docs) says nothing about the corner a floating readout
1606
+ should have. Left to the ramp it stepped 24→8 and read as a different, sharper object than the side
1607
+ panel beside it.
1608
+ Its radius pairs with the side panel's instead — one corner for the surfaces on a canvas, against the
1609
+ controls' 12. Written here rather than as a utility on the component because this file is unlayered:
1610
+ the ramp above outranks any `rounded-*` a component could carry, and only a rule of equal specificity
1611
+ later in the same file can answer it. FCanvasCard adds the marker; UCard always supplies the other
1612
+ half of the selector. */
1613
+ .forest-card.forest-canvas-card {
1614
+ border-radius: var(--forest-canvas-card-radius, 1rem);
1615
+ }
1616
+
1617
+ /* Motion — MOVED. The ladder (--motion-instant/fast/base/slow/dwell, --motion-ease-out/in-out/drawer),
1618
+ the Tailwind default-transition re-point, and the system-wide reduced-motion floor now live in the
1619
+ engine, generated by scripts/motion.ts:
1620
+
1621
+ packages/tokens/motion/motion.css the ladder + the reduced-motion floor (framework-agnostic)
1622
+ packages/tokens/motion/motion.theme.css the @theme re-point of --default-transition-* (Tailwind)
1623
+ packages/tokens/motion/motion.json the same values for consumers that cannot read CSS
1624
+
1625
+ They left this file because this file is the NUXT UI brand layer — anything that cannot pull the
1626
+ preset could not have the tokens either, so every non-Tailwind surface had to restate them. Both
1627
+ generated sheets are pulled by styles/index.css and inlined by scripts/layer.ts, so every consumer
1628
+ that had motion before still has it.
1629
+
1630
+ ★ Do NOT re-add them with an `@import` here. scripts/layer.ts reads THIS FILE AS TEXT and
1631
+ concatenates it into the middle of packages/nuxt-layer/styles.gen.css, where an @import is invalid.
1632
+ The layer takes the motion sheets as their own entries in that array instead. */
1633
+
1634
+ /* ── Scrollbars ────────────────────────────────────────────────────────────────────────────────────
1635
+ Whether a scrollbar auto-hides is an OS decision, not ours. macOS "Show scroll bars: Automatic"
1636
+ means overlay on a trackpad and CLASSIC — opaque, permanent, taking width from the content — the
1637
+ moment a mouse is attached. CSS cannot turn a classic scrollbar into an overlay one. All we can do
1638
+ is make the classic case quiet, and not break the overlay case for everyone else.
1639
+
1640
+ Which rules out `::-webkit-scrollbar`. Setting a width or height on it forces a classic scrollbar
1641
+ even where the platform would have drawn an overlay, so the legacy route makes the problem WORSE
1642
+ for trackpad users. The standard properties respect the platform: on an overlay scrollbar `thin`
1643
+ still only paints the thumb while you actually scroll.
1644
+
1645
+ Two @supports gates, not one — the properties are NOT at the same support level. scrollbar-width
1646
+ is Baseline 2024 (Chrome 121 / Firefox 64 / Safari 18.2), but scrollbar-color only reached Safari
1647
+ in 26.2, so a single combined gate would withhold the thinning from a large slice of Safari.
1648
+
1649
+ `scrollbar-width` is not an inherited property, hence the universal selector; `*` carries zero
1650
+ specificity, so any component recipe still wins. `none` is deliberately not used anywhere — it
1651
+ removes the only visible affordance for mouse-drag scrolling. */
1652
+ :root:not(.dark), .light { --forest-scrollbar-thumb: var(--neutral-400); }
1653
+ .dark { --forest-scrollbar-thumb: var(--neutral-500); }
1654
+
1655
+ @supports (scrollbar-width: thin) {
1656
+ * {
1657
+ scrollbar-width: thin;
1658
+ }
1659
+
1660
+ /* Page-level only. Reserving the gutter stops the layout shifting as you move between a short
1661
+ page and a long one; putting it on every scroller instead would paint an empty lane inside
1662
+ small panels that have nothing to scroll. `stable both-edges` is avoided — Chromium doubles
1663
+ the gutter on retina displays. Inert where scrollbars overlay, which is the correct no-op. */
1664
+ html {
1665
+ scrollbar-gutter: stable;
1666
+ }
1667
+ }
1668
+
1669
+ /* Transparent track, always. The opaque lane is most of what reads as visual weight, and unlike the
1670
+ thumb it stays painted whether or not anything is scrolling. No global hover treatment: there is no
1671
+ selector for "is a scroll container", so strengthening the thumb on hover would mean a universal
1672
+ `*:hover` rule paying a style recalc across the whole tree for polish only scrollers can show.
1673
+ UScrollArea — the one component that colours its scrollbar deliberately — does carry one. */
1674
+ @supports (scrollbar-color: lime hotpink) {
1675
+ * {
1676
+ scrollbar-color: var(--forest-scrollbar-thumb) transparent;
1677
+ }
1678
+ }
1679
+
1680
+ /* Display utility — Mohr Black, always uppercase (web cut = MohrAlt, upright). */
1681
+ .font-display {
1682
+ font-family: var(--font-display);
1683
+ font-weight: 900;
1684
+ text-transform: uppercase;
1685
+ }
1686
+
1687
+ /* One-step touch promotion — for the controls that have NO baked touch ladder of their own. These
1688
+ reach mobile size by DEFAULTING to a larger `size` prop (forestMobileProps), so a touched phone
1689
+ would step twice if this fired there too: hence `:not(.cut-fixed)`, which the mobile cut stamps to
1690
+ opt out. Controls WITH a baked ladder use `touch:` below instead — same numbers under either
1691
+ trigger, so no escape is needed. Ships WITH the preset (the recipes use it, so the variant must
1692
+ travel with them; a consuming app gets it by importing this file). Replaces the built-in
1693
+ `pointer-coarse:`, which the fixed cut couldn't opt out of. */
1694
+ @custom-variant coarse {
1695
+ @media (pointer: coarse) {
1696
+ &:not(.cut-fixed) {
1697
+ @slot;
1698
+ }
1699
+ }
1700
+ /* Docs-simulation hook: a `coarse:` rule ALSO fires inside a `[data-force-coarse]` container, so
1701
+ the docs can preview the touch bump on a fine-pointer desktop (the Context switch's "Coarse
1702
+ pointer" toggle stamps the attribute via ContextScope). Lives here because a @custom-variant is
1703
+ defined once. Inert in production — no app sets this attribute; `:where()` keeps it
1704
+ zero-specificity; `:not(.cut-fixed)` is kept so a baked cut (mobile) stays put under the sim,
1705
+ exactly as on a real phone. */
1706
+ &:where([data-force-coarse] *):not(.cut-fixed) {
1707
+ @slot;
1708
+ }
1709
+ }
1710
+
1711
+ /* THE TOUCH LADDER — one set of sizes, two triggers. A control is drawn at touch size when it is
1712
+ BEING touched (coarse primary pointer) or when it is BAKED for touch (the mobile cut stamps
1713
+ `.cut-fixed`). Both land on the SAME per-size numbers — 28/36/40/44/52 for xs→xl vs the fine-pointer
1714
+ 24/28/32/36/40 — so the values are authored once, in each control's size variant, and a real phone
1715
+ cannot step twice. Every size keeps its own rung: lg is 44 (Apple's min tap target) and xl 52; this
1716
+ is a LADDER SWAP, not a shift to the next size up, which is why xl has somewhere to go.
1717
+ Consumed by the `touch:*` classes in button.ts, input.ts and the other field controls.
1718
+
1719
+ Why the baked branch exists at all: the desktop mock of the mobile app and the native/Flutter
1720
+ export have no pointer to query, so they must show true touch size on a fine-pointer machine.
1721
+
1722
+ Each branch is deliberately TWO classes (0,2,0). Stock hangs a responsive `md:text-*` shrink per
1723
+ size on the field controls; a one-class touch rule ties with it and loses on order, which flipped a
1724
+ control's type between the desktop mock and a real phone. `.cut-fixed` supplies its second class
1725
+ naturally; the pointer branches borrow one from a class that never exists. */
1726
+ @custom-variant touch {
1727
+ &.cut-fixed {
1728
+ @slot;
1729
+ }
1730
+ @media (pointer: coarse) {
1731
+ &:not(.forest-fine) {
1732
+ @slot;
1733
+ }
1734
+ }
1735
+ /* Docs-simulation hook — see `coarse:` above. `:where()` keeps the container selector at zero
1736
+ specificity so the 0,2,0 total is unchanged. Inert in production. */
1737
+ &:where([data-force-coarse] *):not(.forest-fine) {
1738
+ @slot;
1739
+ }
1740
+ }
1741
+
1742
+
1743
+ /* Forest data-visualization palette — generated by scripts/dataviz.ts. Do not edit by hand. */
1744
+ /* Three categorical variants: -safe- = the 6-colour brand-led CVD-safe set (the default, on
1745
+ --vis-color0..5); -vivid- = vivid 12 (first 6 = curated 6, label-dependent, NOT CVD-safe); -pairs- =
1746
+ brand pairs (3 hues × deep/soft, brand-forward, NOT CVD-safe). All flip light/dark. Sequential:
1747
+ invert stop order in dark mode. Diverging: darken the centre stop in dark mode. */
1748
+ :root {
1749
+ --dataviz-categorical-safe-1: #009d7c;
1750
+ --dataviz-categorical-safe-2: #cc7044;
1751
+ --dataviz-categorical-safe-3: #617de6;
1752
+ --dataviz-categorical-safe-4: #b0175f;
1753
+ --dataviz-categorical-safe-5: #8a34ab;
1754
+ --dataviz-categorical-safe-6: #5b6c00;
1755
+ --dataviz-categorical-vivid-green: #00a447;
1756
+ --dataviz-categorical-vivid-amber: #da7600;
1757
+ --dataviz-categorical-vivid-sky: #0084c8;
1758
+ --dataviz-categorical-vivid-indigo: #4f39f6;
1759
+ --dataviz-categorical-vivid-pink: #e30076;
1760
+ --dataviz-categorical-vivid-red: #e40015;
1761
+ --dataviz-categorical-vivid-emerald: #009669;
1762
+ --dataviz-categorical-vivid-orange: #ec5600;
1763
+ --dataviz-categorical-vivid-blue: #155dfc;
1764
+ --dataviz-categorical-vivid-purple: #9810fa;
1765
+ --dataviz-categorical-vivid-fuchsia: #c500da;
1766
+ --dataviz-categorical-vivid-rose: #e60044;
1767
+ --dataviz-categorical-pairs-forest-deep: #019f6d;
1768
+ --dataviz-categorical-pairs-forest-soft: #3bd996;
1769
+ --dataviz-categorical-pairs-warm-deep: #db6300;
1770
+ --dataviz-categorical-pairs-warm-soft: #feaf59;
1771
+ --dataviz-categorical-pairs-river-deep: #3588df;
1772
+ --dataviz-categorical-pairs-river-soft: #81c0ff;
1773
+ --dataviz-sequential-forest-0: #dcffee;
1774
+ --dataviz-sequential-forest-1: #bef9de;
1775
+ --dataviz-sequential-forest-2: #99e8c5;
1776
+ --dataviz-sequential-forest-3: #69d2a8;
1777
+ --dataviz-sequential-forest-4: #30b889;
1778
+ --dataviz-sequential-forest-5: #289c74;
1779
+ --dataviz-sequential-forest-6: #208361;
1780
+ --dataviz-sequential-forest-7: #186c4f;
1781
+ --dataviz-sequential-forest-8: #11563e;
1782
+ --dataviz-sequential-indigo-0: #f3f4fe;
1783
+ --dataviz-sequential-indigo-1: #e6e8fd;
1784
+ --dataviz-sequential-indigo-2: #ced0fc;
1785
+ --dataviz-sequential-indigo-3: #b1b3f8;
1786
+ --dataviz-sequential-indigo-4: #9494e8;
1787
+ --dataviz-sequential-indigo-5: #7b78da;
1788
+ --dataviz-sequential-indigo-6: #6763bb;
1789
+ --dataviz-sequential-indigo-7: #54519b;
1790
+ --dataviz-sequential-indigo-8: #43407b;
1791
+ --dataviz-sequential-moss-0: #ffffe5;
1792
+ --dataviz-sequential-moss-1: #f7fcb9;
1793
+ --dataviz-sequential-moss-2: #d9f0a3;
1794
+ --dataviz-sequential-moss-3: #addd8e;
1795
+ --dataviz-sequential-moss-4: #78c679;
1796
+ --dataviz-sequential-moss-5: #41ab5d;
1797
+ --dataviz-sequential-moss-6: #238443;
1798
+ --dataviz-sequential-moss-7: #006837;
1799
+ --dataviz-sequential-moss-8: #004529;
1800
+ --dataviz-sequential-cividis-0: #00204d;
1801
+ --dataviz-sequential-cividis-1: #00336f;
1802
+ --dataviz-sequential-cividis-2: #39486b;
1803
+ --dataviz-sequential-cividis-3: #575d6d;
1804
+ --dataviz-sequential-cividis-4: #707173;
1805
+ --dataviz-sequential-cividis-5: #8a8779;
1806
+ --dataviz-sequential-cividis-6: #a69d75;
1807
+ --dataviz-sequential-cividis-7: #c4b56c;
1808
+ --dataviz-sequential-cividis-8: #e4cf5b;
1809
+ --dataviz-sequential-cividis-9: #ffea46;
1810
+ --dataviz-sequential-viridis-0: #440154;
1811
+ --dataviz-sequential-viridis-1: #482878;
1812
+ --dataviz-sequential-viridis-2: #3e4a89;
1813
+ --dataviz-sequential-viridis-3: #31688e;
1814
+ --dataviz-sequential-viridis-4: #26828e;
1815
+ --dataviz-sequential-viridis-5: #1f9e89;
1816
+ --dataviz-sequential-viridis-6: #35b779;
1817
+ --dataviz-sequential-viridis-7: #6dcd59;
1818
+ --dataviz-sequential-viridis-8: #b4de2c;
1819
+ --dataviz-sequential-viridis-9: #fde725;
1820
+ --dataviz-sequential-magma-0: #000004;
1821
+ --dataviz-sequential-magma-1: #1c1044;
1822
+ --dataviz-sequential-magma-2: #4f127b;
1823
+ --dataviz-sequential-magma-3: #812581;
1824
+ --dataviz-sequential-magma-4: #b5367a;
1825
+ --dataviz-sequential-magma-5: #e65164;
1826
+ --dataviz-sequential-magma-6: #fb8861;
1827
+ --dataviz-sequential-magma-7: #fec287;
1828
+ --dataviz-sequential-magma-8: #fcfdbf;
1829
+ --dataviz-diverging-forest-warm-0: #155e42;
1830
+ --dataviz-diverging-forest-warm-1: #238862;
1831
+ --dataviz-diverging-forest-warm-2: #38b484;
1832
+ --dataviz-diverging-forest-warm-3: #69deac;
1833
+ --dataviz-diverging-forest-warm-4: #f6ece8;
1834
+ --dataviz-diverging-forest-warm-5: #fab18b;
1835
+ --dataviz-diverging-forest-warm-6: #dc804f;
1836
+ --dataviz-diverging-forest-warm-7: #b05925;
1837
+ --dataviz-diverging-forest-warm-8: #7d3a11;
1838
+ --dataviz-diverging-blue-orange-0: #12518d;
1839
+ --dataviz-diverging-blue-orange-1: #3377be;
1840
+ --dataviz-diverging-blue-orange-2: #5b9fe9;
1841
+ --dataviz-diverging-blue-orange-3: #9dc8f9;
1842
+ --dataviz-diverging-blue-orange-4: #f6ece8;
1843
+ --dataviz-diverging-blue-orange-5: #fab18b;
1844
+ --dataviz-diverging-blue-orange-6: #dc804f;
1845
+ --dataviz-diverging-blue-orange-7: #b05925;
1846
+ --dataviz-diverging-blue-orange-8: #7d3a11;
1847
+ --dataviz-diverging-purple-green-0: #673682;
1848
+ --dataviz-diverging-purple-green-1: #8e5bab;
1849
+ --dataviz-diverging-purple-green-2: #b682d5;
1850
+ --dataviz-diverging-purple-green-3: #ddaff9;
1851
+ --dataviz-diverging-purple-green-4: #e8f1ec;
1852
+ --dataviz-diverging-purple-green-5: #69deac;
1853
+ --dataviz-diverging-purple-green-6: #38b484;
1854
+ --dataviz-diverging-purple-green-7: #238862;
1855
+ --dataviz-diverging-purple-green-8: #155e42;
1856
+ --dataviz-diverging-red-blue-0: #b2182b;
1857
+ --dataviz-diverging-red-blue-1: #d6604d;
1858
+ --dataviz-diverging-red-blue-2: #f4a582;
1859
+ --dataviz-diverging-red-blue-3: #fddbc7;
1860
+ --dataviz-diverging-red-blue-4: #f7f7f7;
1861
+ --dataviz-diverging-red-blue-5: #d1e5f0;
1862
+ --dataviz-diverging-red-blue-6: #92c5de;
1863
+ --dataviz-diverging-red-blue-7: #4393c3;
1864
+ --dataviz-diverging-red-blue-8: #2166ac;
1865
+ }
1866
+ .dark {
1867
+ --dataviz-categorical-safe-1: #029e72;
1868
+ --dataviz-categorical-safe-2: #d46b01;
1869
+ --dataviz-categorical-safe-3: #89b0ff;
1870
+ --dataviz-categorical-safe-4: #ff9cb2;
1871
+ --dataviz-categorical-safe-5: #a663ac;
1872
+ --dataviz-categorical-safe-6: #becf68;
1873
+ --dataviz-categorical-vivid-green: #05df72;
1874
+ --dataviz-categorical-vivid-amber: #fabc00;
1875
+ --dataviz-categorical-vivid-sky: #00bbfd;
1876
+ --dataviz-categorical-vivid-indigo: #7d87ff;
1877
+ --dataviz-categorical-vivid-pink: #fb64b6;
1878
+ --dataviz-categorical-vivid-red: #ff6568;
1879
+ --dataviz-categorical-vivid-emerald: #00d294;
1880
+ --dataviz-categorical-vivid-orange: #ff8b1e;
1881
+ --dataviz-categorical-vivid-blue: #56a2ff;
1882
+ --dataviz-categorical-vivid-purple: #bf7eff;
1883
+ --dataviz-categorical-vivid-fuchsia: #ec6dff;
1884
+ --dataviz-categorical-vivid-rose: #ff6880;
1885
+ --dataviz-categorical-pairs-forest-deep: #0abb7f;
1886
+ --dataviz-categorical-pairs-forest-soft: #99e9bc;
1887
+ --dataviz-categorical-pairs-warm-deep: #f48519;
1888
+ --dataviz-categorical-pairs-warm-soft: #ffd6a6;
1889
+ --dataviz-categorical-pairs-river-deep: #43a2ff;
1890
+ --dataviz-categorical-pairs-river-soft: #b2d9ff;
1891
+ }
1892
+
1893
+
1894
+ /* Forest → Unovis adapter — generated by scripts/dataviz.ts. Do not edit by hand. */
1895
+ /* var() indirection, plus the one vendor rule that cannot be expressed as a var. Import AFTER
1896
+ dataviz.css and the semantic theme. */
1897
+ /* :root:root — Unovis injects its own :root defaults at runtime (after this sheet loads), so we
1898
+ need to win on specificity, not order. */
1899
+ :root:root {
1900
+ --vis-color0: var(--dataviz-categorical-safe-1);
1901
+ --vis-color1: var(--dataviz-categorical-safe-2);
1902
+ --vis-color2: var(--dataviz-categorical-safe-3);
1903
+ --vis-color3: var(--dataviz-categorical-safe-4);
1904
+ --vis-color4: var(--dataviz-categorical-safe-5);
1905
+ --vis-color5: var(--dataviz-categorical-safe-6);
1906
+ --vis-font-family: var(--font-sans);
1907
+ --vis-dark-font-family: var(--font-sans);
1908
+ --vis-axis-grid-color: var(--ui-border);
1909
+ --vis-dark-axis-grid-color: var(--ui-border);
1910
+ --vis-axis-tick-color: var(--ui-border);
1911
+ --vis-dark-axis-tick-color: var(--ui-border);
1912
+ --vis-axis-tick-label-color: var(--ui-text-muted);
1913
+ --vis-dark-axis-tick-label-color: var(--ui-text-muted);
1914
+ --vis-axis-label-color: var(--ui-text-muted);
1915
+ --vis-dark-axis-label-color: var(--ui-text-muted);
1916
+ --vis-legend-label-color: var(--ui-text);
1917
+ --vis-dark-legend-label-color: var(--ui-text);
1918
+ --vis-tooltip-background-color: color-mix(in oklab, var(--ui-bg-elevated) 80%, transparent);
1919
+ --vis-dark-tooltip-background-color: color-mix(in oklab, var(--ui-bg-elevated) 80%, transparent);
1920
+ --vis-tooltip-backdrop-filter: blur(12px) saturate(1.4);
1921
+ --vis-dark-tooltip-backdrop-filter: blur(12px) saturate(1.4);
1922
+ --vis-tooltip-border-color: var(--ui-border);
1923
+ --vis-dark-tooltip-border-color: var(--ui-border);
1924
+ --vis-tooltip-text-color: var(--ui-text-highlighted);
1925
+ --vis-dark-tooltip-text-color: var(--ui-text-highlighted);
1926
+ --vis-tooltip-border-radius: var(--ui-radius);
1927
+ --vis-dark-tooltip-border-radius: var(--ui-radius);
1928
+ --vis-tooltip-padding: 0;
1929
+ --vis-dark-tooltip-padding: 0;
1930
+ --vis-crosshair-line-stroke-color: var(--ui-text-dimmed);
1931
+ --vis-dark-crosshair-line-stroke-color: var(--ui-text-dimmed);
1932
+ --vis-donut-background-color: transparent;
1933
+ --vis-dark-donut-background-color: transparent;
1934
+ --vis-donut-central-label-font-size: 28px;
1935
+ --vis-dark-donut-central-label-font-size: 28px;
1936
+ --vis-donut-central-label-font-weight: 700;
1937
+ --vis-dark-donut-central-label-font-weight: 700;
1938
+ --vis-donut-central-label-text-color: var(--ui-text-highlighted);
1939
+ --vis-dark-donut-central-label-text-color: var(--ui-text-highlighted);
1940
+ --vis-donut-central-sub-label-font-size: 13px;
1941
+ --vis-dark-donut-central-sub-label-font-size: 13px;
1942
+ --vis-donut-central-sub-label-text-color: var(--ui-text-muted);
1943
+ --vis-dark-donut-central-sub-label-text-color: var(--ui-text-muted);
1944
+ --vis-nested-donut-central-label-font-size: 20px;
1945
+ --vis-dark-nested-donut-central-label-font-size: 20px;
1946
+ --vis-nested-donut-central-label-font-weight: 700;
1947
+ --vis-dark-nested-donut-central-label-font-weight: 700;
1948
+ --vis-nested-donut-central-label-text-color: var(--ui-text-highlighted);
1949
+ --vis-dark-nested-donut-central-label-text-color: var(--ui-text-highlighted);
1950
+ --vis-nested-donut-central-sublabel-text-color: var(--ui-text-muted);
1951
+ --vis-dark-nested-donut-central-sublabel-text-color: var(--ui-text-muted);
1952
+ --vis-nested-donut-background-color: transparent;
1953
+ --vis-dark-nested-donut-background-color: transparent;
1954
+ --vis-nested-donut-segment-stroke-color: var(--ui-bg);
1955
+ --vis-dark-nested-donut-segment-stroke-color: var(--ui-bg);
1956
+ --vis-nested-donut-segment-stroke-width: 3px;
1957
+ --vis-dark-nested-donut-segment-stroke-width: 3px;
1958
+ --vis-timeline-row-even-fill-color: var(--ui-bg);
1959
+ --vis-dark-timeline-row-even-fill-color: var(--ui-bg);
1960
+ --vis-timeline-row-odd-fill-color: var(--ui-bg-muted);
1961
+ --vis-dark-timeline-row-odd-fill-color: var(--ui-bg-muted);
1962
+ --vis-timeline-label-color: var(--ui-text-muted);
1963
+ --vis-dark-timeline-label-color: var(--ui-text-muted);
1964
+ --vis-timeline-scrollbar-background-color: var(--ui-bg-muted);
1965
+ --vis-dark-timeline-scrollbar-background-color: var(--ui-bg-muted);
1966
+ --vis-timeline-scrollbar-color: var(--ui-border-accented);
1967
+ --vis-dark-timeline-scrollbar-color: var(--ui-border-accented);
1968
+ --vis-sankey-node-label-color: var(--ui-text-highlighted);
1969
+ --vis-dark-sankey-node-label-color: var(--ui-text-highlighted);
1970
+ --vis-sankey-node-label-background-fill-color: var(--ui-bg);
1971
+ --vis-dark-sankey-node-label-background-fill-color: var(--ui-bg);
1972
+ --vis-sankey-node-label-background-stroke-color: var(--ui-border);
1973
+ --vis-dark-sankey-node-label-background-stroke-color: var(--ui-border);
1974
+ --vis-sankey-icon-color: var(--ui-bg);
1975
+ --vis-dark-sankey-icon-color: var(--ui-bg);
1976
+ --vis-treemap-internal-label-font-size: 16px;
1977
+ --vis-dark-treemap-internal-label-font-size: 16px;
1978
+ --vis-treemap-tile-radius: 12px;
1979
+ --vis-dark-treemap-tile-radius: 12px;
1980
+ --vis-treemap-tile-gap: 4px;
1981
+ --vis-dark-treemap-tile-gap: 4px;
1982
+ --vis-tooltip-title-color: var(--ui-text-highlighted);
1983
+ --vis-tooltip-title-border-bottom: 1px solid var(--ui-border);
1984
+ --vis-tooltip-label-color: var(--ui-text);
1985
+ --vis-tooltip-value-color: var(--ui-text-highlighted);
1986
+ }
1987
+
1988
+ /* Treemap concentric corners. Unovis draws group and leaf tiles as flat sibling rects sharing one
1989
+ rx, so nesting gets no radius of its own and equal radii read as pinched inner corners. The
1990
+ concentric rule is inner = outer − gap: with the house 8px corner and 4px padding, leaves take
1991
+ 4px. Leaves are the only tiles carrying the clickable class (treemapDefaults turns the affordance
1992
+ on for exactly this reason); rx/ry are SVG geometry properties, so CSS overrides the attribute
1993
+ Unovis sets. Matched on the emotion class suffix — Unovis hashes the prefix per build, the label
1994
+ is stable. Separation comes from the padding gap showing the darker group tile beneath, not from
1995
+ a stroke: a stroke on every tile double-borders the nesting. */
1996
+ rect[class*="-clickable-tile"] {
1997
+ rx: calc(var(--vis-treemap-tile-radius, 8px) - var(--vis-treemap-tile-gap, 4px));
1998
+ ry: calc(var(--vis-treemap-tile-radius, 8px) - var(--vis-treemap-tile-gap, 4px));
1999
+ }
2000
+
2001
+ /* Re-centre the central label pair. Unovis anchors both texts on the hole's centre and separates
2002
+ them with dy ±0.55em on each text's OWN font-size, so the block's optical centre always sits
2003
+ above the true centre — and the gap widens with the label size. Shifting by a fraction of the
2004
+ label size restores the centring and keeps it correct if a chart overrides the size locally.
2005
+ dy is an attribute CSS cannot reach; translate composes with the transform attribute Unovis
2006
+ sets, so it is the one lever here.
2007
+
2008
+ The two components need separate rules because they disagree twice. The flat donut sizes its
2009
+ label from --vis-donut-*, the nested one from --vis-nested-donut-*, so one shared rule shifts
2010
+ the nested label by half the FLAT donut's 28px. And they class their texts differently: the flat
2011
+ donut gives BOTH texts the -central-label suffix, while the nested one names its second
2012
+ -central-sub-label. Matching only the first suffix therefore moved the nested label down onto a
2013
+ sub-label that stayed put. Both texts of a pair must shift by the same amount — that is what preserves
2014
+ their spacing while re-centring the block. (No backticks in this comment: it lives inside a
2015
+ template literal.) */
2016
+ g[class*="-donut-component"]:not([class*="-nested-donut-component"]) text[class*="-central-label"] {
2017
+ translate: 0 calc(0.5 * var(--vis-donut-central-label-font-size, 16px));
2018
+ }
2019
+
2020
+ g[class*="-nested-donut-component"] text[class*="-central-label"],
2021
+ g[class*="-nested-donut-component"] text[class*="-central-sub-label"] {
2022
+ translate: 0 calc(0.5 * var(--vis-nested-donut-central-label-font-size, 16px));
2023
+ }
2024
+
2025
+ /* Group labels get their own size. Unovis scales label type to tile area for LEAVES only
2026
+ (enableTileLabelFontSizeVariation, gated on the node having no children), so an internal node
2027
+ keeps the flat default
2028
+ however large its group is — a zone heading ends up smaller than the station labels inside it.
2029
+ Internal nodes carry their own class, so the heading can be sized here; it has to stay within
2030
+ the tilePaddingTop strip the group reserves for it. */
2031
+ text[class*="-internal-label"] {
2032
+ font-size: var(--vis-treemap-internal-label-font-size, 16px);
2033
+ }
2034
+
2035
+ /* Square the label clip. Unovis gives each tile's label a clipPath rect that copies the tile's
2036
+ corner radius and is inset by labelOffset on every side — and it translates the label group to
2037
+ exactly that rect's top-left. The first glyph therefore lands on its own clip's rounded corner
2038
+ and gets sliced, at every radius, and raising labelOffset cannot help because it moves the label
2039
+ and the clip together. The clip's job is to stop text overflowing the tile, which a square does;
2040
+ the tile keeps its own radius, and the label stays inset by labelOffset. */
2041
+ [class*="-tile-group"] clipPath rect {
2042
+ rx: 0;
2043
+ ry: 0;
2044
+ }
2045
+
2046
+
2047
+ @custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *)));