@nuxtseo/design-system 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +181 -2
  2. package/app/app.config.ts +592 -0
  3. package/app/component-attributes.d.ts +8 -0
  4. package/app/components/README.md +38 -0
  5. package/app/components/brand/UiBrandMark.vue +63 -0
  6. package/app/components/brand/UiModuleIcon.vue +45 -0
  7. package/app/components/brand/UiProLettering.vue +26 -0
  8. package/app/components/brand/UiRangeTerrain.vue +311 -0
  9. package/app/components/brand/range-glyphs.ts +18 -0
  10. package/app/components/container/BrowserTab.vue +145 -0
  11. package/app/components/container/IntegrationRow.vue +120 -0
  12. package/app/components/container/KnowDoRails.vue +40 -0
  13. package/app/components/container/RailPanel.vue +93 -0
  14. package/app/components/container/UiAlert.vue +120 -0
  15. package/app/components/container/UiAppShell.vue +165 -0
  16. package/app/components/container/UiCard.vue +204 -0
  17. package/app/components/container/UiDisclosure.vue +43 -0
  18. package/app/components/container/UiEmptyState.vue +102 -0
  19. package/app/components/container/UiErrorState.vue +61 -0
  20. package/app/components/container/UiLoadingState.vue +20 -0
  21. package/app/components/container/UiPageHeader.vue +95 -0
  22. package/app/components/container/UiPopover.vue +102 -0
  23. package/app/components/container/UiSampleDataOverlay.vue +47 -0
  24. package/app/components/container/UiSectionHeader.vue +83 -0
  25. package/app/components/container/UiSummaryStrip.vue +40 -0
  26. package/app/components/container/UiToast.vue +230 -0
  27. package/app/components/container/UiToaster.vue +271 -0
  28. package/app/components/container/UiWidgetState.vue +143 -0
  29. package/app/components/container/ui-nav-drawer.ts +48 -0
  30. package/app/components/data/ActionRail.vue +152 -0
  31. package/app/components/data/FacetStrip.vue +93 -0
  32. package/app/components/data/UiBarCell.vue +37 -0
  33. package/app/components/data/UiBarChart.vue +281 -0
  34. package/app/components/data/UiChartAnnotationControls.vue +74 -0
  35. package/app/components/data/UiChartAnnotations.vue +163 -0
  36. package/app/components/data/UiChartBrushOverlay.vue +96 -0
  37. package/app/components/data/UiChartDataReveal.vue +81 -0
  38. package/app/components/data/UiChartFrame.vue +123 -0
  39. package/app/components/data/UiChartTooltip.vue +81 -0
  40. package/app/components/data/UiCoverageNote.vue +90 -0
  41. package/app/components/data/UiCwvSparkline.vue +188 -0
  42. package/app/components/data/UiDataList.vue +276 -0
  43. package/app/components/data/UiDataTableSection.vue +233 -0
  44. package/app/components/data/UiDistributionBars.vue +164 -0
  45. package/app/components/data/UiDonutChart.vue +132 -0
  46. package/app/components/data/UiDoorwayColumn.vue +108 -0
  47. package/app/components/data/UiDoorwayGrid.vue +58 -0
  48. package/app/components/data/UiDoorwayRow.vue +83 -0
  49. package/app/components/data/UiEntitySummary.vue +261 -0
  50. package/app/components/data/UiFactsGrid.vue +107 -0
  51. package/app/components/data/UiImpactMeter.vue +33 -0
  52. package/app/components/data/UiIssueRow.vue +149 -0
  53. package/app/components/data/UiLineChart.vue +431 -0
  54. package/app/components/data/UiMetricBarPopover.vue +121 -0
  55. package/app/components/data/UiMetricLabel.vue +30 -0
  56. package/app/components/data/UiMetricStat.vue +73 -0
  57. package/app/components/data/UiMetricToggle.vue +156 -0
  58. package/app/components/data/UiMetricsRow.vue +64 -0
  59. package/app/components/data/UiPositionMetric.vue +53 -0
  60. package/app/components/data/UiRelatedFixes.vue +113 -0
  61. package/app/components/data/UiRichText.vue +325 -0
  62. package/app/components/data/UiScatterPlot.vue +144 -0
  63. package/app/components/data/UiSeverityMarker.vue +30 -0
  64. package/app/components/data/UiSparkColumns.vue +160 -0
  65. package/app/components/data/UiSparkline.vue +681 -0
  66. package/app/components/data/UiStat.vue +445 -0
  67. package/app/components/data/UiStats.vue +76 -0
  68. package/app/components/data/UiTable.vue +517 -0
  69. package/app/components/data/UiTableExport.vue +40 -0
  70. package/app/components/data/UiTableFrame.vue +149 -0
  71. package/app/components/data/UiTablePagination.vue +37 -0
  72. package/app/components/data/UiTableShell.vue +48 -0
  73. package/app/components/data/UiTableTd.vue +39 -0
  74. package/app/components/data/UiTableTh.vue +61 -0
  75. package/app/components/data/UiTopEntityStackChart.vue +393 -0
  76. package/app/components/data/cells/UiTableDash.vue +3 -0
  77. package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
  78. package/app/components/data/cells/UiTableMetricCell.vue +72 -0
  79. package/app/components/data/cells/UiTablePathCell.vue +76 -0
  80. package/app/components/data/cells/UiTableScoreTile.vue +49 -0
  81. package/app/components/data/cells/UiTableTrendCell.vue +41 -0
  82. package/app/components/element/UiAtmosphere.vue +64 -0
  83. package/app/components/element/UiAuthHeading.vue +24 -0
  84. package/app/components/element/UiAuthProviders.vue +46 -0
  85. package/app/components/element/UiBrandIcon.vue +39 -0
  86. package/app/components/element/UiButton.vue +549 -0
  87. package/app/components/element/UiCelebration.vue +65 -0
  88. package/app/components/element/UiChannelChips.vue +62 -0
  89. package/app/components/element/UiChatQuickLink.vue +319 -0
  90. package/app/components/element/UiChip.vue +101 -0
  91. package/app/components/element/UiConfirmDialog.vue +84 -0
  92. package/app/components/element/UiCopyButton.vue +90 -0
  93. package/app/components/element/UiDitherAvatar.vue +177 -0
  94. package/app/components/element/UiDotLabel.vue +19 -0
  95. package/app/components/element/UiEjectMenu.vue +181 -0
  96. package/app/components/element/UiEjectOverlay.vue +29 -0
  97. package/app/components/element/UiFavicon.vue +235 -0
  98. package/app/components/element/UiFaviconStack.vue +84 -0
  99. package/app/components/element/UiFilterMenu.vue +324 -0
  100. package/app/components/element/UiFocusedSurface.vue +129 -0
  101. package/app/components/element/UiHealthDot.vue +32 -0
  102. package/app/components/element/UiHelpLabel.vue +28 -0
  103. package/app/components/element/UiIcon.vue +22 -0
  104. package/app/components/element/UiInput.vue +35 -0
  105. package/app/components/element/UiLogoPlausible.vue +12 -0
  106. package/app/components/element/UiLogoPro.vue +25 -0
  107. package/app/components/element/UiLogoUmami.vue +12 -0
  108. package/app/components/element/UiNavIcon.vue +107 -0
  109. package/app/components/element/UiNavList.vue +156 -0
  110. package/app/components/element/UiOnboardingAnswers.vue +127 -0
  111. package/app/components/element/UiOverflowCount.vue +31 -0
  112. package/app/components/element/UiProBadge.vue +19 -0
  113. package/app/components/element/UiProgressCircle.vue +79 -0
  114. package/app/components/element/UiProgressPercent.vue +35 -0
  115. package/app/components/element/UiRelativeTime.vue +58 -0
  116. package/app/components/element/UiScrollFade.vue +16 -0
  117. package/app/components/element/UiSelect.vue +68 -0
  118. package/app/components/element/UiSeverityDot.vue +27 -0
  119. package/app/components/element/UiSiteFavicon.vue +24 -0
  120. package/app/components/element/UiSkeleton.vue +148 -0
  121. package/app/components/element/UiSlotText.vue +65 -0
  122. package/app/components/element/UiSourceLogos.vue +113 -0
  123. package/app/components/element/UiSourceRail.vue +71 -0
  124. package/app/components/element/UiStaticTerminal.vue +107 -0
  125. package/app/components/element/UiStatusBadge.vue +62 -0
  126. package/app/components/element/UiSyncDot.vue +46 -0
  127. package/app/components/element/UiTabs.vue +245 -0
  128. package/app/components/element/UiTogglePill.vue +86 -0
  129. package/app/components/element/UiTooltip.vue +390 -0
  130. package/app/components/element/UiTooltipGrid.vue +457 -0
  131. package/app/components/element/UiTrend.vue +184 -0
  132. package/app/components/element/UiTrialBenefits.vue +71 -0
  133. package/app/components/element/UiUrlLabel.vue +75 -0
  134. package/app/components/element/UiValenceDot.vue +37 -0
  135. package/app/components/element/UiWizardModal.vue +284 -0
  136. package/app/components/element/UiWizardNav.vue +59 -0
  137. package/app/components/element/UiWizardProgress.vue +78 -0
  138. package/app/components/element/UiWizardShell.vue +43 -0
  139. package/app/components/element/eject-menu.ts +359 -0
  140. package/app/components/element/eject-tracker.ts +34 -0
  141. package/app/components/element/onboarding-answers.ts +93 -0
  142. package/app/components/element/onboarding-intents.ts +19 -0
  143. package/app/composables/dataVizColors.ts +350 -0
  144. package/app/composables/formatting.ts +252 -0
  145. package/app/composables/semanticColors.ts +80 -0
  146. package/app/composables/useActivityOverlay.ts +55 -0
  147. package/app/composables/useAppShellNav.ts +21 -0
  148. package/app/composables/useChartBrush.ts +221 -0
  149. package/app/composables/useChartHover.ts +153 -0
  150. package/app/composables/useChartTickPlan.ts +186 -0
  151. package/app/composables/useCloseOnNavigation.ts +7 -0
  152. package/app/composables/useConfirm.ts +38 -0
  153. package/app/composables/useEdgeFade.ts +37 -0
  154. package/app/composables/useEject.ts +69 -0
  155. package/app/composables/useEntranceMotion.ts +22 -0
  156. package/app/composables/useFaviconBacking.ts +164 -0
  157. package/app/composables/useGridCols.ts +39 -0
  158. package/app/composables/useIssueOverlay.ts +43 -0
  159. package/app/composables/useOnboardingService.ts +106 -0
  160. package/app/composables/useOpenedOnce.ts +19 -0
  161. package/app/composables/useScrollSpy.ts +178 -0
  162. package/app/composables/useSquircleFallback.ts +21 -0
  163. package/app/composables/useWizardRoute.ts +123 -0
  164. package/app/plugins/favicon.ts +15 -0
  165. package/app/plugins/lazy-motion.ts +22 -0
  166. package/app/plugins/selection-highlight.client.ts +176 -0
  167. package/app/shared/cli.ts +10 -0
  168. package/app/shared/error-state.ts +150 -0
  169. package/app/shared/icons.ts +1 -0
  170. package/app/shared/motion.ts +1 -0
  171. package/app/shared/nav.ts +38 -0
  172. package/app/shared/navigation.ts +5 -0
  173. package/app/shared/static-terminal.ts +14 -0
  174. package/app/shared/table-export.ts +36 -0
  175. package/app/shared/table.ts +105 -0
  176. package/app/shared/ui-icons.ts +1 -0
  177. package/app/shared/view-state.ts +85 -0
  178. package/app/shared/widget-failure.ts +126 -0
  179. package/app/utils/app-favicon.ts +64 -0
  180. package/app/utils/chartAnnotations.ts +326 -0
  181. package/app/utils/countries.ts +337 -0
  182. package/app/utils/export-download.ts +100 -0
  183. package/app/utils/favicon-fallback.ts +12 -0
  184. package/app/utils/icon-color.ts +105 -0
  185. package/app/utils/number.ts +74 -0
  186. package/app/utils/render-link.ts +14 -0
  187. package/app/utils/slot-text.ts +18 -0
  188. package/app/utils/squircle.ts +154 -0
  189. package/app/utils/topEntityStack.ts +197 -0
  190. package/app/utils/vue.ts +57 -0
  191. package/build/design-system.ts +69 -0
  192. package/build/external-links.ts +38 -0
  193. package/build/fonts.ts +39 -0
  194. package/build/icon-server-bundle.ts +276 -0
  195. package/build/runtime/icon-server-bundle.ts +29 -0
  196. package/css/global.css +1896 -0
  197. package/nuxt.config.ts +143 -0
  198. package/package.json +93 -3
  199. package/public/fonts/README.md +33 -0
  200. package/public/fonts/fira-code-OFL.txt +93 -0
  201. package/public/fonts/fira-code-var-latin.woff2 +0 -0
  202. package/public/fonts/hubot-sans-OFL.txt +93 -0
  203. package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
  204. package/server/api/favicon.get.ts +122 -0
  205. package/server/utils/favicon-source.ts +51 -0
  206. package/shared/contracts/feedback.ts +42 -0
  207. package/shared/contracts/index.ts +1 -0
  208. package/shared/delivery-channels.ts +53 -0
  209. package/shared/icons/index.ts +143 -0
  210. package/shared/icons/registry.ts +347 -0
  211. package/shared/icons/ui-slots.ts +58 -0
  212. package/shared/markdown.ts +44 -0
  213. package/shared/motion.ts +120 -0
  214. package/shared/rangi.ts +60 -0
  215. package/shared/rich-text.ts +34 -0
  216. package/shared/ui-icons.ts +6 -0
  217. package/shared/utils/urls.ts +64 -0
  218. package/types/index.ts +1 -0
  219. package/types/ui.ts +339 -0
package/css/global.css ADDED
@@ -0,0 +1,1896 @@
1
+ @import 'tailwindcss';
2
+ @import '@nuxt/ui';
3
+
4
+ /* Type families. A site sets its own in an unlayered `:root` rule; OG images
5
+ read them from the Tailwind entry instead (README, Brand seams). `--font-title`
6
+ carries page titles (`.text-title`, `font-title`) and defaults to the display
7
+ family. The faces load only with `designSystem: { fonts: true }`. */
8
+ @theme {
9
+ --font-sans: 'Hubot Sans', sans-serif;
10
+ --font-display: 'Hubot Sans', sans-serif;
11
+ --font-title: 'Hubot Sans', sans-serif;
12
+ --font-mono: 'Fira Code', monospace;
13
+ }
14
+
15
+ /* Tracking token — `tracking-data` utility + `var(--tracking-data)` for the
16
+ handful of CSS rules (popover/alert/stat-card) that share the same dense
17
+ tabular-data letter-spacing. `@theme static` because the var is read by
18
+ global.css rules and would otherwise be tree-shaken when no utility hits. */
19
+ @theme static {
20
+ --tracking-data: -0.005em;
21
+ }
22
+
23
+ .ui-slot-text {
24
+ display: inline-flex;
25
+ white-space: pre;
26
+ }
27
+
28
+ .ui-slot-text__slot {
29
+ position: relative;
30
+ display: inline-flex;
31
+ flex: none;
32
+ justify-content: center;
33
+ overflow: hidden;
34
+ overflow-x: visible;
35
+ overflow-y: clip;
36
+ line-height: 1.3;
37
+ vertical-align: bottom;
38
+ }
39
+
40
+ .ui-slot-text__sizer {
41
+ visibility: hidden;
42
+ white-space: pre;
43
+ }
44
+
45
+ .ui-slot-text__face {
46
+ position: absolute;
47
+ inset: 0;
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ white-space: pre;
52
+ transition:
53
+ transform var(--ui-slot-duration, 260ms) var(--ui-slot-easing, cubic-bezier(0.34, 1.56, 0.64, 1)) var(--ui-slot-delay, 0ms),
54
+ opacity var(--ui-slot-duration, 260ms) var(--ease-standard) var(--ui-slot-delay, 0ms);
55
+ will-change: transform, opacity;
56
+ }
57
+
58
+ .ui-slot-text-roll-enter-from {
59
+ opacity: 0;
60
+ }
61
+
62
+ .ui-slot-text-roll-leave-to {
63
+ opacity: 0;
64
+ }
65
+
66
+ .ui-slot-text--up .ui-slot-text-roll-enter-from {
67
+ transform: translateY(105%);
68
+ }
69
+
70
+ .ui-slot-text--up .ui-slot-text-roll-leave-to {
71
+ transform: translateY(-105%);
72
+ }
73
+
74
+ .ui-slot-text--down .ui-slot-text-roll-enter-from {
75
+ transform: translateY(-105%);
76
+ }
77
+
78
+ .ui-slot-text--down .ui-slot-text-roll-leave-to {
79
+ transform: translateY(105%);
80
+ }
81
+
82
+ .ui-slot-text-roll-enter-to,
83
+ .ui-slot-text-roll-leave-from {
84
+ opacity: 1;
85
+ transform: translateY(0);
86
+ }
87
+
88
+ /* Horizontal scroller edge fade (`useEdgeFade`): mask the side(s) with content
89
+ off-screen so the strip reads as scrollable. JS sets data-fade-* only while
90
+ the strip overflows, so a wide viewport never masks. Shared by `UiTabs` and
91
+ the `ProDataToolbar` metric row. --fade is the ramp width. */
92
+ .ui-edge-fade {
93
+ --fade: 1.75rem;
94
+ scrollbar-width: none;
95
+ }
96
+ .ui-edge-fade::-webkit-scrollbar {
97
+ display: none;
98
+ }
99
+ .ui-edge-fade[data-fade-end] {
100
+ -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
101
+ mask-image: linear-gradient(to right, #000 calc(100% - var(--fade)), transparent);
102
+ }
103
+ .ui-edge-fade[data-fade-start] {
104
+ -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade));
105
+ mask-image: linear-gradient(to right, transparent, #000 var(--fade));
106
+ }
107
+ .ui-edge-fade[data-fade-start][data-fade-end] {
108
+ -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
109
+ mask-image: linear-gradient(to right, transparent, #000 var(--fade), #000 calc(100% - var(--fade)), transparent);
110
+ }
111
+
112
+ /* Palette overrides — MUST be `@theme static`.
113
+ Tailwind v4 tree-shakes any `@theme` variable that no utility class
114
+ references, so it never lands in `:root`. Nuxt UI's color plugin builds
115
+ `--ui-color-primary-*: var(--color-violet-*, <stock fallback>)` (and the
116
+ same for `--color-slate-*`) inside a runtime <style> tag that Tailwind
117
+ never scans — so without `static` the override is shaken out and the stock
118
+ fallback wins. `static` forces emission so the overrides actually bind. */
119
+ @theme static {
120
+ /* Neutral — Nuxt UI resolves `neutral: 'slate'` (app.config) to
121
+ `--color-slate-*`, NOT `--color-neutral-*`. Slate realigned from its
122
+ stock hue 257 to 292 (the violet primary hue) and chroma lifted ~2.4x so
123
+ a clear violet warmth bleeds through every surface; the dashboard reads
124
+ as a violet-tinted grey rather than a cold blue-grey. Chroma stays low
125
+ enough that neutrals still read as neutral, not as a colour wash. Dark bg
126
+ held at 11% lightness so the surface is not near-black. */
127
+ --color-slate-50: oklch(98.4% 0.005 292);
128
+ --color-slate-100: oklch(96.8% 0.009 292);
129
+ --color-slate-200: oklch(92.9% 0.020 292);
130
+ --color-slate-300: oklch(86.9% 0.033 292);
131
+ --color-slate-400: oklch(70.4% 0.052 292);
132
+ --color-slate-500: oklch(55.4% 0.058 292);
133
+ --color-slate-600: oklch(44.6% 0.056 292);
134
+ --color-slate-700: oklch(32% 0.050 292);
135
+ --color-slate-800: oklch(22% 0.044 292);
136
+ --color-slate-900: oklch(16% 0.036 292);
137
+ --color-slate-950: oklch(11% 0.029 292);
138
+
139
+ /* Primary — Nuxt UI resolves `primary: 'violet'` / `pro: 'violet'` to
140
+ `--color-violet-*`. Stock Tailwind v4 violet ran chroma .25-.28 at high
141
+ lightness across the 400-600 band, reading electric against the
142
+ de-chromatized surface. Rather than cut chroma (which drifts the mids
143
+ toward gray-mauve), the 300-600 band trades lightness for chroma: darker
144
+ stops carry full saturation, so primary reads as a deep, rich violet —
145
+ not bright, not gray. Hue pinned to 292 to match the neutral ramp. Light
146
+ shoulder (50-200) chroma lifted above a linear ramp so pale tints read as
147
+ visible lilac, not near-white grey. */
148
+ --color-violet-50: oklch(97.0% 0.037 292);
149
+ --color-violet-100: oklch(94.4% 0.065 292);
150
+ --color-violet-200: oklch(89.5% 0.110 292);
151
+ --color-violet-300: oklch(76.0% 0.150 292);
152
+ --color-violet-400: oklch(63.0% 0.180 292);
153
+ --color-violet-500: oklch(54.0% 0.225 292);
154
+ --color-violet-600: oklch(48.0% 0.240 292);
155
+ --color-violet-700: oklch(43.0% 0.230 292);
156
+ --color-violet-800: oklch(38.0% 0.200 292);
157
+ --color-violet-900: oklch(33.0% 0.165 292);
158
+ --color-violet-950: oklch(24.0% 0.120 292);
159
+ }
160
+
161
+ /* Semantic TEXT legibility — `--text-color-*` outranks `--color-*` for the
162
+ `text-*` utilities only, so `text-warning` et al resolve through these while
163
+ bg/border/ring/charts keep Nuxt UI's `--ui-*` defaults (tints, badges and
164
+ solid fills are untouched). Why: the stock 500 shades fail WCAG on white
165
+ (amber-500 1.9:1, red-500 3.8:1, green-500 2.2:1, blue-500 3.8:1), so light
166
+ mode steps each hue down to its lightest AA-passing stop (warning 700 5.0:1,
167
+ error 700, success 700 5.0:1, info 600 5.3:1). Error takes 700 because 600
168
+ measured 4.05 to 4.23:1 on its own `bg-error/10` tint, the badge and soft
169
+ button surface; 700 holds AA there. Dark mode keeps the
170
+ Nuxt UI per-mode `--ui-*` value (400 shades — already passing). `static`
171
+ because several consumers reference these via runtime-built class maps. */
172
+ @theme static {
173
+ --text-color-success: var(--ui-text-success);
174
+ --text-color-info: var(--ui-text-info);
175
+ --text-color-warning: var(--ui-text-warning);
176
+ --text-color-error: var(--ui-text-error);
177
+ --text-color-amber-400: var(--ui-text-warning);
178
+ --text-color-amber-500: var(--ui-text-warning);
179
+ --text-color-green-500: var(--ui-text-success);
180
+ }
181
+
182
+ /* Primary TEXT seam. This site's violet ramp already passes AA as text, so
183
+ `text-primary` reads `--ui-primary` unchanged. A site whose primary fails as
184
+ text on white sets `--ui-text-primary` (e.g. its 700 shade in light mode),
185
+ the same way the semantic hues above step down. `inline` resolves the
186
+ fallback on the element, so a scoped `--ui-primary` still applies. */
187
+ @theme inline {
188
+ --text-color-primary: var(--ui-text-primary, var(--ui-primary));
189
+ }
190
+
191
+ /* Data-viz seam. Each chart identity slot in `dataVizColors.ts` reads
192
+ `--ui-viz-<slot>` when a site sets it, and this site's Tailwind hue
193
+ otherwise. These utilities (`bg-viz-clicks`, `text-viz-clicks`, the `/15`
194
+ fills) carry the class side; the `hex` field there carries the same
195
+ property with the hex fallback for SVG paint. `inline` resolves the
196
+ fallback on the element, so a scoped override applies too.
197
+
198
+ Text slots whose hue this file already steps down for AA (amber 400/500
199
+ read `--ui-text-warning`) keep that step. Period text reads the 600 shade
200
+ in light mode and the 400 shade in dark, which pass AA on each page.
201
+
202
+ Marks need 3:1 against the page (WCAG 1.4.11). A slot whose stock hue falls
203
+ under that on white reads `--_viz-<slot>` in light mode: the lightest shade
204
+ of the same hue that clears 3:1 there. Only `.light` sets it. Dark mode
205
+ falls through to the stock hue, which already passes, so dark charts keep
206
+ their exact colours. The chart neutral and the semantic chart fills step
207
+ down the same way. */
208
+ :root {
209
+ --_viz-current-text: var(--color-blue-600);
210
+ --_viz-comparison-text: var(--color-purple-600);
211
+ }
212
+
213
+ .dark {
214
+ --_viz-current-text: var(--color-blue-400);
215
+ --_viz-comparison-text: var(--color-purple-400);
216
+ }
217
+
218
+ .light {
219
+ --_viz-ctr: var(--color-emerald-600);
220
+ --_viz-position: var(--color-orange-600);
221
+ --_viz-users: var(--color-cyan-600);
222
+ --_viz-inp: var(--color-amber-600);
223
+ --_viz-device-desktop: var(--color-blue-500);
224
+ --_viz-device-mobile: var(--color-green-600);
225
+ --_viz-device-tablet: var(--color-amber-600);
226
+ --_viz-non-brand: var(--color-emerald-600);
227
+ --_viz-rank-3: var(--color-emerald-600);
228
+ --_viz-rank-4: var(--color-orange-600);
229
+ --_viz-rank-5: var(--color-cyan-600);
230
+ --_viz-rank-7: var(--color-amber-600);
231
+ --_viz-rank-9: var(--color-teal-600);
232
+ --_viz-rank-10: var(--color-rose-500);
233
+ --_viz-resource-script: var(--color-orange-600);
234
+ --_viz-resource-image: var(--color-cyan-600);
235
+ --_viz-category-1: var(--color-blue-500);
236
+ --_viz-category-2: var(--color-green-600);
237
+ --_viz-category-3: var(--color-amber-600);
238
+ --_viz-category-4: var(--color-red-500);
239
+ --_viz-category-5: var(--color-violet-500);
240
+ --_viz-neutral: oklch(52% 0.03 292);
241
+ --_viz-success: var(--color-green-600);
242
+ --_viz-warning: var(--color-amber-600);
243
+ }
244
+
245
+ @theme inline {
246
+ --color-viz-clicks: var(--ui-viz-clicks, var(--color-blue-500));
247
+ --color-viz-impressions: var(--ui-viz-impressions, var(--color-purple-500));
248
+ --color-viz-ctr: var(--ui-viz-ctr, var(--_viz-ctr, var(--color-emerald-500)));
249
+ --color-viz-position: var(--ui-viz-position, var(--_viz-position, var(--color-orange-500)));
250
+ --color-viz-sessions: var(--ui-viz-sessions, var(--color-blue-500));
251
+ --color-viz-users: var(--ui-viz-users, var(--_viz-users, var(--color-cyan-500)));
252
+ --color-viz-pageviews: var(--ui-viz-pageviews, var(--color-purple-500));
253
+ --color-viz-lcp: var(--ui-viz-lcp, var(--color-blue-500));
254
+ --color-viz-inp: var(--ui-viz-inp, var(--_viz-inp, var(--color-amber-500)));
255
+ --color-viz-cls: var(--ui-viz-cls, var(--color-purple-500));
256
+ --color-viz-current: var(--ui-viz-current, var(--color-blue-500));
257
+ --color-viz-comparison: var(--ui-viz-comparison, var(--color-purple-500));
258
+ --color-viz-device-desktop: var(--ui-viz-device-desktop, var(--_viz-device-desktop, var(--color-blue-400)));
259
+ --color-viz-device-mobile: var(--ui-viz-device-mobile, var(--_viz-device-mobile, var(--color-green-400)));
260
+ --color-viz-device-tablet: var(--ui-viz-device-tablet, var(--_viz-device-tablet, var(--color-amber-400)));
261
+ --color-viz-brand: var(--ui-viz-brand, var(--color-violet-500));
262
+ --color-viz-non-brand: var(--ui-viz-non-brand, var(--_viz-non-brand, var(--color-emerald-500)));
263
+ --color-viz-rank-1: var(--ui-viz-rank-1, var(--color-blue-500));
264
+ --color-viz-rank-2: var(--ui-viz-rank-2, var(--color-purple-500));
265
+ --color-viz-rank-3: var(--ui-viz-rank-3, var(--_viz-rank-3, var(--color-emerald-400)));
266
+ --color-viz-rank-4: var(--ui-viz-rank-4, var(--_viz-rank-4, var(--color-orange-500)));
267
+ --color-viz-rank-5: var(--ui-viz-rank-5, var(--_viz-rank-5, var(--color-cyan-500)));
268
+ --color-viz-rank-6: var(--ui-viz-rank-6, var(--color-pink-500));
269
+ --color-viz-rank-7: var(--ui-viz-rank-7, var(--_viz-rank-7, var(--color-amber-400)));
270
+ --color-viz-rank-8: var(--ui-viz-rank-8, var(--color-indigo-400));
271
+ --color-viz-rank-9: var(--ui-viz-rank-9, var(--_viz-rank-9, var(--color-teal-400)));
272
+ --color-viz-rank-10: var(--ui-viz-rank-10, var(--_viz-rank-10, var(--color-rose-400)));
273
+ --color-viz-resource-document: var(--ui-viz-resource-document, var(--color-blue-500));
274
+ --color-viz-resource-script: var(--ui-viz-resource-script, var(--_viz-resource-script, var(--color-orange-500)));
275
+ --color-viz-resource-stylesheet: var(--ui-viz-resource-stylesheet, var(--color-purple-500));
276
+ --color-viz-resource-image: var(--ui-viz-resource-image, var(--_viz-resource-image, var(--color-cyan-500)));
277
+ --color-viz-resource-font: var(--ui-viz-resource-font, var(--color-pink-500));
278
+
279
+ /* Text side. `--ui-viz-<slot>-text` recolours only the `text-viz-<slot>`
280
+ class, for a mark hue that fails AA as text; unset, text follows the mark. */
281
+ --text-color-viz-clicks: var(--ui-viz-clicks-text, var(--ui-viz-clicks, var(--color-blue-500)));
282
+ --text-color-viz-impressions: var(--ui-viz-impressions-text, var(--ui-viz-impressions, var(--color-purple-500)));
283
+ --text-color-viz-ctr: var(--ui-viz-ctr-text, var(--ui-viz-ctr, var(--_viz-ctr, var(--color-emerald-500))));
284
+ --text-color-viz-position: var(--ui-viz-position-text, var(--ui-viz-position, var(--_viz-position, var(--color-orange-500))));
285
+ --text-color-viz-sessions: var(--ui-viz-sessions-text, var(--ui-viz-sessions, var(--color-blue-500)));
286
+ --text-color-viz-users: var(--ui-viz-users-text, var(--ui-viz-users, var(--_viz-users, var(--color-cyan-500))));
287
+ --text-color-viz-pageviews: var(--ui-viz-pageviews-text, var(--ui-viz-pageviews, var(--color-purple-500)));
288
+ --text-color-viz-lcp: var(--ui-viz-lcp-text, var(--ui-viz-lcp, var(--color-blue-500)));
289
+ --text-color-viz-inp: var(--ui-viz-inp-text, var(--ui-viz-inp, var(--ui-text-warning)));
290
+ --text-color-viz-cls: var(--ui-viz-cls-text, var(--ui-viz-cls, var(--color-purple-500)));
291
+ --text-color-viz-current: var(--ui-viz-current-text, var(--ui-viz-current, var(--_viz-current-text)));
292
+ --text-color-viz-comparison: var(--ui-viz-comparison-text, var(--ui-viz-comparison, var(--_viz-comparison-text)));
293
+ --text-color-viz-device-desktop: var(--ui-viz-device-desktop-text, var(--ui-viz-device-desktop, var(--_viz-device-desktop, var(--color-blue-400))));
294
+ --text-color-viz-device-mobile: var(--ui-viz-device-mobile-text, var(--ui-viz-device-mobile, var(--_viz-device-mobile, var(--color-green-400))));
295
+ --text-color-viz-device-tablet: var(--ui-viz-device-tablet-text, var(--ui-viz-device-tablet, var(--ui-text-warning)));
296
+ --text-color-viz-brand: var(--ui-viz-brand-text, var(--ui-viz-brand, var(--color-violet-500)));
297
+ --text-color-viz-non-brand: var(--ui-viz-non-brand-text, var(--ui-viz-non-brand, var(--_viz-non-brand, var(--color-emerald-500))));
298
+ --text-color-viz-rank-1: var(--ui-viz-rank-1-text, var(--ui-viz-rank-1, var(--color-blue-500)));
299
+ --text-color-viz-rank-2: var(--ui-viz-rank-2-text, var(--ui-viz-rank-2, var(--color-purple-500)));
300
+ --text-color-viz-rank-3: var(--ui-viz-rank-3-text, var(--ui-viz-rank-3, var(--_viz-rank-3, var(--color-emerald-400))));
301
+ --text-color-viz-rank-4: var(--ui-viz-rank-4-text, var(--ui-viz-rank-4, var(--_viz-rank-4, var(--color-orange-500))));
302
+ --text-color-viz-rank-5: var(--ui-viz-rank-5-text, var(--ui-viz-rank-5, var(--_viz-rank-5, var(--color-cyan-500))));
303
+ --text-color-viz-rank-6: var(--ui-viz-rank-6-text, var(--ui-viz-rank-6, var(--color-pink-500)));
304
+ --text-color-viz-rank-7: var(--ui-viz-rank-7-text, var(--ui-viz-rank-7, var(--ui-text-warning)));
305
+ --text-color-viz-rank-8: var(--ui-viz-rank-8-text, var(--ui-viz-rank-8, var(--color-indigo-400)));
306
+ --text-color-viz-rank-9: var(--ui-viz-rank-9-text, var(--ui-viz-rank-9, var(--_viz-rank-9, var(--color-teal-400))));
307
+ --text-color-viz-rank-10: var(--ui-viz-rank-10-text, var(--ui-viz-rank-10, var(--_viz-rank-10, var(--color-rose-400))));
308
+ --text-color-viz-resource-document: var(--ui-viz-resource-document-text, var(--ui-viz-resource-document, var(--color-blue-500)));
309
+ --text-color-viz-resource-script: var(--ui-viz-resource-script-text, var(--ui-viz-resource-script, var(--_viz-resource-script, var(--color-orange-500))));
310
+ --text-color-viz-resource-stylesheet: var(--ui-viz-resource-stylesheet-text, var(--ui-viz-resource-stylesheet, var(--color-purple-500)));
311
+ --text-color-viz-resource-image: var(--ui-viz-resource-image-text, var(--ui-viz-resource-image, var(--_viz-resource-image, var(--color-cyan-500))));
312
+ --text-color-viz-resource-font: var(--ui-viz-resource-font-text, var(--ui-viz-resource-font, var(--color-pink-500)));
313
+ }
314
+
315
+ :root {
316
+ --ui-text-success: var(--ui-color-success-700);
317
+ --ui-text-info: var(--ui-color-info-600);
318
+ --ui-text-warning: var(--ui-color-warning-800);
319
+ --ui-text-error: var(--ui-color-error-700);
320
+ }
321
+
322
+ .dark {
323
+ --ui-text-success: var(--ui-success);
324
+ --ui-text-info: var(--ui-info);
325
+ --ui-text-warning: var(--ui-warning);
326
+ --ui-text-error: var(--ui-error);
327
+ }
328
+
329
+ :root {
330
+ /* Let the UA animate to/from intrinsic keywords (height: auto, width:
331
+ min-content, …). Pure progressive enhancement — unsupported browsers
332
+ ignore it. Inherited, so declaring once on :root covers the tree. */
333
+ interpolate-size: allow-keywords;
334
+
335
+ /* Pro dashboard container — single source of truth for the page-content
336
+ width + horizontal padding. Consumed via the `.pro-container` utility
337
+ defined below; never reach for `max-w-[1128px]` inline. */
338
+ --pro-container-width: 1280px;
339
+
340
+ /* Motion easings — standard for page/content transitions, spring for reveals
341
+ with subtle overshoot, exit for leave animations that accelerate away. */
342
+ --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
343
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
344
+ --ease-exit: cubic-bezier(0.4, 0, 1, 1);
345
+
346
+ /* Density — the central spacing knobs. Consumed by UiCard (md size, sm+
347
+ breakpoint) and ProPageZone (zone margins/gaps); values are re-based to
348
+ match the shipped rendering, so changing one here retunes the whole
349
+ surface in a single review. */
350
+ --density-card-padding: 1.5rem; /* UiCard md body/header padding at sm+ */
351
+ --density-element-gap: 1.5rem; /* in-zone gap (secondary/tertiary; primary runs 0.5rem tighter) */
352
+ --density-section-gap: 2.5rem; /* between-zone margin (secondary/tertiary; primary runs 0.5rem tighter) */
353
+
354
+ /* ═══ Variable-font type axes (Hubot Sans: wght 200–900, wdth 75–125%) ═══
355
+ Single source of truth for every typographic axis the semantic text tokens
356
+ (.text-title/.text-heading/.text-subheading, .font-emphasis/.font-strong,
357
+ .numerals-display) read from — tune the whole type system here.
358
+ Defaults live on :root so the tokens work on marketing surfaces too;
359
+ `.dashboard-theme` overrides to its denser tuning. */
360
+ --wght-body: 430;
361
+ --wght-emphasis: 540;
362
+ --wght-heading: 620;
363
+ --wght-title: 680;
364
+ --wght-numeral: 560;
365
+ --wdth-body: 100%;
366
+ --wdth-heading: 92%;
367
+ --wdth-numeral: 95%;
368
+ --tracking-tight: -0.02em;
369
+ --tracking-snug: -0.01em;
370
+ /* Heading sizes are role vars too, so .text-title/.text-heading/.text-subheading
371
+ resize per context: editorial defaults here, compact override in .dashboard-theme. */
372
+ --size-title: 1.5rem;
373
+ --size-heading: 1.125rem;
374
+ --size-subheading: 1rem;
375
+
376
+ /* ═══ Depth & elevation ═══════════════════════════════════════════════
377
+ Kept outside @theme so Tailwind doesn't inline the values at build time
378
+ (which would break the runtime .dashboard-theme remap below). Consumed
379
+ via plain CSS — `box-shadow: var(--elevation-raised)` — never a Tailwind
380
+ `shadow-*` utility.
381
+
382
+ TWO TIERS. Components reference ONLY tier B.
383
+ Tier A — ingredients (--_depth-*): composable atoms, never used direct.
384
+ Tier B — presets (--elevation-*): the component-facing API.
385
+
386
+ ── Tier A: ingredients ─────────────────────────────────────────────── */
387
+ --_depth-ambient-low:
388
+ 0 1px 2px rgb(0 0 0 / 0.03),
389
+ 0 1px 5px rgb(0 0 0 / 0.035);
390
+ --_depth-ambient-high:
391
+ 0 1px 1px 0 rgb(0 0 0 / 0.04),
392
+ 0 2px 6px -2px rgb(0 0 0 / 0.06),
393
+ 0 8px 16px -6px rgb(0 0 0 / 0.08);
394
+ /* Inset edge: white top highlight (light-from-above) + dark bottom shade. */
395
+ --_depth-bevel:
396
+ inset 0 1px 0 0 rgb(255 255 255 / 0.06),
397
+ inset 0 -1px 0 0 rgb(0 0 0 / 0.04);
398
+ /* Brand bevel: the popover/tooltip variant — top highlight tinted primary
399
+ so detached chrome carries a subtle brand edge. */
400
+ --_depth-bevel-brand:
401
+ inset 0 1.5px 0 0 color-mix(in srgb, var(--ui-color-primary-400) 26%, transparent),
402
+ inset 0 -1px 0 0 rgb(0 0 0 / 0.04);
403
+ --_depth-ring: inset 0 0 0 1px var(--ui-border);
404
+ --_depth-sheen: linear-gradient(
405
+ to bottom,
406
+ rgb(255 255 255 / 0.025),
407
+ rgb(0 0 0 / 0.015) 60%,
408
+ rgb(0 0 0 / 0.03)
409
+ );
410
+
411
+ /* ── Tier B: elevation presets (component-facing API) ───────────────────
412
+ inset recessed, carved edge, no lift — resting stat cards, skeletons
413
+ flat 1px hairline, zero lift — dashboard cards, table shells
414
+ raised resting drop shadow + bevel — marketing cards, hover states
415
+ overlay atmospheric shadow + bevel — modals, drawers, alerts
416
+ popover atmospheric + brand bevel — tooltip / popover / dropdown
417
+ emphasis lift + brand bevel, remap-proof — THE one card that matters (max 1/page)
418
+ hover accent-tinted halo, hover-only — buttons, cards, links
419
+ --surface-raised is the matching top-lit gradient FILL for overlay-tier
420
+ surfaces (applied as background-image, not box-shadow). */
421
+ --elevation-inset: var(--_depth-bevel);
422
+ --elevation-flat: var(--_depth-ring);
423
+ --elevation-raised: var(--_depth-ambient-low), var(--_depth-bevel);
424
+ --elevation-overlay: var(--_depth-ambient-high), var(--_depth-bevel);
425
+ --elevation-popover: var(--_depth-ambient-high), var(--_depth-bevel-brand);
426
+ /* Emphasis: the single sanctioned in-page contrast move. Deliberately NOT
427
+ remapped by .dashboard-theme (unlike raised→flat) — it exists so exactly
428
+ one surface per page can outrank the flat hairline field. Budget: one.
429
+ Stack = primary-tinted hairline ring + soft brand ambient + lift + brand
430
+ bevel (the login-panel glow recipe, dialed down to card scale). */
431
+ --elevation-emphasis:
432
+ 0 0 0 1px color-mix(in oklab, var(--ui-color-primary-400) 35%, var(--ui-border)),
433
+ 0 2px 16px -2px color-mix(in oklab, var(--ui-color-primary-500) 18%, transparent),
434
+ var(--_depth-ambient-low),
435
+ inset 0 1.5px 0 0 color-mix(in srgb, var(--ui-color-primary-400) 32%, transparent);
436
+ --elevation-hover:
437
+ 0 2px 8px -2px color-mix(in oklab, var(--fx-accent) 10%, transparent),
438
+ 0 8px 20px -8px color-mix(in oklab, var(--fx-accent) 8%, transparent);
439
+ --surface-raised: var(--_depth-sheen);
440
+
441
+ /* Hover-FX accent — the tint of --elevation-hover and the cursor FX. Neutral
442
+ by default; a surface can set `--fx-accent: var(--ui-color-primary-500)`
443
+ for a brand-tinted lift. */
444
+ --fx-accent: var(--ui-text);
445
+
446
+ /* Focus ring — an interaction-state token, orthogonal to the elevation
447
+ ladder. Left standalone; not part of the --elevation-* family. The colour
448
+ is its own token because the ring ships in two forms: a box-shadow halo
449
+ (--shadow-focus) and the outline every Nuxt UI control draws on
450
+ `:focus-visible` (see "One focus treatment" below). One tint, both forms.
451
+ Opaque on purpose: a focus indicator needs 3:1 against the page (WCAG
452
+ 1.4.11), and the old 30% tint measured 1.57:1 on white and 1.28:1 on dark.
453
+ Primary 500 measures 5.6:1 on white; `.dark` swaps to 400 (5.5:1). */
454
+ --focus-ring-color: var(--ui-color-primary-500);
455
+ --shadow-focus: 0 0 0 3px var(--focus-ring-color);
456
+ }
457
+
458
+ /* ═══ Greydients — the visibility arc ══════════════════════════════════════
459
+ A curated family of muted, luminous gradients (Nuevo.Tokyo "greydient" genre,
460
+ derived onto our hue-285 warm-slate + violet). They render the product's core
461
+ arc — a site moving from UNSEEN to SEEN (unindexed → crawled → discovered →
462
+ ranked) — as a day-cycle of light. The governing rule: ATMOSPHERE AT THE
463
+ EDGES, SILENCE AT THE CENTER. Greydients live on threshold + narrative
464
+ surfaces (auth, hero, empty/preview, share render); the moment real user data
465
+ appears the near-monochrome discipline takes over. NEVER on dashboard data
466
+ surfaces (see the Avoid list). Applied only through <UiAtmosphere> / the
467
+ `.ui-atmosphere` layer below — never as a raw background per surface.
468
+
469
+ Kept OUTSIDE @theme (like --elevation-*) so Tailwind never inlines them and
470
+ the light/dark twin resolves at runtime. Consumed as `background: var(--…)`.
471
+ Character comes from HUE-TRAVEL + value-shaping at low chroma (≤ 0.04; Dusk's
472
+ 0.085 is the single brand exception), interpolated `in oklch` so the blend
473
+ stays luminous. Light mode is a pale daylight wash; dark is the dramatic
474
+ dark→luminous sweep — authored per mode, nothing auto-inverts.
475
+
476
+ ink 深夜 unseen · deep night — OG / share ground, dark cards
477
+ twilight 黄昏 crawled · the reference — auth / onboarding front door
478
+ dawn 暁 discovered · warm first light — marketing warmth
479
+ dusk 紫 ranked · the violet hour — the brand hero (spends color)
480
+ mist 霞 soft light · diffuse — the only one calm behind text / previews
481
+ ash 鼠 baseline · pure warm-slate grey — neutral rest state */
482
+ :root {
483
+ --greydient-ink: linear-gradient(135deg in oklch, oklch(93% 0.012 300) 0%, oklch(89% 0.022 300) 55%, oklch(85% 0.03 314) 100%);
484
+ --greydient-twilight: linear-gradient(135deg in oklch, oklch(95% 0.015 340) 0%, oklch(90% 0.03 350) 48%, oklch(88% 0.025 300) 100%);
485
+ --greydient-dawn: linear-gradient(135deg in oklch, oklch(94% 0.02 45) 0%, oklch(91% 0.038 30) 50%, oklch(97% 0.012 66) 100%);
486
+ --greydient-dusk: linear-gradient(135deg in oklch, oklch(95% 0.018 300) 0%, oklch(89% 0.05 293) 46%, oklch(91% 0.02 286) 100%);
487
+ --greydient-dusk-vivid: linear-gradient(135deg in oklch, var(--ui-color-primary-950) 0%, var(--ui-color-primary-600) 48%, var(--ui-color-primary-800) 100%);
488
+ --greydient-mist: linear-gradient(135deg in oklch, oklch(96% 0.012 300) 0%, oklch(93% 0.018 330) 52%, oklch(97% 0.01 350) 100%);
489
+ --greydient-ash: linear-gradient(135deg in oklch, oklch(95% 0.004 285) 0%, oklch(91% 0.008 285) 50%, oklch(87% 0.012 285) 100%);
490
+ }
491
+
492
+ .dark {
493
+ --greydient-ink: linear-gradient(135deg in oklch, oklch(13% 0.02 288) 0%, oklch(21% 0.032 300) 55%, oklch(30% 0.045 316) 100%);
494
+ --greydient-twilight: linear-gradient(135deg in oklch, oklch(17% 0.04 324) 0%, oklch(72% 0.03 350) 48%, oklch(77% 0.032 286) 100%);
495
+ --greydient-dawn: linear-gradient(135deg in oklch, oklch(30% 0.035 40) 0%, oklch(74% 0.045 24) 50%, oklch(92% 0.018 66) 100%);
496
+ --greydient-dusk: linear-gradient(135deg in oklch, oklch(20% 0.05 300) 0%, oklch(52% 0.085 292) 46%, oklch(84% 0.028 286) 100%);
497
+ --greydient-dusk-vivid: linear-gradient(135deg in oklch, var(--ui-color-primary-200) 0%, var(--ui-color-primary-500) 48%, var(--ui-color-primary-300) 100%);
498
+ --greydient-mist: linear-gradient(135deg in oklch, oklch(66% 0.022 300) 0%, oklch(80% 0.024 330) 52%, oklch(91% 0.014 350) 100%);
499
+ --greydient-ash: linear-gradient(135deg in oklch, oklch(24% 0.006 285) 0%, oklch(54% 0.01 285) 50%, oklch(82% 0.014 285) 100%);
500
+ }
501
+
502
+ .dark {
503
+ /* Dark mode redefines the ingredients; presets inherit via var() automatically. */
504
+ --_depth-ambient-low:
505
+ 0 1px 2px rgb(0 0 0 / 0.35),
506
+ 0 1px 5px rgb(0 0 0 / 0.22);
507
+ --_depth-ambient-high:
508
+ 0 1px 1px 0 rgb(0 0 0 / 0.4),
509
+ 0 2px 6px -2px rgb(0 0 0 / 0.35),
510
+ 0 8px 16px -6px rgb(0 0 0 / 0.4);
511
+ /* Highlight strengthened + shade deepened — a 6% white edge vanishes on
512
+ dark chrome, and dark surfaces need a heavier settle line. */
513
+ --_depth-bevel:
514
+ inset 0 1px 0 0 rgb(255 255 255 / 0.08),
515
+ inset 0 -1px 0 0 rgb(0 0 0 / 0.2);
516
+ --_depth-bevel-brand:
517
+ inset 0 1.5px 0 0 color-mix(in srgb, var(--ui-color-primary-400) 40%, transparent),
518
+ inset 0 -1px 0 0 rgb(0 0 0 / 0.2);
519
+ --_depth-sheen: linear-gradient(
520
+ to bottom,
521
+ rgb(255 255 255 / 0.04),
522
+ rgb(255 255 255 / 0.015) 55%,
523
+ rgb(0 0 0 / 0.04)
524
+ );
525
+ }
526
+
527
+ /* Dashboard scope — compact density and a hairline-style card shadow so dense
528
+ data surfaces sit flat instead of floating. Remapping --elevation-raised to
529
+ --elevation-flat means every card resolves to the hairline ring here; nothing
530
+ else changes. Variable-font type axes (wght, wdth, tracking vars) now live on
531
+ :root as the single source of truth; override them here if the dashboard ever
532
+ wants denser tuning than the editorial default. */
533
+ .dashboard-theme {
534
+ /* Density currently matches the editorial defaults (the previous 0.75rem/
535
+ 0.5rem/1.5rem overrides were aspirational — no component read them, and
536
+ wiring them in as-was would have halved padding on ~130 cards). Tighten
537
+ deliberately by overriding the --density-* vars here; UiCard md and
538
+ ProPageZone pick it up everywhere at once. */
539
+ --elevation-raised: var(--elevation-flat);
540
+
541
+ /* Compact heading sizes — the dense dashboard runs titles far smaller than the
542
+ editorial defaults. Three distinct tiers: title 16 / heading 13 / sub 12. */
543
+ --size-title: 1rem;
544
+ --size-heading: 0.8125rem;
545
+ --size-subheading: 0.75rem;
546
+ }
547
+
548
+ /* Tabular figures everywhere so numeric columns and KPIs align. */
549
+ .dashboard-theme,
550
+ .dashboard-theme :where(p, span, div, td, th, li, button, input, label, a) {
551
+ font-variant-numeric: tabular-nums;
552
+ }
553
+
554
+ /* Dashboard body uses the body family (Hubot Sans, variable) tuned to a
555
+ slightly-above-Regular weight. The family is set once here and inherited.
556
+ Set on each element in the list above, this unlayered rule beat the layered
557
+ `font-mono` utility, so a mono value in a dashboard span rendered in the
558
+ body face. */
559
+ .dashboard-theme {
560
+ font-family: var(--font-sans);
561
+ font-weight: var(--wght-body);
562
+ font-stretch: var(--wdth-body);
563
+ }
564
+
565
+ .dashboard-theme :where(h1, h2, h3, h4, h5, h6) {
566
+ font-weight: var(--wght-heading);
567
+ font-stretch: var(--wdth-heading);
568
+ letter-spacing: var(--tracking-snug);
569
+ }
570
+
571
+ /* Drop the 300ms double-tap-zoom delay on interactive controls (touch). Scoped
572
+ to controls (zero-specificity :where) so component classes still win and page
573
+ content keeps normal pinch-zoom. */
574
+ :where(button, a, summary, label, [role="button"], [role="tab"], [role="menuitem"], [role="switch"], [role="option"]) {
575
+ touch-action: manipulation;
576
+ }
577
+
578
+ .light {
579
+ color-scheme: light;
580
+ /* Bind surfaces to `--ui-color-neutral-*` (Nuxt UI's neutral alias → the
581
+ hue-292 slate override) rather than stock `--color-neutral-*` (cold hue
582
+ 257), so the documented violet warmth reaches marketing/docs too. The
583
+ `.dashboard-theme` blocks below already bind this way (to mauve). */
584
+ --ui-bg: #fff;
585
+ --ui-bg-muted: var(--ui-color-neutral-50);
586
+ --ui-bg-elevated: var(--ui-color-neutral-100);
587
+ --ui-bg-accented: var(--ui-color-neutral-200);
588
+ --ui-bg-inverted: var(--ui-color-neutral-900);
589
+ /* Text seam. These three sit on Nuxt SEO's violet hue, off the neutral
590
+ ramp; a site re-hues them by setting `--ui-light-text*` (README). */
591
+ --ui-text-dimmed: var(--ui-light-text-dimmed, oklch(47.5% 0.052 292));
592
+ --ui-text-muted: var(--ui-color-neutral-600);
593
+ --ui-text-toned: var(--ui-color-neutral-700);
594
+ --ui-text: var(--ui-light-text, oklch(37% 0.044 285));
595
+ --ui-text-highlighted: var(--ui-light-text-highlighted, oklch(21% 0.042 285));
596
+ --ui-text-inverted: #fff;
597
+ --ui-border: var(--ui-color-neutral-200);
598
+ --ui-border-muted: var(--ui-color-neutral-100);
599
+ --ui-border-accented: var(--ui-color-neutral-300);
600
+ --ui-border-inverted: var(--ui-color-neutral-900);
601
+ /* Brand landing dots (nuxtseo-shared/brand): green = a module shipped it,
602
+ violet = Pro read it back. The tile is the favicon plate. */
603
+ --nuxtseo-brand-free: oklch(62.7% 0.194 149.2);
604
+ --nuxtseo-brand-pro: var(--ui-color-primary-500);
605
+ /* The Pro climb. Deeper on light ground, where the favicon's pale end disappears. */
606
+ --nuxtseo-brand-gradient-from: #5b21b6;
607
+ --nuxtseo-brand-gradient-to: #a68af7;
608
+ --nuxtseo-brand-tile: oklch(21% 0.042 285);
609
+ --nuxtseo-brand-tile-ink: oklch(98.4% 0.005 292);
610
+ }
611
+
612
+ .dark {
613
+ color-scheme: dark;
614
+ /* Tier ladder in dark: 8.5 → 15 → 21 → 30. The old 7.5 → 13 gap read flat
615
+ on large surfaces; the new deltas give nav / body / elevated cards a
616
+ visible hierarchy without breaking the "no pure black" rule. */
617
+ --ui-bg: var(--ui-color-neutral-950);
618
+ --ui-bg-muted: var(--ui-color-neutral-900);
619
+ --ui-bg-elevated: var(--ui-color-neutral-800);
620
+ --ui-bg-accented: var(--ui-color-neutral-700);
621
+ --ui-bg-inverted: #fff;
622
+ --focus-ring-color: var(--ui-color-primary-400);
623
+ /* Dimmed shares muted's 400 step in dark. The 500 step measured 4.22:1 on
624
+ bg-default and 2.65:1 on bg-accented, under the 4.5:1 AA floor; 400 holds
625
+ 4.8:1 or more on every surface up to bg-accented. */
626
+ --ui-text-dimmed: var(--ui-color-neutral-400);
627
+ --ui-text-muted: var(--ui-color-neutral-400);
628
+ --ui-text-toned: var(--ui-color-neutral-300);
629
+ --ui-text: var(--ui-color-neutral-300);
630
+ --ui-text-highlighted: var(--ui-color-neutral-100);
631
+ --ui-text-inverted: var(--ui-color-neutral-900);
632
+ --ui-border: var(--ui-color-neutral-800);
633
+ --ui-border-muted: var(--ui-color-neutral-800);
634
+ --ui-border-accented: var(--ui-color-neutral-700);
635
+ --ui-border-inverted: #fff;
636
+ --nuxtseo-brand-free: oklch(79.2% 0.209 151.7);
637
+ --nuxtseo-brand-pro: var(--ui-color-primary-400);
638
+ --nuxtseo-brand-gradient-from: #6925d5;
639
+ --nuxtseo-brand-gradient-to: #ddcfff;
640
+ --nuxtseo-brand-tile: oklch(27% 0.05 292);
641
+ --nuxtseo-brand-tile-ink: oklch(98.4% 0.005 292);
642
+ }
643
+
644
+ body {
645
+ font-family: var(--font-sans);
646
+ -webkit-font-smoothing: antialiased;
647
+ /* Normalize fallback x-height to Hubot's so the swap (and any web-font failure)
648
+ holds layout — complements @nuxt/fonts' generated metric overrides. */
649
+ font-size-adjust: from-font;
650
+ /* Hubot Sans has a full 200–900 weight axis, so faux-bold should never fire;
651
+ kill WEIGHT synthesis only. Style synthesis is left ON deliberately — Hubot
652
+ ships no italic master, so `<em>`, blockquote cites and the italic-selection
653
+ effect all rely on synthesized obliques. (Not `font-synthesis: none`, which
654
+ would render every italic upright.) */
655
+ font-synthesis-weight: none;
656
+ /* The legibility win without `text-rendering: optimizeLegibility` (a layout
657
+ footgun on long bodies): turn on kerning + common ligatures explicitly. */
658
+ font-kerning: normal;
659
+ font-variant-ligatures: common-ligatures;
660
+ }
661
+
662
+ p, h1, h2, h3, h4, h5, h6 {
663
+ overflow-wrap: break-word;
664
+ }
665
+
666
+ p {
667
+ text-wrap: pretty;
668
+ }
669
+
670
+ h1, h2, h3, h4, h5, h6 {
671
+ font-family: var(--font-display);
672
+ text-wrap: balance;
673
+ }
674
+
675
+ #root, #__nuxt {
676
+ isolation: isolate;
677
+ }
678
+
679
+ /* ─── Highlighter text selection ──────────────────────────────────────────────
680
+ Instead of the native highlight bar, selected text gets a hand-drawn violet
681
+ marker swipe. CSS `::selection` can't do gradients / slanted ends / rough
682
+ edges, so the plugin (`app/plugins/selection-highlight.client.ts`) paints an
683
+ OVERLAY of `.mark` divs over the selection's per-line client-rects. The overlay
684
+ never mutates the document (selection, copy and live Vue surfaces are all
685
+ untouched) and paints live during the drag.
686
+
687
+ These rules: (1) blank the native highlight, (2) re-assert it inside regions
688
+ the overlay skips, (3) style the overlay marks, (4) expose the per-theme ink
689
+ tokens the plugin reads off the selected element. Everything is gated on
690
+ `html.selection-hl-ready`, a class the plugin adds ONLY when JS runs and
691
+ reduced-motion is not requested, so the no-JS / reduced-motion fallback keeps
692
+ the normal native selection. */
693
+ :root {
694
+ /* Ink tokens (light defaults). `--hl`/`--hl2` are the duotone stroke, `--pool`
695
+ the ink-pool depth at the bottom edge, `--blend`/`--hlop` how the layer
696
+ composites, `--streak` the dry-marker texture. Dark overrides below. */
697
+ --selection-hl: oklch(62% 0.19 292);
698
+ --selection-hl2: oklch(58% 0.21 326);
699
+ --selection-pool: oklch(46% 0.22 300 / 0.55);
700
+ --selection-streak: rgba(255, 255, 255, 0.05);
701
+ --selection-blend: multiply;
702
+ --selection-op: 0.56;
703
+ }
704
+ .dark {
705
+ --selection-hl: oklch(64% 0.18 292);
706
+ --selection-hl2: oklch(70% 0.16 268);
707
+ --selection-pool: oklch(74% 0.16 300 / 0.40);
708
+ --selection-streak: rgba(0, 0, 0, 0.05);
709
+ --selection-blend: screen;
710
+ }
711
+
712
+ html.selection-hl-ready ::selection {
713
+ background-color: transparent;
714
+ color: inherit;
715
+ }
716
+ html.selection-hl-ready ::-moz-selection {
717
+ background-color: transparent;
718
+ color: inherit;
719
+ }
720
+
721
+ /* Re-assert the native highlight inside regions the overlay never paints (it
722
+ bails on these — keep in sync with EXCLUDE in selection-highlight.client.ts).
723
+ Without this the blank above leaves selection invisible there. Placed after the
724
+ blank so it wins the specificity tie. */
725
+ html.selection-hl-ready :is(input, textarea, [contenteditable], pre, code, .rangi)::selection,
726
+ html.selection-hl-ready :is(input, textarea, [contenteditable], pre, code, .rangi) ::selection {
727
+ background-color: Highlight;
728
+ color: HighlightText;
729
+ }
730
+ html.selection-hl-ready :is(input, textarea, [contenteditable], pre, code, .rangi)::-moz-selection,
731
+ html.selection-hl-ready :is(input, textarea, [contenteditable], pre, code, .rangi) ::-moz-selection {
732
+ background-color: Highlight;
733
+ color: HighlightText;
734
+ }
735
+
736
+ /* Overlay container (one per document) + per-theme isolated, blended sub-layers
737
+ + the marks. Opaque marks inside an isolated group can overlap to close the
738
+ line gaps without doubling the darkness; blend + translucency live on the
739
+ group. The rough hand-drawn edge is applied only once a selection settles
740
+ (`.rough`), since displacing every frame while dragging makes edges crawl. */
741
+ #selection-hl-layer {
742
+ position: fixed;
743
+ inset: 0;
744
+ pointer-events: none;
745
+ z-index: 60;
746
+ }
747
+ #selection-hl-layer .group {
748
+ position: fixed;
749
+ inset: 0;
750
+ isolation: isolate;
751
+ }
752
+ #selection-hl-layer .mark {
753
+ position: fixed;
754
+ background:
755
+ repeating-linear-gradient(91deg, transparent 0 9px, var(--mk-streak, transparent) 9px 10px),
756
+ linear-gradient(180deg, transparent 52%, var(--mk-pool, transparent) 108%),
757
+ linear-gradient(94deg, transparent 0, var(--mk-hl, transparent) 7px, var(--mk-hl2, transparent) 50%, var(--mk-hl, transparent) calc(100% - 7px), transparent 100%);
758
+ border-radius: 6px 7px 5px 8px;
759
+ }
760
+ #selection-hl-layer.rough .mark {
761
+ filter: url(#selection-hl-rough);
762
+ }
763
+
764
+
765
+ :root {
766
+ --scrollbar-track-light: #f1f1f1;
767
+ --scrollbar-thumb-light: #888;
768
+ --scrollbar-track-dark: #2c2c2c;
769
+ --scrollbar-thumb-dark: #555;
770
+ }
771
+
772
+ html.light {
773
+ --scrollbar-track: var(--scrollbar-track-light);
774
+ --scrollbar-thumb: var(--scrollbar-thumb-light);
775
+ }
776
+
777
+ html.dark {
778
+ --scrollbar-track: var(--scrollbar-track-dark);
779
+ --scrollbar-thumb: var(--scrollbar-thumb-dark);
780
+ }
781
+
782
+ /* scrollbar-width/color are inherited, so set them on the root — propagates to
783
+ every scroll container without a universal `*` selector forcing a match on
784
+ every node in the DOM (a style-recalc cost on large docs pages / tables). */
785
+ html {
786
+ scrollbar-width: thin;
787
+ scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
788
+ /* Reserve the gutter whether or not this page needs to scroll. Without it the
789
+ document scrollbar appears only on tall pages, and every centred column
790
+ jumps by its width on the way in and out — Settings shows it plainly:
791
+ Billing and Notifications overflow, General, Members and API tokens do not,
792
+ so moving between them shifted the content ~10px each time and read as a
793
+ glitch. */
794
+ scrollbar-gutter: stable;
795
+ }
796
+
797
+ /* A `position: fixed` element is laid out against the initial containing block,
798
+ which EXCLUDES the gutter reserved above. So `inset-0` on a dim overlay stops
799
+ ~10px short of the right edge and leaves an undimmed strip beside every
800
+ modal, measured at 1280, 1440, 1920 and 2560 — the dialog itself then centres
801
+ inside the short box and sits 5px left of the viewport centre.
802
+
803
+ Before the gutter was reserved this only showed on pages tall enough to
804
+ scroll, which is how it reached prod as "the invite modal looks fixed-width".
805
+ `100vw` includes the gutter by definition. A fixed element contributes no
806
+ scrollable overflow, so the extra 10px cannot produce a scrollbar of its
807
+ own. */
808
+ [data-slot="overlay"] {
809
+ width: 100vw;
810
+ }
811
+
812
+ /* Same cause, the centred dialog rather than its overlay: `left-1/2` resolves
813
+ against that same short containing block, so the dialog lands 5px left of the
814
+ viewport centre. Matched on the utility itself, because a slideover is also
815
+ `[data-slot="content"]` and is anchored to an edge on purpose. */
816
+ [data-slot="content"][class*="left-1/2"] {
817
+ left: 50vw;
818
+ }
819
+
820
+ ::-webkit-scrollbar {
821
+ width: 8px;
822
+ height: 8px;
823
+ }
824
+
825
+ ::-webkit-scrollbar-track {
826
+ background-color: var(--scrollbar-track);
827
+ }
828
+
829
+ ::-webkit-scrollbar-thumb {
830
+ background-color: var(--scrollbar-thumb);
831
+ border-radius: .25rem;
832
+ }
833
+
834
+ /* Thin overlay scrollbar — hidden track, faint thumb that firms up on hover.
835
+ Opt-in via `.scroll-overlay` on a scroll container (e.g. the dashboard
836
+ sidebar) so it reads modern without restyling every scroller. Standard
837
+ props are Baseline; the @supports block is the WebKit/Blink fallback. */
838
+ .scroll-overlay {
839
+ scrollbar-width: thin;
840
+ scrollbar-gutter: stable;
841
+ scrollbar-color: transparent transparent;
842
+ transition: scrollbar-color .2s ease;
843
+ }
844
+
845
+ .scroll-overlay:hover,
846
+ .scroll-overlay:focus-within {
847
+ scrollbar-color: var(--scrollbar-thumb) transparent;
848
+ }
849
+
850
+ @supports not (scrollbar-color: auto) {
851
+ .scroll-overlay::-webkit-scrollbar {
852
+ width: 6px;
853
+ height: 6px;
854
+ }
855
+
856
+ .scroll-overlay::-webkit-scrollbar-track {
857
+ background-color: transparent;
858
+ }
859
+
860
+ .scroll-overlay::-webkit-scrollbar-thumb {
861
+ background-color: transparent;
862
+ border-radius: 9999px;
863
+ transition: background-color .2s ease;
864
+ }
865
+
866
+ .scroll-overlay:hover::-webkit-scrollbar-thumb,
867
+ .scroll-overlay:focus-within::-webkit-scrollbar-thumb {
868
+ background-color: var(--scrollbar-thumb);
869
+ }
870
+ }
871
+
872
+ /* ─── Modern form affordances (2026) ──────────────────────────────────────
873
+ Progressive enhancements — unsupported browsers fall back to native UA
874
+ behaviour with no JS. */
875
+
876
+ /* Brand-tint built-in controls (checkbox / radio / range / progress) so native
877
+ inputs in prose and raw markup match the violet primary. `accent-color`
878
+ tracks the active primary, so `.dashboard-theme` keeps it on-palette too.
879
+ Paired with the per-mode `color-scheme` already set on `.light`/`.dark`. */
880
+ :root {
881
+ accent-color: var(--ui-color-primary-500);
882
+ }
883
+
884
+ /* One focus treatment, and it is the brand's. Every Nuxt UI control focuses
885
+ with `outline-primary/25`, which reads `--ui-primary` — and `apps/site` sets
886
+ that variable to near-black in light mode and a pale blue in dark (Nuxt UI's
887
+ "black as primary" trick). So a focused field drew a 3px translucent
888
+ near-black halo outside the violet inset ring it also turns on: a grey drop
889
+ shadow around a violet edge, two treatments arguing. Colour the halo from the
890
+ focus token, and the halo and the ring become one violet edge at the width
891
+ DESIGN.md asks for.
892
+
893
+ Every colour gets it, not only primary: neutral buttons drew `inverted/25`
894
+ (1.78:1 on white), and the error, success and info controls drew their own
895
+ 25% tint. The 2px offset puts page colour on both sides of the ring, so it
896
+ holds 3:1 beside a dark solid button as well as beside the page.
897
+
898
+ Matched on Nuxt UI's default focus width: only a control that took that
899
+ default is repainted, and anything that sets its own outline on purpose
900
+ keeps it. Unlayered on purpose, so it beats the utility. */
901
+ [class*="focus-visible:outline-3"]:focus-visible {
902
+ outline-color: var(--focus-ring-color);
903
+ outline-offset: 2px;
904
+ }
905
+
906
+ /* Autofill: the UA paints a hard yellow fill that ignores `background-color`.
907
+ Override it via the inset box-shadow trick so autofilled fields read as our
908
+ surface, keep token text colour, and don't fade back to yellow. */
909
+ input:-webkit-autofill,
910
+ input:-webkit-autofill:hover,
911
+ input:-webkit-autofill:focus,
912
+ textarea:-webkit-autofill,
913
+ select:-webkit-autofill {
914
+ -webkit-text-fill-color: var(--ui-text-highlighted);
915
+ -webkit-box-shadow: 0 0 0 1000px var(--ui-bg-elevated) inset;
916
+ box-shadow: 0 0 0 1000px var(--ui-bg-elevated) inset;
917
+ caret-color: var(--ui-text);
918
+ /* Defeat the UA's delayed fade-to-yellow without animating anything real. */
919
+ transition: background-color 9999s ease-in-out 0s;
920
+ }
921
+
922
+ /* Textareas grow to fit their content (vertical only — width stays stable),
923
+ bounded 3–16 lines so they can't collapse to nothing or run off a card.
924
+ Native CSS auto-grow; degrades to a fixed box + scroll where unsupported
925
+ (Firefox). */
926
+ textarea {
927
+ field-sizing: content;
928
+ min-block-size: 3lh;
929
+ max-block-size: 16lh;
930
+ }
931
+
932
+ /* Figma-style squircle corners on box primitives, as a native progressive
933
+ enhancement (Chrome 139+). `corner-shape` applies to the border, background
934
+ and overflow clipping in one declaration, so it needs no JS and renders at
935
+ SSR with zero hydration morph. `:where()` keeps specificity 0 so anything
936
+ can still override it. `rounded-full` is excluded deliberately — a
937
+ superellipse at pill/avatar radius distorts the circle into a blob. Browsers
938
+ without support ignore the property and keep the round `border-radius`;
939
+ UiCard additionally falls back to Lisse's clip-path (see `useSquircle`). */
940
+ @supports (corner-shape: squircle) {
941
+ :where(.rounded-md, .rounded-lg, .rounded-xl, .rounded-2xl) {
942
+ corner-shape: squircle;
943
+ }
944
+ }
945
+
946
+ .page-enter-active,
947
+ .page-leave-active {
948
+ /* Scope to opacity + transform only: `transition: all` forces the browser to
949
+ watch every property, and animating `filter: blur()` across a full dashboard
950
+ subtree (charts + tables) is GPU-expensive and drops frames on every nav.
951
+ 0.12s each way keeps the out-in cross-fade tight (~0.24s total). */
952
+ transition: opacity 0.12s var(--ease-standard), transform 0.12s var(--ease-standard);
953
+ }
954
+
955
+ .page-enter-from,
956
+ .page-leave-to {
957
+ opacity: 0;
958
+ transform: translateY(0.5rem);
959
+ }
960
+
961
+ /* Auth card content entrance (login / onboarding). These pages set
962
+ `pageTransition: false`: wrapping their Suspense subtree in Nuxt's page
963
+ `<Transition>` deadlocks the leave (the leaving page's unmount callback waits
964
+ on the incoming Suspense and never fires), so the incoming page never mounts
965
+ and the form stays blank until a hard reload. A mount-triggered CSS animation
966
+ gives the same fade-in without any Transition/Suspense involvement — it
967
+ replays whenever the page root element is freshly inserted on navigation. */
968
+ @keyframes auth-content-in {
969
+ from {
970
+ opacity: 0;
971
+ transform: translateY(0.5rem);
972
+ }
973
+ to {
974
+ opacity: 1;
975
+ transform: none;
976
+ }
977
+ }
978
+
979
+ .auth-content-in {
980
+ animation: auth-content-in 0.28s var(--ease-standard) both;
981
+ }
982
+
983
+ /* Ambient page glow. The diffuse halo is baked into the gradient itself (soft
984
+ stops fading to transparent) instead of a `filter: blur(180px)` over a
985
+ viewport-wide fixed element — a 180px blur is one of the most expensive
986
+ raster operations and re-rasterizes the whole surface on scroll / theme
987
+ change. `contain: paint` isolates it from the rest of the page. */
988
+ .gradient {
989
+ position: fixed;
990
+ top: 10vh;
991
+ width: 100%;
992
+ height: 60vh;
993
+ z-index: -1;
994
+ background: radial-gradient(60% 60% at 50% 40%, var(--ui-color-primary-200) 0%, color-mix(in oklab, var(--ui-color-primary-300) 60%, transparent) 35%, transparent 70%);
995
+ opacity: 0.5;
996
+ contain: paint;
997
+ pointer-events: none;
998
+ }
999
+
1000
+ .dark .gradient {
1001
+ background: radial-gradient(60% 60% at 50% 40%, var(--ui-color-primary-800) 0%, color-mix(in oklab, var(--ui-color-primary-900) 60%, transparent) 35%, transparent 70%);
1002
+ opacity: 0.18;
1003
+ }
1004
+
1005
+ @keyframes slide-right {
1006
+ from {
1007
+ transform: scale(0.6) translateX(100%);
1008
+ opacity: 0;
1009
+ }
1010
+ to {
1011
+ transform: translateX(0);
1012
+ opacity: 1;
1013
+ }
1014
+ }
1015
+
1016
+ @keyframes pop {
1017
+ from {
1018
+ transform: scale(0);
1019
+ opacity: 0;
1020
+ }
1021
+ to {
1022
+ transform: scale(1);
1023
+ opacity: 1;
1024
+ }
1025
+ }
1026
+
1027
+ .slide-right {
1028
+ animation: slide-right 400ms var(--ease-spring) forwards;
1029
+ }
1030
+
1031
+ .pop {
1032
+ animation: pop 400ms var(--ease-spring) forwards;
1033
+ }
1034
+
1035
+ /* One-time reveal for a RichSegment `delta` pair (UiRichText) — fades + lifts
1036
+ 2px on mount, once, never re-triggers on re-render (a plain `animation`
1037
+ on a freshly-created node runs on mount only, no watcher/class-toggle
1038
+ needed). Ends opaque + untransformed so the reduced-motion override below
1039
+ still lands on the correct final state. */
1040
+ @keyframes rich-delta-reveal {
1041
+ from {
1042
+ opacity: 0;
1043
+ transform: translateY(2px);
1044
+ }
1045
+ to {
1046
+ opacity: 1;
1047
+ transform: translateY(0);
1048
+ }
1049
+ }
1050
+ .rich-delta-reveal {
1051
+ animation: rich-delta-reveal 400ms var(--ease-spring) both;
1052
+ }
1053
+
1054
+ /* One-time dot pulse for UiCoverageNote's `partial` state (evidence still
1055
+ growing) — a single soft expand-and-settle, not the infinite
1056
+ `motion-safe:animate-pulse` UiHealthDot/UiSyncDot use for ongoing status.
1057
+ Ends at its resting scale/opacity. */
1058
+ @keyframes coverage-dot-pulse-once {
1059
+ 0% {
1060
+ transform: scale(1);
1061
+ opacity: 1;
1062
+ }
1063
+ 50% {
1064
+ transform: scale(1.8);
1065
+ opacity: 0.45;
1066
+ }
1067
+ 100% {
1068
+ transform: scale(1);
1069
+ opacity: 1;
1070
+ }
1071
+ }
1072
+ .coverage-dot-pulse-once {
1073
+ animation: coverage-dot-pulse-once 900ms var(--ease-standard) 1;
1074
+ }
1075
+
1076
+ .slide-up-enter-active, .slide-up-leave-active {
1077
+ transition: all 0.2s var(--ease-standard);
1078
+ }
1079
+ .slide-up-enter-from, .slide-up-leave-to {
1080
+ opacity: 0;
1081
+ transform: translateY(0.5rem);
1082
+ }
1083
+
1084
+ /* Unovis chart theming — prevent blinding white grid lines in dark mode.
1085
+ The tooltip vars repeat on the tooltip element itself because a tooltip
1086
+ mounted on `document.body` (UiTopEntityStackChart) is outside the container. */
1087
+ .dark :is(.unovis-xy-container, .unovis-single-container, .unovis-tooltip, .vis-tooltip) {
1088
+ --vis-axis-grid-color: var(--ui-border);
1089
+ --vis-axis-tick-color: var(--ui-border);
1090
+ --vis-axis-tick-label-color: var(--ui-text-dimmed);
1091
+ --vis-tooltip-background-color: var(--ui-bg-elevated);
1092
+ --vis-tooltip-border-color: var(--ui-border);
1093
+ --vis-tooltip-text-color: var(--ui-text);
1094
+ }
1095
+
1096
+ /* SVG ignores font-variant-numeric inheritance from the dashboard root — set it
1097
+ explicitly on Unovis tick labels and tooltip text. Same for crosshair/tooltip
1098
+ wrappers that teleport outside .dashboard-theme. */
1099
+ :is(.unovis-xy-container, .unovis-single-container) :is(.vis-axis-tick-label, text) {
1100
+ font-variant-numeric: tabular-nums;
1101
+ }
1102
+
1103
+ .unovis-tooltip,
1104
+ .unovis-crosshair-tooltip,
1105
+ .vis-tooltip {
1106
+ font-variant-numeric: tabular-nums;
1107
+ }
1108
+
1109
+ /* ─── Provisional data (texture channel) ───────────────────────────────────
1110
+ Diagonal hatch marking a mark whose observation is INCOMPLETE: the producer
1111
+ hit a row budget, the window is still filling, or the scope was truncated.
1112
+ The value drawn is a floor, not a count.
1113
+
1114
+ Texture is a third encoding channel alongside position and colour. It NEVER
1115
+ carries severity — colour keeps that job (`semanticColors`) and series
1116
+ identity (`proDataVizColors`), so a hatched mark stays readable in greyscale
1117
+ and the two channels never say different things about the same glyph.
1118
+
1119
+ Ink is thinned so a provisional mark RECEDES against its solid neighbour;
1120
+ incomplete must never read as louder than measured. Colour comes from
1121
+ `currentColor`, so callers set `text-*` (or inherit) and the hatch follows.
1122
+
1123
+ Never hand-roll a repeating-linear-gradient at a call site — DESIGN.md
1124
+ ("Data Visualization → Provisional marks") makes these two the only forms. */
1125
+ .provisional-hatch {
1126
+ background-image: repeating-linear-gradient(
1127
+ 45deg,
1128
+ color-mix(in oklab, currentColor 72%, transparent) 0 1.5px,
1129
+ transparent 1.5px 6px
1130
+ );
1131
+ }
1132
+
1133
+ /* Tighter pitch for small glyphs (day pills, legend swatches, table cells).
1134
+ Below ~20px the default pitch collapses to one or two strokes and reads as a
1135
+ smudge rather than a texture. */
1136
+ .provisional-hatch-fine {
1137
+ background-image: repeating-linear-gradient(
1138
+ 45deg,
1139
+ color-mix(in oklab, currentColor 80%, transparent) 0 1px,
1140
+ transparent 1px 3.5px
1141
+ );
1142
+ }
1143
+
1144
+ /* Mega menu animations — shared across all header dropdown menus */
1145
+ @keyframes mega-menu-enter {
1146
+ from { opacity: 0; transform: translateY(-4px) scale(0.98); }
1147
+ to { opacity: 1; transform: translateY(0) scale(1); }
1148
+ }
1149
+
1150
+ @keyframes mega-column-stagger {
1151
+ from { opacity: 0; transform: translateY(8px); }
1152
+ to { opacity: 1; transform: translateY(0); }
1153
+ }
1154
+
1155
+ @keyframes mega-item-fade {
1156
+ from { opacity: 0; transform: translateX(-6px); }
1157
+ to { opacity: 1; transform: translateX(0); }
1158
+ }
1159
+
1160
+ .mega-menu {
1161
+ animation: mega-menu-enter 0.2s ease-out;
1162
+ }
1163
+
1164
+ .mega-column {
1165
+ animation: mega-column-stagger 0.25s ease-out backwards;
1166
+ animation-delay: calc(var(--stagger, 0) * 40ms);
1167
+ }
1168
+
1169
+ .mega-item {
1170
+ animation: mega-item-fade 0.2s ease-out backwards;
1171
+ animation-delay: calc(var(--stagger, 0) * 40ms + var(--item-delay, 0) * 30ms + 80ms);
1172
+ }
1173
+
1174
+ /* Pro dashboard page container — width + responsive horizontal padding.
1175
+ Every pro dashboard page wrapper (ProPage, ProPageHeader, ProSiteFeaturePage,
1176
+ chat shells, scheduled-reports views) composes this class so the page-width
1177
+ token has one definition. Enforced by the `nuxtseo/pro-container-class`
1178
+ ESLint rule which bans inline `max-w-[1128px]` in /pro/dashboard files. */
1179
+ .pro-container {
1180
+ max-width: var(--pro-container-width);
1181
+ margin-inline: auto;
1182
+ @apply px-4 sm:px-6 lg:px-9;
1183
+ }
1184
+
1185
+ /* Section accent — module-colored section tints (landing sections, docs headers, module-scoped pages) */
1186
+ .section-accent-pink { @apply bg-pink-50 dark:bg-pink-500/5 border-pink-200/50 dark:border-pink-500/10; }
1187
+ .section-accent-purple { @apply bg-purple-50 dark:bg-purple-500/5 border-purple-200/50 dark:border-purple-500/10; }
1188
+ .section-accent-yellow { @apply bg-yellow-50 dark:bg-yellow-500/5 border-yellow-200/50 dark:border-yellow-500/10; }
1189
+ .section-accent-green { @apply bg-green-50 dark:bg-green-500/5 border-green-200/50 dark:border-green-500/10; }
1190
+ .section-accent-blue { @apply bg-blue-50 dark:bg-blue-500/5 border-blue-200/50 dark:border-blue-500/10; }
1191
+ .section-accent-sky { @apply bg-sky-50 dark:bg-sky-500/5 border-sky-200/50 dark:border-sky-500/10; }
1192
+
1193
+ /* Dashboard mauve palette: editorial monochrome tone reserved for /pro/dashboard.
1194
+ Marketing/docs pages stay on the warm-slate neutral (hue 285). A site keeps
1195
+ its own ramp here by setting `--ui-dashboard-neutral-<step>` (README). */
1196
+ .dashboard-theme {
1197
+ --ui-color-neutral-50: var(--ui-dashboard-neutral-50, var(--color-mauve-50, oklch(98.5% 0 0)));
1198
+ --ui-color-neutral-100: var(--ui-dashboard-neutral-100, var(--color-mauve-100, oklch(96.7% 0.001 286.375)));
1199
+ --ui-color-neutral-200: var(--ui-dashboard-neutral-200, var(--color-mauve-200, oklch(92% 0.004 286.32)));
1200
+ --ui-color-neutral-300: var(--ui-dashboard-neutral-300, var(--color-mauve-300, oklch(87.1% 0.006 286.286)));
1201
+ --ui-color-neutral-400: var(--ui-dashboard-neutral-400, var(--color-mauve-400, oklch(70.5% 0.015 286.067)));
1202
+ --ui-color-neutral-500: var(--ui-dashboard-neutral-500, var(--color-mauve-500, oklch(55.2% 0.016 285.938)));
1203
+ --ui-color-neutral-600: var(--ui-dashboard-neutral-600, var(--color-mauve-600, oklch(44.2% 0.017 285.786)));
1204
+ --ui-color-neutral-700: var(--ui-dashboard-neutral-700, var(--color-mauve-700, oklch(37.4% 0.016 285.822)));
1205
+ --ui-color-neutral-800: var(--ui-dashboard-neutral-800, var(--color-mauve-800, oklch(27.4% 0.006 286.033)));
1206
+ --ui-color-neutral-900: var(--ui-dashboard-neutral-900, var(--color-mauve-900, oklch(21% 0.006 285.885)));
1207
+ --ui-color-neutral-950: var(--ui-dashboard-neutral-950, var(--color-mauve-950, oklch(14.1% 0.005 285.823)));
1208
+ }
1209
+
1210
+ .light.dashboard-theme {
1211
+ background-color: var(--ui-bg);
1212
+ --ui-bg: #fff;
1213
+ --ui-bg-muted: var(--ui-color-neutral-50);
1214
+ --ui-bg-elevated: var(--ui-color-neutral-100);
1215
+ --ui-bg-accented: var(--ui-color-neutral-200);
1216
+ --ui-bg-inverted: var(--ui-color-neutral-900);
1217
+ --ui-text-dimmed: var(--ui-dashboard-light-text-dimmed, oklch(47.5% 0.018 286));
1218
+ --ui-text-muted: var(--ui-color-neutral-600);
1219
+ --ui-text-toned: var(--ui-color-neutral-700);
1220
+ --ui-text: var(--ui-color-neutral-800);
1221
+ --ui-text-highlighted: var(--ui-color-neutral-900);
1222
+ --ui-text-inverted: #fff;
1223
+ --ui-border: var(--ui-color-neutral-200);
1224
+ --ui-border-muted: var(--ui-color-neutral-100);
1225
+ --ui-border-accented: var(--ui-color-neutral-300);
1226
+ --ui-border-inverted: var(--ui-color-neutral-900);
1227
+ }
1228
+
1229
+ .dark.dashboard-theme {
1230
+ background-color: var(--ui-bg);
1231
+ --color-mauve-50: oklch(98.5% 0.002 326);
1232
+ --color-mauve-100: oklch(96.4% 0.005 325.6);
1233
+ --color-mauve-200: oklch(92.5% 0.011 325.62);
1234
+ --color-mauve-300: oklch(86.7% 0.020 325.68);
1235
+ --color-mauve-400: oklch(70.8% 0.032 323.02);
1236
+ --color-mauve-500: oklch(54.7% 0.044 322.5);
1237
+ --color-mauve-600: oklch(44% 0.040 321.78);
1238
+ --color-mauve-700: oklch(34.4% 0.038 323.89);
1239
+ --color-mauve-800: oklch(24.4% 0.032 320.12);
1240
+ --color-mauve-900: oklch(18.9% 0.026 322.12);
1241
+ --color-mauve-950: oklch(14.8% 0.018 326);
1242
+ /* `.dashboard-theme` binds `--ui-color-neutral-*` to these steps on this same
1243
+ element, so the dark values above flow through without a second binding. */
1244
+
1245
+ /* Mauve 400 measures 4.48:1 on mauve 700 (bg-accented), so muted and dimmed
1246
+ share one step just above it: 4.6:1 there, 7.7:1 on bg-default. */
1247
+ --ui-text-dimmed: var(--ui-dashboard-dark-text-dimmed, oklch(71.5% 0.032 323));
1248
+ --ui-text-muted: var(--ui-dashboard-dark-text-muted, oklch(71.5% 0.032 323));
1249
+ --ui-text-toned: var(--ui-color-neutral-300);
1250
+ --ui-text: var(--ui-color-neutral-200);
1251
+ --ui-text-highlighted: #fff;
1252
+ --ui-text-inverted: var(--ui-color-neutral-900);
1253
+ --ui-bg: var(--ui-color-neutral-950);
1254
+ --ui-bg-muted: var(--ui-color-neutral-900);
1255
+ --ui-bg-elevated: var(--ui-color-neutral-800);
1256
+ --ui-bg-accented: var(--ui-color-neutral-700);
1257
+ --ui-bg-inverted: #fff;
1258
+ --ui-border: var(--ui-color-neutral-800);
1259
+ --ui-border-muted: var(--ui-color-neutral-700);
1260
+ --ui-border-accented: var(--ui-color-neutral-700);
1261
+ --ui-border-inverted: #fff;
1262
+ }
1263
+
1264
+ /* Surface utility for hero/feature/pricing cards on marketing pages.
1265
+ One source of truth for the translucent card treatment — tune here, not per-page.
1266
+ Tier: "feature" cards in the radius-by-elevation rule (rounded-xl / 12px). */
1267
+ .surface-soft {
1268
+ background-color: color-mix(in oklab, var(--ui-bg) 60%, transparent);
1269
+ border: 1px solid var(--ui-border);
1270
+ border-radius: 0.75rem;
1271
+ box-shadow: var(--elevation-raised);
1272
+ transition: border-color 200ms var(--ease-standard), box-shadow 200ms var(--ease-standard);
1273
+ }
1274
+
1275
+ .dark .surface-soft {
1276
+ background-color: color-mix(in oklab, var(--ui-bg-elevated) 5%, transparent);
1277
+ }
1278
+
1279
+ .surface-soft:hover {
1280
+ border-color: var(--ui-border-accented);
1281
+ }
1282
+
1283
+ /* Marketing links/cards — shared landing-page surfaces built on semantic tokens. */
1284
+ .surface-link {
1285
+ @apply relative block h-full rounded-lg ring-1 ring-default bg-default transition-all hover:ring-accented hover:bg-muted/50;
1286
+ }
1287
+
1288
+ .surface-stat {
1289
+ @apply p-5;
1290
+ background-color: color-mix(in oklab, var(--ui-bg) 60%, transparent);
1291
+ border: 1px solid var(--ui-border);
1292
+ border-radius: 0.75rem;
1293
+ box-shadow: var(--elevation-raised);
1294
+ transition: border-color 200ms var(--ease-standard), box-shadow 200ms var(--ease-standard);
1295
+ }
1296
+
1297
+ .dark .surface-stat {
1298
+ background-color: color-mix(in oklab, var(--ui-bg-elevated) 5%, transparent);
1299
+ }
1300
+
1301
+ .surface-stat:hover {
1302
+ border-color: var(--ui-border-accented);
1303
+ }
1304
+
1305
+ .marketing-pill {
1306
+ @apply inline-flex items-center gap-2 rounded-full bg-default text-highlighted ring-1 ring-default shadow-sm transition-all;
1307
+ }
1308
+
1309
+ .marketing-label {
1310
+ @apply inline-flex items-center gap-2 rounded-full bg-default ring-1 ring-default text-xs font-semibold tracking-wide text-highlighted;
1311
+ }
1312
+
1313
+ /* Inset surface for nested content (code rows, mini-stats, whispered panels)
1314
+ one tier deeper than surface-soft. Used inside cards. */
1315
+ .surface-inset {
1316
+ background-color: var(--ui-bg-muted);
1317
+ border: 1px solid var(--ui-border-muted);
1318
+ }
1319
+
1320
+ .dark .surface-inset {
1321
+ background-color: color-mix(in oklab, var(--ui-bg) 50%, var(--ui-bg-muted));
1322
+ }
1323
+
1324
+ /* ═══ Atmosphere — the greydient application layer ═════════════════════════
1325
+ Rendered through <UiAtmosphere>. Three axes tuned here ONCE:
1326
+ palette — which --greydient-* (set via --_atmo-greydient)
1327
+ geometry — sky | bloom | wash | veil (position + mask)
1328
+ intensity — subtle | ambient | present | vivid (opacity, mode-aware)
1329
+ The .dark overrides are GLOBAL on purpose: a scoped `:global(.dark) .x` rule
1330
+ collapses to a bare `.dark {}` and dims the whole app (learned the hard way).
1331
+ Consumers never hand-roll masks/opacity again — they pick intent. */
1332
+ .ui-atmosphere {
1333
+ position: absolute;
1334
+ inset: 0;
1335
+ z-index: 0;
1336
+ pointer-events: none;
1337
+ background-image: var(--_atmo-greydient, var(--greydient-ash));
1338
+ opacity: var(--_atmo-op, 0.6);
1339
+ }
1340
+
1341
+ /* intensity → opacity (light) */
1342
+ .ui-atmosphere[data-intensity="subtle"] { --_atmo-op: 0.45; }
1343
+ .ui-atmosphere[data-intensity="ambient"] { --_atmo-op: 0.6; }
1344
+ .ui-atmosphere[data-intensity="present"] { --_atmo-op: 0.78; }
1345
+ .ui-atmosphere[data-intensity="vivid"] { --_atmo-op: 0.58; }
1346
+ /* intensity → opacity (dark — quieter so data/text stays the signal) */
1347
+ .dark .ui-atmosphere[data-intensity="subtle"] { --_atmo-op: 0.14; }
1348
+ .dark .ui-atmosphere[data-intensity="ambient"] { --_atmo-op: 0.26; }
1349
+ .dark .ui-atmosphere[data-intensity="present"] { --_atmo-op: 0.38; }
1350
+ .dark .ui-atmosphere[data-intensity="vivid"] { --_atmo-op: 0.58; }
1351
+
1352
+ /* geometry → position + mask */
1353
+ .ui-atmosphere[data-geometry="sky"] {
1354
+ inset: 0 0 auto 0;
1355
+ height: 70vh;
1356
+ mask-image: linear-gradient(to bottom, black 0%, transparent 86%);
1357
+ -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 86%);
1358
+ }
1359
+ .ui-atmosphere[data-geometry="bloom"] {
1360
+ mask-image: radial-gradient(58% 50% at 50% 38%, black, transparent 76%);
1361
+ -webkit-mask-image: radial-gradient(58% 50% at 50% 38%, black, transparent 76%);
1362
+ }
1363
+ .ui-atmosphere[data-geometry="wash"] {
1364
+ mask-image: radial-gradient(92% 120% at 16% 22%, black, transparent 76%);
1365
+ -webkit-mask-image: radial-gradient(92% 120% at 16% 22%, black, transparent 76%);
1366
+ }
1367
+ /* veil = full scrim tint over a faded preview; no mask, softer by default */
1368
+
1369
+ /* grain — baked in, static (soft-light so it reads as tooth, not dirt) */
1370
+ .ui-atmosphere[data-grain]::after {
1371
+ content: "";
1372
+ position: absolute;
1373
+ inset: 0;
1374
+ pointer-events: none;
1375
+ opacity: 0.5;
1376
+ mix-blend-mode: soft-light;
1377
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
1378
+ background-size: 200px 200px;
1379
+ }
1380
+
1381
+ /* ═══ Semantic type tokens ═══════════════════════════════════════════════
1382
+ Each bundles every Hubot Sans axis (family + size + weight + width + tracking
1383
+ + leading) for its role, all read from the :root wght/wdth/tracking vars — so
1384
+ the whole type system tunes from one place. Headings are full "super tokens";
1385
+ .font-emphasis/.font-strong are the orthogonal weight axis for body text
1386
+ (combine with text-sm, text-xs, .text-mini). */
1387
+
1388
+ /* Page title — the largest in-page heading (page headers, hero metrics). Size is
1389
+ a role var so it stays editorial on marketing, compact inside .dashboard-theme. */
1390
+ .text-title {
1391
+ font-family: var(--font-title);
1392
+ font-size: var(--size-title);
1393
+ font-weight: var(--wght-title);
1394
+ font-stretch: var(--wdth-heading);
1395
+ letter-spacing: var(--tracking-tight);
1396
+ line-height: 1.15;
1397
+ text-wrap-style: balance;
1398
+ }
1399
+
1400
+ /* Section heading — groups of cards / a titled region within a page. */
1401
+ .text-heading {
1402
+ font-family: var(--font-display);
1403
+ font-size: var(--size-heading);
1404
+ font-weight: var(--wght-heading);
1405
+ font-stretch: var(--wdth-heading);
1406
+ letter-spacing: var(--tracking-snug);
1407
+ line-height: 1.3;
1408
+ text-wrap: balance;
1409
+ }
1410
+
1411
+ /* Subheading — card / group title, one tier below a section heading. Carries the
1412
+ lighter emphasis weight (not full heading weight) so it stays distinct from
1413
+ .text-heading even where their sizes sit close in the dense dashboard. */
1414
+ .text-subheading {
1415
+ font-family: var(--font-display);
1416
+ font-size: var(--size-subheading);
1417
+ font-weight: var(--wght-emphasis);
1418
+ font-stretch: var(--wdth-heading);
1419
+ letter-spacing: var(--tracking-snug);
1420
+ line-height: 1.35;
1421
+ text-wrap: balance;
1422
+ }
1423
+
1424
+ /* Body weight axis — emphasis (slightly bold) and strong (heading-weight) for
1425
+ inline/body text. Weight only; pair with a size token. Both read the vars so
1426
+ weight tunes centrally instead of via raw font-medium/font-semibold. */
1427
+ .font-emphasis {
1428
+ font-weight: var(--wght-emphasis);
1429
+ }
1430
+ .font-strong {
1431
+ font-weight: var(--wght-heading);
1432
+ }
1433
+
1434
+ /* Stat numerals — display-font tabular figures for counters, KPIs, big numbers.
1435
+ Size-agnostic (figures vary); weight/width read the numeral axis vars. */
1436
+ .numerals-display {
1437
+ font-family: var(--font-display);
1438
+ font-weight: var(--wght-numeral);
1439
+ font-stretch: var(--wdth-numeral);
1440
+ font-variant-numeric: tabular-nums;
1441
+ font-feature-settings: 'tnum' 1, 'ss01' 1, 'lnum' 1;
1442
+ }
1443
+
1444
+ /* Heavier numeral weight step — layers on top of .numerals-display (same
1445
+ family/stretch/tabular features), stepping only the weight up the --wght
1446
+ axis. Same-specificity selector defined after .numerals-display, so source
1447
+ order resolves the font-weight conflict for elements carrying both classes.
1448
+ Use for the side of a pair that should read as the "current" or hero value
1449
+ (a delta's `to`, a lone metric standing in for a whole sentence) — never
1450
+ both sides of a pair at once, that would cancel the emphasis. */
1451
+ .numerals-display-heavy {
1452
+ font-weight: var(--wght-heading);
1453
+ }
1454
+
1455
+ /* Description text — the muted subtitle that sits under a title in page headers
1456
+ (PageHeader, ProPageHeader, auth screens). One token so size / leading / color
1457
+ stay locked together. Re-based onto text-xs(12) when .text-dense was retired. */
1458
+ .text-description {
1459
+ @apply text-xs text-muted leading-snug;
1460
+ }
1461
+
1462
+ /* Mini — the 10px micro tier below Tailwind's 12px floor: metadata, captions,
1463
+ timestamps, tiny chart/tooltip labels, badges, kbd, micro figures. Size only;
1464
+ color (usually dimmed) at call site. Absorbed the retired .text-meta (11→10).
1465
+ For 12px+ use Tailwind primitives (text-xs/sm/…) directly. */
1466
+ .text-mini {
1467
+ @apply text-[10px];
1468
+ }
1469
+
1470
+ /* Label — the dense uppercase-tracked chrome label inside the dashboard:
1471
+ column headers, metric/filter labels, stat captions. Consolidates a sprawl
1472
+ of 10–12px uppercase combos (varying weight/tracking) onto one spec.
1473
+ Color is a muted DEFAULT baked in: at 10px this is normal text for WCAG, and
1474
+ the old `text-label text-dimmed` idiom measured 4.22:1 on bg-default in dark
1475
+ (3.61 on bg-elevated) — under the 4.5:1 AA floor. Muted lands at 7.67:1 and
1476
+ still recedes behind the data. In @layer components so the call sites that
1477
+ need primary/success/current keep overriding it (an unlayered rule would beat
1478
+ the utility and silently force muted). */
1479
+ @layer components {
1480
+ .text-label {
1481
+ @apply text-[10px] font-semibold uppercase tracking-wider;
1482
+ color: var(--ui-text-muted);
1483
+ }
1484
+ }
1485
+
1486
+ /* Micro label — the 9px tier below .text-label, for the tiniest tags (table
1487
+ corner flags, inline status pills). Same uppercase-tracked treatment. */
1488
+ .text-micro {
1489
+ @apply text-[9px] uppercase tracking-wider;
1490
+ }
1491
+
1492
+ /* Inline code chip — one definition for `<code>` inside prose / UI so the mono
1493
+ size, tint and padding stop drifting (bg-elevated vs bg-muted, text-mini vs
1494
+ text-xs, varying px were all in the wild). Relative size scales with context. */
1495
+ .code-inline {
1496
+ font-family: var(--font-mono);
1497
+ font-size: 0.85em;
1498
+ background-color: var(--ui-bg-muted);
1499
+ color: var(--ui-text);
1500
+ border-radius: 0.25rem;
1501
+ padding: 0.0625rem 0.3125rem;
1502
+ }
1503
+
1504
+ /* ─── Overlay chrome (UiPopover / UDropdownMenu / UContextMenu / USelectMenu) ───
1505
+ Tooltip-grade depth without gradient surfaces. Inset edges create the bevel.
1506
+ shadow-sm tier is permitted here per DESIGN.pro.md (overlays only). */
1507
+ .ui-popover-content {
1508
+ background-image: var(--surface-raised);
1509
+ box-shadow: var(--elevation-popover) !important;
1510
+ letter-spacing: var(--tracking-data);
1511
+ -webkit-font-smoothing: antialiased;
1512
+ font-variant-numeric: tabular-nums;
1513
+ }
1514
+
1515
+ /* Alert depth — same tooltip-grade bevel as .ui-popover-content, tuned for
1516
+ inline alert containers. Pairs with the color-tinted bg/border from Alert.vue. */
1517
+ .ui-alert {
1518
+ background-image: var(--surface-raised);
1519
+ box-shadow: var(--elevation-overlay);
1520
+ letter-spacing: var(--tracking-data);
1521
+ -webkit-font-smoothing: antialiased;
1522
+ font-variant-numeric: tabular-nums;
1523
+ }
1524
+
1525
+ /* Menu item hover: outline-ring (no bg fill, no layout shift) — matches the
1526
+ UiTooltipGrid active-cell vocabulary. Active/selected gets a left accent bar. */
1527
+ .ui-dropdown-item {
1528
+ position: relative;
1529
+ /* Transparent base outline so transition-colors fades to the accent on
1530
+ highlight instead of animating from the initial outline-color (invert/
1531
+ currentColor), which flashed white when switching the selected item. */
1532
+ outline: 1.5px solid transparent;
1533
+ outline-offset: -2px;
1534
+ }
1535
+ .ui-dropdown-item:where([data-highlighted], :hover, :focus-visible) {
1536
+ outline-color: var(--ui-border-accented);
1537
+ }
1538
+ .ui-dropdown-item:where([data-state="checked"], [aria-checked="true"], [aria-selected="true"])::before {
1539
+ content: '';
1540
+ position: absolute;
1541
+ left: 0;
1542
+ top: 4px;
1543
+ bottom: 4px;
1544
+ width: 2px;
1545
+ border-radius: 999px;
1546
+ background: var(--ui-text);
1547
+ opacity: 0.55;
1548
+ }
1549
+
1550
+ .ui-dropdown-separator {
1551
+ border-top-width: 1px;
1552
+ }
1553
+
1554
+ /* Disclosure (UiDisclosure) — native <details>. Chromium animates the content
1555
+ block to its intrinsic height via interpolate-size + ::details-content;
1556
+ Firefox/Safari toggle instantly (graceful degradation, no JS fallback). */
1557
+ .ui-disclosure {
1558
+ interpolate-size: allow-keywords;
1559
+ }
1560
+
1561
+ .ui-disclosure::details-content {
1562
+ block-size: 0;
1563
+ overflow: clip;
1564
+ transition:
1565
+ block-size 200ms var(--ease-standard),
1566
+ content-visibility 200ms allow-discrete;
1567
+ }
1568
+
1569
+ .ui-disclosure[open]::details-content {
1570
+ block-size: auto;
1571
+ }
1572
+
1573
+ @media (prefers-reduced-motion: reduce) {
1574
+ .ui-dropdown-item,
1575
+ .ui-disclosure::details-content {
1576
+ transition: none !important;
1577
+ }
1578
+
1579
+ /* Pure-CSS animations/transitions don't go through motion-v's
1580
+ useReducedMotion() gate, so disable them here. Elements still settle on
1581
+ their final (visible) state because every keyframe/transition above ends
1582
+ opaque and untransformed. */
1583
+ .page-enter-active,
1584
+ .page-leave-active,
1585
+ .slide-up-enter-active,
1586
+ .slide-up-leave-active {
1587
+ transition: none !important;
1588
+ }
1589
+
1590
+ .auth-content-in {
1591
+ animation: none !important;
1592
+ }
1593
+
1594
+ .slide-right,
1595
+ .pop,
1596
+ .mega-menu,
1597
+ .mega-column,
1598
+ .mega-item,
1599
+ .rich-delta-reveal,
1600
+ .coverage-dot-pulse-once {
1601
+ animation: none !important;
1602
+ }
1603
+ }
1604
+
1605
+ /* Leading icon in dropdown/context/select menus rendered like ProNavIcon:
1606
+ primary-tinted rounded square behind the iconify mask. The icon span is
1607
+ mask-rendered (no children, no own background possible), so ::before draws
1608
+ the container behind it. Light + dark variants. */
1609
+ /* The icon span is mask-rendered; ::before tricks behind it get clipped by
1610
+ sibling stacking contexts. Use double box-shadow instead — paints rings
1611
+ OUTSIDE the icon's box, creating a visible tinted container. */
1612
+ .ui-dropdown-lead-icon {
1613
+ border-radius: 4px;
1614
+ margin-inline: 4px;
1615
+ box-shadow:
1616
+ 0 0 0 4px color-mix(in srgb, var(--ui-color-primary-400) 14%, transparent),
1617
+ 0 0 0 5px color-mix(in srgb, var(--ui-color-primary-400) 24%, transparent);
1618
+ transition: box-shadow 140ms ease-out;
1619
+ }
1620
+ .dark .ui-dropdown-lead-icon {
1621
+ box-shadow:
1622
+ 0 0 0 4px color-mix(in srgb, var(--ui-color-primary-700) 22%, transparent),
1623
+ 0 0 0 5px color-mix(in srgb, var(--ui-color-primary-600) 30%, transparent);
1624
+ }
1625
+ /* Highlighted/hovered — container picks up a bit more primary. */
1626
+ [data-highlighted] .ui-dropdown-lead-icon,
1627
+ .ui-dropdown-item:hover .ui-dropdown-lead-icon {
1628
+ box-shadow:
1629
+ 0 0 0 4px color-mix(in srgb, var(--ui-color-primary-400) 22%, transparent),
1630
+ 0 0 0 5px color-mix(in srgb, var(--ui-color-primary-400) 36%, transparent);
1631
+ }
1632
+ .dark [data-highlighted] .ui-dropdown-lead-icon,
1633
+ .dark .ui-dropdown-item:hover .ui-dropdown-lead-icon {
1634
+ box-shadow:
1635
+ 0 0 0 4px color-mix(in srgb, var(--ui-color-primary-600) 35%, transparent),
1636
+ 0 0 0 5px color-mix(in srgb, var(--ui-color-primary-500) 45%, transparent);
1637
+ }
1638
+
1639
+ /* Focused surface (auth screens + onboarding wizard). UiCard's translucent fill
1640
+ (bg-elevated at 5–35%) is tuned for the dashboard's recessed backdrop; on the
1641
+ focused surface's near-white base that leaves cards ~1% off the page, so they
1642
+ read as empty outlines. Make the fill opaque here so onboarding/auth cards are
1643
+ legible. Dashboard cards are outside `.ui-focused` and keep their translucency.
1644
+ The emphasis card's shadow is an inline style and survives this. */
1645
+ .ui-focused [data-ui-card] {
1646
+ background-color: var(--ui-bg-elevated);
1647
+ }
1648
+
1649
+ /* ─────────────────────────────── Docs prose ───────────────────────────────
1650
+ Markdown rendering fixes that belong to the renderer, not to any one page.
1651
+ Every selector keys on a `prose-*` hook added by `prose.*` in
1652
+ `app/app.config.ts`, so one rule reaches docs, learn-seo and recipes, and
1653
+ module Markdown in ~/pkg stays untouched. These rules are unlayered on
1654
+ purpose: Nuxt UI's prose themes ship arbitrary variants such as
1655
+ `[&>code]:border-dashed` in `@layer utilities`, which a layered rule of the
1656
+ same specificity would lose to. */
1657
+
1658
+ /* Inline code must never set the page's min-content width. API headings carry
1659
+ type signatures with no break opportunity ("urls:
1660
+ MaybeFunction<MaybePromise<SitemapUrlInput[]>>"), which scrolled the whole
1661
+ document sideways at 390px. */
1662
+ code.prose-code {
1663
+ max-width: 100%;
1664
+ overflow-wrap: anywhere;
1665
+ }
1666
+
1667
+ /* …except inside a table cell. A cell is sized from its min-content width, and
1668
+ `anywhere` makes that one character, so the auto table layout crushed the
1669
+ Type column of every API table into a four-line sliver at 390px
1670
+ ("string | string[]" read as "stri / ng | / stri / ng[]"). The table root
1671
+ scrolls, so the extra width has somewhere to go: break a token only when the
1672
+ token alone cannot fit, which leaves the column as wide as its longest word.
1673
+ `break-word` is the load-bearing value here — it does not feed back into
1674
+ min-content sizing the way `anywhere` does. */
1675
+ :is(.prose-table th, .prose-table td) code.prose-code {
1676
+ overflow-wrap: break-word;
1677
+ }
1678
+
1679
+ /* A table wider than the article scrolls inside its own container, and nothing
1680
+ at the border said so: the last column of a five-column table just stopped,
1681
+ which reads as a four-column table. Two `local` covers ride with the content
1682
+ and two `scroll` shadows stay pinned to the container, so an edge is shaded
1683
+ exactly while there is more table past it and clean once you reach the end.
1684
+ The shadows sit under the cells, so they show against the body rows; the
1685
+ header keeps its own tinted fill. */
1686
+ .prose-table-scroll {
1687
+ background-image:
1688
+ linear-gradient(to right, var(--ui-bg), transparent),
1689
+ linear-gradient(to left, var(--ui-bg), transparent),
1690
+ linear-gradient(to right, color-mix(in oklab, var(--ui-text) 28%, transparent), transparent),
1691
+ linear-gradient(to left, color-mix(in oklab, var(--ui-text) 28%, transparent), transparent);
1692
+ background-position: 0 0, 100% 0, 0 0, 100% 0;
1693
+ background-repeat: no-repeat;
1694
+ background-size: 3rem 100%, 3rem 100%, 1.25rem 100%, 1.25rem 100%;
1695
+ background-attachment: local, local, scroll, scroll;
1696
+ }
1697
+
1698
+ /* A fenced code block scrolls; it never wraps. Nuxt UI ships the `pre` as
1699
+ `whitespace-pre-wrap wrap-break-word`, which broke a URL, a token or a type
1700
+ signature mid-token with nothing to say the break was not in the code — and
1701
+ because nothing ever overflowed, `overflow-x: auto` never produced a
1702
+ scrollbar (`scrollWidth === clientWidth` on every block measured). Keep the
1703
+ line intact and let the block scroll. Inline code in prose and in table cells
1704
+ still wraps: the rules above are scoped to `code.prose-code`, and a fenced
1705
+ block's `code` child carries no such class. */
1706
+ pre.prose-pre {
1707
+ white-space: pre;
1708
+ overflow-wrap: normal;
1709
+ word-break: normal;
1710
+ overflow-x: auto;
1711
+ /* A sideways flick inside the block must not chain to the page or to the
1712
+ browser's back gesture. */
1713
+ overscroll-behavior-x: contain;
1714
+ }
1715
+
1716
+ /* The token stream inside a fenced block is never its own surface. Nuxt UI's
1717
+ callout theme fills every descendant `code` with `bg-default`, which is right
1718
+ for an inline pill and wrong for a fenced block: it painted an opaque white
1719
+ band across the block inside a tip callout, ending where the longest line
1720
+ ended, so the block read as a mis-sized inner element. The fill is dropped
1721
+ for every fenced block, not only in a callout, so the next rule that tints
1722
+ `code` cannot leak into one either. */
1723
+ pre.prose-pre > code {
1724
+ background-color: transparent;
1725
+ border-color: transparent;
1726
+ }
1727
+
1728
+ /* The scroll has to announce itself, and it has to do it while the pointer is
1729
+ somewhere else. Two `local` covers ride with the content and two `scroll`
1730
+ shadows stay pinned, so an edge is shaded exactly while there is more code
1731
+ past it — the same treatment the wide-table container carries, keyed to the
1732
+ block's own `bg-muted` surface. The track is drawn too: the document
1733
+ scrollbar palette is a flat grey pair, so a block on `bg-muted` takes a track
1734
+ and thumb mixed from the text colour and reads in both themes. */
1735
+ pre.prose-pre {
1736
+ scrollbar-width: thin;
1737
+ scrollbar-color:
1738
+ color-mix(in oklab, var(--ui-text) 32%, transparent)
1739
+ color-mix(in oklab, var(--ui-text) 9%, transparent);
1740
+ background-image:
1741
+ linear-gradient(to right, var(--ui-bg-muted), transparent),
1742
+ linear-gradient(to left, var(--ui-bg-muted), transparent),
1743
+ linear-gradient(to right, color-mix(in oklab, var(--ui-text) 28%, transparent), transparent),
1744
+ linear-gradient(to left, color-mix(in oklab, var(--ui-text) 28%, transparent), transparent);
1745
+ background-position: 0 0, 100% 0, 0 0, 100% 0;
1746
+ background-repeat: no-repeat;
1747
+ background-size: 2rem 100%, 2rem 100%, 1rem 100%, 1rem 100%;
1748
+ background-attachment: local, local, scroll, scroll;
1749
+ }
1750
+ /* …except inside a callout, which tints its own code block from the callout's
1751
+ hue (see the `div.prose-callout pre` rule below). A cover keyed to `bg-muted`
1752
+ paints a grey smear over that tint at both edges, so there the scrollbar
1753
+ carries the affordance alone. */
1754
+ div.prose-callout pre.prose-pre {
1755
+ background-image: none;
1756
+ }
1757
+
1758
+ pre.prose-pre::-webkit-scrollbar {
1759
+ height: 8px;
1760
+ }
1761
+ pre.prose-pre::-webkit-scrollbar-track {
1762
+ background-color: color-mix(in oklab, var(--ui-text) 9%, transparent);
1763
+ border-radius: 9999px;
1764
+ }
1765
+ pre.prose-pre::-webkit-scrollbar-thumb {
1766
+ background-color: color-mix(in oklab, var(--ui-text) 32%, transparent);
1767
+ border-radius: 9999px;
1768
+ }
1769
+
1770
+ /* The external-link arrow on outbound prose links lives in
1771
+ build/external-links.ts: its own-site hosts are a build option. */
1772
+
1773
+ /* Inline code inside a link. `prose.code` is an atomic inline (`inline-block`
1774
+ with its own fill), so the link underline stopped at the pill and the pill's
1775
+ 6px side padding read as a gap before the next word ("AiBots constant").
1776
+ Inside a link the code keeps the mono face and drops the pill, so the
1777
+ underline runs unbroken and the gap disappears. */
1778
+ a.prose-link > code {
1779
+ /* `inline-block` is an atomic inline: the link's underline stops at its edge
1780
+ whatever the fill is, so the display change is load-bearing here. */
1781
+ display: inline;
1782
+ background-color: transparent;
1783
+ border-color: transparent;
1784
+ padding-inline: 0;
1785
+ color: inherit;
1786
+ }
1787
+
1788
+ /* Callout. Nuxt UI lays the icon out as an inline box and expects the first
1789
+ paragraph to be unwrapped (`mdc-unwrap="p"`); comark keeps the `<p>`, so the
1790
+ icon was pushed onto a line of its own. Give it a fixed gutter instead, and
1791
+ flatten the paragraph margins the unwrap used to remove. */
1792
+ div.prose-callout:has(> .prose-callout-icon) {
1793
+ padding-inline-start: 2.5rem;
1794
+ }
1795
+ div.prose-callout > .prose-callout-icon {
1796
+ position: absolute;
1797
+ inset-inline-start: 1rem;
1798
+ top: 0.9375rem;
1799
+ margin-inline-end: 0;
1800
+ }
1801
+ div.prose-callout > p {
1802
+ margin-block: 0;
1803
+ }
1804
+ div.prose-callout > p + * {
1805
+ margin-block-start: 0.625rem;
1806
+ }
1807
+
1808
+ /* Callout body copy is body copy. Nuxt UI tints the whole block with
1809
+ `text-{color}-600 dark:text-{color}-300`, so an info callout read as a blue
1810
+ paragraph and a tip callout as a green one — the hue said "this sentence is
1811
+ special" when it only meant "this is a note". The icon, the hairline border
1812
+ and the low-alpha fill carry the semantic hue; the sentence does not. Links
1813
+ and the icon set their own colour on themselves, so they are untouched. */
1814
+ div.prose-callout {
1815
+ color: var(--ui-text);
1816
+ }
1817
+
1818
+ /* Inline code inside a callout. Nuxt UI fills the pill with `bg-default` and a
1819
+ full border, which punched a hard white hole through the tinted fill and made
1820
+ a package name heavier than the sentence holding it. Derive the pill from the
1821
+ callout it sits in: a faint tint of its own colour, a hairline of the same
1822
+ hue, no second surface. `currentcolor` keeps one rule correct for all nine
1823
+ callout colours and both themes. */
1824
+ div.prose-callout :not(pre) > code {
1825
+ background-color: color-mix(in oklab, currentcolor 10%, transparent);
1826
+ border-color: color-mix(in oklab, currentcolor 20%, transparent);
1827
+ }
1828
+
1829
+ /* A `pre` inside a callout is a real code block, not a pill, so it keeps a
1830
+ surface of its own — but `bg-default` made it a white card floating in the
1831
+ green fill of a tip. Tint it from the callout instead, and let its `code`
1832
+ child stay a bare token stream: the pill treatment above is scoped to
1833
+ inline code for exactly this reason. */
1834
+ div.prose-callout pre {
1835
+ background-color: color-mix(in oklab, currentcolor 6%, transparent);
1836
+ border-color: color-mix(in oklab, currentcolor 18%, transparent);
1837
+ }
1838
+
1839
+ /* Package-manager tabs. Five full-colour brand marks (npm red, yarn blue, pnpm
1840
+ yellow, bun pink) against a near-monochrome page are a wash, not a pop: the
1841
+ strip out-shouted the command it labels. Desaturate every tab until it is the
1842
+ selected one, which is also the one piece of state the strip has to show. */
1843
+ .prose-code-tab .prose-code-tab-icon {
1844
+ filter: grayscale(1);
1845
+ opacity: 0.6;
1846
+ transition:
1847
+ filter 150ms var(--ease-standard, ease),
1848
+ opacity 150ms var(--ease-standard, ease);
1849
+ }
1850
+ .prose-code-tab[data-state='active'] .prose-code-tab-icon {
1851
+ filter: none;
1852
+ opacity: 1;
1853
+ }
1854
+
1855
+ /* API reference property entries. Every module's API page writes a property as
1856
+ `## `name: Type`{lang="ts"}` followed by a one-item `- Default: x` list. The
1857
+ pill read as a stray chip and the list as a stray bullet, with nothing
1858
+ separating one property from the next. Render the heading as a real entry
1859
+ header over a hairline, and the metadata list as a quiet row. */
1860
+ h2.prose-h2:has(> code:only-child),
1861
+ h3.prose-h3:has(> code:only-child) {
1862
+ margin-top: 2.5rem;
1863
+ margin-bottom: 0.5rem;
1864
+ padding-top: 2rem;
1865
+ border-top: 1px solid var(--ui-border);
1866
+ }
1867
+ h2.prose-h2:first-child:has(> code:only-child),
1868
+ h3.prose-h3:first-child:has(> code:only-child) {
1869
+ margin-top: 0;
1870
+ padding-top: 0;
1871
+ border-top: 0;
1872
+ }
1873
+ h2.prose-h2 > code:only-child,
1874
+ h3.prose-h3 > code:only-child {
1875
+ display: inline;
1876
+ padding: 0;
1877
+ border: 0;
1878
+ background-color: transparent;
1879
+ color: var(--ui-text-highlighted);
1880
+ font-size: inherit;
1881
+ font-weight: inherit;
1882
+ }
1883
+ h2.prose-h2:has(> code:only-child) + ul.prose-ul,
1884
+ h3.prose-h3:has(> code:only-child) + ul.prose-ul {
1885
+ margin-block: 0;
1886
+ padding-inline-start: 0;
1887
+ list-style: none;
1888
+ font-size: 0.875rem;
1889
+ color: var(--ui-text-muted);
1890
+ }
1891
+ h2.prose-h2:has(> code:only-child) + ul.prose-ul > li,
1892
+ h3.prose-h3:has(> code:only-child) + ul.prose-ul > li {
1893
+ margin-block: 0;
1894
+ padding-inline-start: 0;
1895
+ line-height: 1.5rem;
1896
+ }