@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.
- package/LICENSE +67 -0
- package/README.md +208 -0
- package/app.config.ts +10 -0
- package/assets/icons/forest/bike-asterisk.svg +21 -0
- package/assets/icons/forest/bike-down.svg +19 -0
- package/assets/icons/forest/bike-off.svg +20 -0
- package/assets/icons/forest/bike-up.svg +19 -0
- package/assets/icons/forest/bike-x.svg +19 -0
- package/assets/icons/forest/bike.svg +18 -0
- package/assets/icons/forest/mark-fill.svg +6 -0
- package/assets/icons/forest/mark.svg +6 -0
- package/assets/icons/forest/parking.svg +3 -0
- package/assets/icons/forest/star-fill.svg +4 -0
- package/assets/icons/forest/star.svg +4 -0
- package/assets/icons/forest/traffic-light-caution.svg +8 -0
- package/assets/icons/forest/traffic-light-go.svg +8 -0
- package/assets/icons/forest/traffic-light-lit.svg +8 -0
- package/assets/icons/forest/traffic-light-stop.svg +8 -0
- package/assets/icons/forest/traffic-light.svg +8 -0
- package/nuxt.config.ts +81 -0
- package/package.json +35 -0
- package/packages/charts/src/FBoroughShape.vue +52 -0
- package/packages/charts/src/FCalendarGrid.vue +332 -0
- package/packages/charts/src/FCellLegend.vue +54 -0
- package/packages/charts/src/FChartFrame.vue +201 -0
- package/packages/charts/src/FChartFrameSingle.vue +91 -0
- package/packages/charts/src/FChartLegend.vue +26 -0
- package/packages/charts/src/FDistributionBar.vue +224 -0
- package/packages/charts/src/FDottedMap.vue +335 -0
- package/packages/charts/src/FLondonMap.vue +399 -0
- package/packages/charts/src/FSparkline.vue +198 -0
- package/packages/charts/src/FStatusTrack.vue +151 -0
- package/packages/charts/src/cellSize.ts +46 -0
- package/packages/charts/src/distribution.ts +99 -0
- package/packages/charts/src/engine.ts +15 -0
- package/packages/charts/src/forestTooltip.ts +110 -0
- package/packages/charts/src/fromCategories.ts +43 -0
- package/packages/charts/src/index.ts +87 -0
- package/packages/charts/src/londonAreas.ts +75 -0
- package/packages/charts/src/londonGrid.ts +67 -0
- package/packages/charts/src/motionDuration.ts +49 -0
- package/packages/charts/src/presets.ts +143 -0
- package/packages/charts/src/resolveVar.ts +21 -0
- package/packages/charts/src/snapTooltip.ts +154 -0
- package/packages/charts/src/tooltipStandIn.ts +58 -0
- package/packages/charts/src/useCellScale.ts +155 -0
- package/packages/charts/src/useChartPalette.ts +61 -0
- package/packages/charts/src/useChartRepaintKey.ts +22 -0
- package/packages/charts/src/useThemeVersion.ts +17 -0
- package/packages/frames/src/FBrowserFrame.vue +53 -0
- package/packages/frames/src/FDeviceFrame.vue +242 -0
- package/packages/frames/src/FFitFrame.vue +83 -0
- package/packages/frames/src/art/iphone-11.svg +124 -0
- package/packages/frames/src/art/iphone-12.svg +601 -0
- package/packages/frames/src/art/iphone-13-pro.svg +820 -0
- package/packages/frames/src/art/iphone-13.svg +849 -0
- package/packages/frames/src/art/iphone-14.svg +681 -0
- package/packages/frames/src/art/iphone-15-pro.svg +633 -0
- package/packages/frames/src/art/iphone-15.svg +713 -0
- package/packages/frames/src/art/iphone-16-plus.svg +685 -0
- package/packages/frames/src/art/iphone-16-pro-max.svg +611 -0
- package/packages/frames/src/art/iphone-16-pro.svg +631 -0
- package/packages/frames/src/art/iphone-16.svg +713 -0
- package/packages/frames/src/art/iphone-17-pro-max.svg +355 -0
- package/packages/frames/src/art/iphone-17.svg +305 -0
- package/packages/frames/src/art/iphone-air.svg +389 -0
- package/packages/frames/src/art/iphone-se-3.svg +473 -0
- package/packages/frames/src/art/iphone-x.svg +652 -0
- package/packages/frames/src/art/pixel-10-pro-xl.svg +32 -0
- package/packages/frames/src/art/pixel-10-pro.svg +32 -0
- package/packages/frames/src/art/pixel-10.svg +32 -0
- package/packages/frames/src/art/pixel-8-pro.svg +208 -0
- package/packages/frames/src/art/pixel-8.svg +208 -0
- package/packages/frames/src/art/pixel-8a.svg +8 -0
- package/packages/frames/src/art/pixel-9-pro-xl.svg +32 -0
- package/packages/frames/src/art/pixel-9-pro.svg +34 -0
- package/packages/frames/src/art/pixel-9a.svg +25 -0
- package/packages/frames/src/art.ts +29 -0
- package/packages/frames/src/devices.ts +337 -0
- package/packages/frames/src/index.ts +15 -0
- package/packages/tokens/geo/london-boroughs.json +1 -0
- package/packages/tokens/geo/london-dots-coarse.json +1 -0
- package/packages/tokens/geo/london-dots-fine.json +1 -0
- package/packages/tokens/geo/london-dots-medium.json +1 -0
- package/packages/tokens/geo/london-dots-ultra.json +1 -0
- package/packages/tokens/geo/london-wards.json +1 -0
- package/packages/tokens/glyphs/glyphs.json +1 -0
- package/packages/tokens/logos/f-rot.svg +4 -0
- package/packages/tokens/logos/f.svg +4 -0
- package/packages/tokens/logos/favicon-c2w.svg +9 -0
- package/packages/tokens/logos/favicon-console.svg +9 -0
- package/packages/tokens/logos/favicon-ds.svg +9 -0
- package/packages/tokens/logos/favicon-fleet.svg +9 -0
- package/packages/tokens/logos/favicon-flex.svg +9 -0
- package/packages/tokens/logos/favicon-plus.svg +9 -0
- package/packages/tokens/logos/favicon-radar.svg +9 -0
- package/packages/tokens/logos/favicon-tower.svg +9 -0
- package/packages/tokens/logos/lockup-horizontal.svg +17 -0
- package/packages/tokens/logos/lockup-vertical.svg +17 -0
- package/packages/tokens/logos/mark-mono.svg +4 -0
- package/packages/tokens/logos/mark.svg +7 -0
- package/packages/tokens/logos/wordmark.svg +9 -0
- package/packages/tokens/src/colourEngine.ts +604 -0
- package/packages/tokens/src/glyphs.ts +127 -0
- package/packages/tokens/src/logo.ts +50 -0
- package/packages/tokens/src/subbrands.ts +44 -0
- package/packages/tokens/src/sublogo.ts +80 -0
- package/packages/ui/icons/forest/map/forest-place-count.svg +21 -0
- package/packages/ui/icons/forest/map/forest-place-marker.svg +17 -0
- package/packages/ui/icons/forest/map/forest-vehicle-marker.svg +73 -0
- package/packages/ui/icons/forest/map/manifest.json +373 -0
- package/packages/ui/icons/forest/map/place-count-manifest.json +83 -0
- package/packages/ui/icons/forest/map/place-manifest.json +74 -0
- package/packages/ui/src/address/address.ts +125 -0
- package/packages/ui/src/address/autofill.ts +78 -0
- package/packages/ui/src/address/geocode.ts +55 -0
- package/packages/ui/src/address/index.ts +31 -0
- package/packages/ui/src/address/validate.ts +91 -0
- package/packages/ui/src/canvas/FCanvasCard.vue +97 -0
- package/packages/ui/src/canvas/FCanvasControl.vue +121 -0
- package/packages/ui/src/canvas/FCanvasControlGroup.vue +100 -0
- package/packages/ui/src/canvas/FCanvasFlyout.vue +216 -0
- package/packages/ui/src/canvas/FCanvasHud.vue +402 -0
- package/packages/ui/src/canvas/canvasSurface.ts +39 -0
- package/packages/ui/src/canvas/index.ts +17 -0
- package/packages/ui/src/contexts/ForestContext.vue +53 -0
- package/packages/ui/src/contexts/contexts.ts +153 -0
- package/packages/ui/src/contexts/index.ts +23 -0
- package/packages/ui/src/contexts/marketing-theme.ts +214 -0
- package/packages/ui/src/contexts/mobile-theme.ts +402 -0
- package/packages/ui/src/filter/FFilterBar.vue +72 -0
- package/packages/ui/src/filter/FFilterChip.vue +362 -0
- package/packages/ui/src/filter/FFilterClauses.vue +104 -0
- package/packages/ui/src/filter/FFilterTrigger.vue +183 -0
- package/packages/ui/src/filter/filter.theme.ts +27 -0
- package/packages/ui/src/filter/filterClause.ts +265 -0
- package/packages/ui/src/filter/filterPredicate.ts +134 -0
- package/packages/ui/src/filter/filterSchema.ts +82 -0
- package/packages/ui/src/filter/index.ts +36 -0
- package/packages/ui/src/forest-preset.ts +68 -0
- package/packages/ui/src/icons.ts +79 -0
- package/packages/ui/src/index.ts +17 -0
- package/packages/ui/src/kpi/FKpi.vue +253 -0
- package/packages/ui/src/kpi/FKpiGroup.vue +161 -0
- package/packages/ui/src/kpi/index.ts +20 -0
- package/packages/ui/src/kpi/kpi.theme.ts +168 -0
- package/packages/ui/src/kpi/kpiDelta.ts +82 -0
- package/packages/ui/src/kpi/kpiFormat.ts +72 -0
- package/packages/ui/src/kpi/kpiGroupState.ts +63 -0
- package/packages/ui/src/logo/FLogo.vue +52 -0
- package/packages/ui/src/logo/FSubLogo.vue +214 -0
- package/packages/ui/src/logo/index.ts +8 -0
- package/packages/ui/src/map/FMap.vue +355 -0
- package/packages/ui/src/map/FMapCompass.vue +154 -0
- package/packages/ui/src/map/FMapCoordinates.vue +134 -0
- package/packages/ui/src/map/FMapDrawTools.vue +116 -0
- package/packages/ui/src/map/FMapFullscreen.vue +92 -0
- package/packages/ui/src/map/FMapGeolocate.vue +208 -0
- package/packages/ui/src/map/FMapLayers.vue +75 -0
- package/packages/ui/src/map/FMapLegend.vue +45 -0
- package/packages/ui/src/map/FMapPopup.vue +129 -0
- package/packages/ui/src/map/FMapRamp.vue +69 -0
- package/packages/ui/src/map/FMapRegion.vue +77 -0
- package/packages/ui/src/map/FMapScale.vue +107 -0
- package/packages/ui/src/map/FMapSearch.vue +362 -0
- package/packages/ui/src/map/FMapTilt.vue +87 -0
- package/packages/ui/src/map/FMapTools.vue +61 -0
- package/packages/ui/src/map/FMapZoom.vue +81 -0
- package/packages/ui/src/map/FMarker.vue +218 -0
- package/packages/ui/src/map/FTrackScrubber.vue +94 -0
- package/packages/ui/src/map/FVehicleLegend.vue +72 -0
- package/packages/ui/src/map/FVehicleMark.vue +166 -0
- package/packages/ui/src/map/bin.ts +196 -0
- package/packages/ui/src/map/coordinates.ts +83 -0
- package/packages/ui/src/map/density.ts +401 -0
- package/packages/ui/src/map/draw.ts +356 -0
- package/packages/ui/src/map/ease.ts +67 -0
- package/packages/ui/src/map/fullscreen.ts +59 -0
- package/packages/ui/src/map/heat.ts +160 -0
- package/packages/ui/src/map/index.ts +351 -0
- package/packages/ui/src/map/liveMarkers.ts +60 -0
- package/packages/ui/src/map/map.css +116 -0
- package/packages/ui/src/map/mapColor.ts +31 -0
- package/packages/ui/src/map/mapContext.ts +12 -0
- package/packages/ui/src/map/mapLayers.ts +55 -0
- package/packages/ui/src/map/mapLegend.ts +70 -0
- package/packages/ui/src/map/mapSearch.theme.ts +79 -0
- package/packages/ui/src/map/mapSearch.ts +279 -0
- package/packages/ui/src/map/mapUrl.ts +117 -0
- package/packages/ui/src/map/markerAsset.ts +3 -0
- package/packages/ui/src/map/padding.ts +39 -0
- package/packages/ui/src/map/place.ts +192 -0
- package/packages/ui/src/map/ramp.ts +95 -0
- package/packages/ui/src/map/regionSelect.ts +163 -0
- package/packages/ui/src/map/route.ts +167 -0
- package/packages/ui/src/map/scale.ts +329 -0
- package/packages/ui/src/map/scaleBar.ts +67 -0
- package/packages/ui/src/map/selectionModel.ts +50 -0
- package/packages/ui/src/map/sources/coordinates.ts +58 -0
- package/packages/ui/src/map/sources/mapboxPlaces.ts +71 -0
- package/packages/ui/src/map/style.ts +14 -0
- package/packages/ui/src/map/track.ts +279 -0
- package/packages/ui/src/map/useFeatureSelect.ts +230 -0
- package/packages/ui/src/map/useLiveMarkers.ts +84 -0
- package/packages/ui/src/map/useMap.ts +150 -0
- package/packages/ui/src/map/useMapDraw.ts +776 -0
- package/packages/ui/src/map/useMapOverlay.ts +421 -0
- package/packages/ui/src/map/useMapTableSelection.ts +101 -0
- package/packages/ui/src/map/useMapUrlState.ts +155 -0
- package/packages/ui/src/map/useMarkerSource.ts +163 -0
- package/packages/ui/src/map/useRegionSelect.ts +309 -0
- package/packages/ui/src/map/useTokenMode.ts +61 -0
- package/packages/ui/src/map/useTrackPlayback.ts +131 -0
- package/packages/ui/src/map/useVehicleFilter.ts +51 -0
- package/packages/ui/src/map/useVisibleFeatures.ts +124 -0
- package/packages/ui/src/map/vehicle.ts +807 -0
- package/packages/ui/src/map/vehicleFilter.ts +284 -0
- package/packages/ui/src/map/vehicleLegend.ts +155 -0
- package/packages/ui/src/map/visibleFeatures.ts +192 -0
- package/packages/ui/src/map/zone.ts +291 -0
- package/packages/ui/src/shell/FAppNavbar.vue +68 -0
- package/packages/ui/src/shell/FAppShell.vue +163 -0
- package/packages/ui/src/shell/FAppSidebar.vue +215 -0
- package/packages/ui/src/shell/FPageToolbar.vue +126 -0
- package/packages/ui/src/shell/FScrollShadow.vue +43 -0
- package/packages/ui/src/shell/FSidePanel.vue +258 -0
- package/packages/ui/src/shell/FToolbarActions.vue +32 -0
- package/packages/ui/src/shell/index.ts +33 -0
- package/packages/ui/src/shell/nav.ts +37 -0
- package/packages/ui/src/shell/pageToolbar.ts +69 -0
- package/packages/ui/src/shell/shell.ts +74 -0
- package/packages/ui/src/shell/sidePanel.ts +115 -0
- package/packages/ui/src/themes/accordion.ts +36 -0
- package/packages/ui/src/themes/alert.ts +16 -0
- package/packages/ui/src/themes/auth-form.ts +19 -0
- package/packages/ui/src/themes/avatar-group.ts +9 -0
- package/packages/ui/src/themes/avatar.ts +18 -0
- package/packages/ui/src/themes/badge.ts +51 -0
- package/packages/ui/src/themes/banner.ts +21 -0
- package/packages/ui/src/themes/breadcrumb.ts +10 -0
- package/packages/ui/src/themes/button.mechanics.ts +191 -0
- package/packages/ui/src/themes/button.ts +136 -0
- package/packages/ui/src/themes/calendar.ts +29 -0
- package/packages/ui/src/themes/card.ts +67 -0
- package/packages/ui/src/themes/carousel.ts +41 -0
- package/packages/ui/src/themes/checkbox-group.ts +20 -0
- package/packages/ui/src/themes/checkbox.ts +47 -0
- package/packages/ui/src/themes/chip.ts +50 -0
- package/packages/ui/src/themes/collapsible.ts +26 -0
- package/packages/ui/src/themes/color-picker.ts +13 -0
- package/packages/ui/src/themes/command-palette.ts +37 -0
- package/packages/ui/src/themes/container.ts +14 -0
- package/packages/ui/src/themes/context-menu.ts +38 -0
- package/packages/ui/src/themes/dashboard-group.ts +11 -0
- package/packages/ui/src/themes/dashboard-navbar.ts +10 -0
- package/packages/ui/src/themes/dashboard-panel.ts +18 -0
- package/packages/ui/src/themes/dashboard-resize-handle.ts +35 -0
- package/packages/ui/src/themes/dashboard-sidebar-collapse.ts +7 -0
- package/packages/ui/src/themes/dashboard-sidebar-toggle.ts +7 -0
- package/packages/ui/src/themes/dashboard-sidebar.ts +58 -0
- package/packages/ui/src/themes/dashboard-toolbar.ts +15 -0
- package/packages/ui/src/themes/drawer.ts +45 -0
- package/packages/ui/src/themes/dropdown-menu.ts +38 -0
- package/packages/ui/src/themes/empty.ts +43 -0
- package/packages/ui/src/themes/field-group.ts +52 -0
- package/packages/ui/src/themes/file-upload.ts +38 -0
- package/packages/ui/src/themes/footer-columns.ts +13 -0
- package/packages/ui/src/themes/footer.ts +9 -0
- package/packages/ui/src/themes/form-field.ts +25 -0
- package/packages/ui/src/themes/form.ts +22 -0
- package/packages/ui/src/themes/header.ts +12 -0
- package/packages/ui/src/themes/icon.ts +61 -0
- package/packages/ui/src/themes/index.ts +99 -0
- package/packages/ui/src/themes/input-date.ts +73 -0
- package/packages/ui/src/themes/input-menu.ts +66 -0
- package/packages/ui/src/themes/input-number.ts +58 -0
- package/packages/ui/src/themes/input-rating.ts +25 -0
- package/packages/ui/src/themes/input-tags.ts +63 -0
- package/packages/ui/src/themes/input-time.ts +70 -0
- package/packages/ui/src/themes/input.ts +148 -0
- package/packages/ui/src/themes/kbd.ts +27 -0
- package/packages/ui/src/themes/link.ts +27 -0
- package/packages/ui/src/themes/listbox.ts +48 -0
- package/packages/ui/src/themes/main.ts +6 -0
- package/packages/ui/src/themes/marquee.ts +45 -0
- package/packages/ui/src/themes/modal.ts +46 -0
- package/packages/ui/src/themes/navigation-menu.ts +80 -0
- package/packages/ui/src/themes/page-anchors.ts +7 -0
- package/packages/ui/src/themes/page-aside.ts +7 -0
- package/packages/ui/src/themes/page-card.ts +22 -0
- package/packages/ui/src/themes/page-cta.ts +18 -0
- package/packages/ui/src/themes/page-feature.ts +13 -0
- package/packages/ui/src/themes/page-header.ts +16 -0
- package/packages/ui/src/themes/page-hero.ts +15 -0
- package/packages/ui/src/themes/page-links.ts +12 -0
- package/packages/ui/src/themes/page-section.ts +14 -0
- package/packages/ui/src/themes/page.ts +7 -0
- package/packages/ui/src/themes/pagination.ts +17 -0
- package/packages/ui/src/themes/pin-input.ts +73 -0
- package/packages/ui/src/themes/popover.ts +30 -0
- package/packages/ui/src/themes/progress.ts +29 -0
- package/packages/ui/src/themes/prose.ts +45 -0
- package/packages/ui/src/themes/radio-group.ts +39 -0
- package/packages/ui/src/themes/scroll-area.ts +23 -0
- package/packages/ui/src/themes/select-menu.ts +71 -0
- package/packages/ui/src/themes/select.ts +72 -0
- package/packages/ui/src/themes/separator.ts +28 -0
- package/packages/ui/src/themes/sidebar.ts +127 -0
- package/packages/ui/src/themes/skeleton.ts +15 -0
- package/packages/ui/src/themes/slideover.ts +87 -0
- package/packages/ui/src/themes/slider.ts +48 -0
- package/packages/ui/src/themes/stepper.ts +41 -0
- package/packages/ui/src/themes/switch.ts +63 -0
- package/packages/ui/src/themes/table.ts +34 -0
- package/packages/ui/src/themes/tabs.ts +74 -0
- package/packages/ui/src/themes/textarea.ts +77 -0
- package/packages/ui/src/themes/timeline.ts +25 -0
- package/packages/ui/src/themes/toast.ts +29 -0
- package/packages/ui/src/themes/toaster.ts +66 -0
- package/packages/ui/src/themes/tooltip.ts +31 -0
- package/packages/ui/src/themes/tree.ts +41 -0
- package/packages/ui/src/themes/user.ts +28 -0
- package/styles.cdn.gen.css +2047 -0
- 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 *)));
|