@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.
- package/README.md +181 -2
- package/app/app.config.ts +592 -0
- package/app/component-attributes.d.ts +8 -0
- package/app/components/README.md +38 -0
- package/app/components/brand/UiBrandMark.vue +63 -0
- package/app/components/brand/UiModuleIcon.vue +45 -0
- package/app/components/brand/UiProLettering.vue +26 -0
- package/app/components/brand/UiRangeTerrain.vue +311 -0
- package/app/components/brand/range-glyphs.ts +18 -0
- package/app/components/container/BrowserTab.vue +145 -0
- package/app/components/container/IntegrationRow.vue +120 -0
- package/app/components/container/KnowDoRails.vue +40 -0
- package/app/components/container/RailPanel.vue +93 -0
- package/app/components/container/UiAlert.vue +120 -0
- package/app/components/container/UiAppShell.vue +165 -0
- package/app/components/container/UiCard.vue +204 -0
- package/app/components/container/UiDisclosure.vue +43 -0
- package/app/components/container/UiEmptyState.vue +102 -0
- package/app/components/container/UiErrorState.vue +61 -0
- package/app/components/container/UiLoadingState.vue +20 -0
- package/app/components/container/UiPageHeader.vue +95 -0
- package/app/components/container/UiPopover.vue +102 -0
- package/app/components/container/UiSampleDataOverlay.vue +47 -0
- package/app/components/container/UiSectionHeader.vue +83 -0
- package/app/components/container/UiSummaryStrip.vue +40 -0
- package/app/components/container/UiToast.vue +230 -0
- package/app/components/container/UiToaster.vue +271 -0
- package/app/components/container/UiWidgetState.vue +143 -0
- package/app/components/container/ui-nav-drawer.ts +48 -0
- package/app/components/data/ActionRail.vue +152 -0
- package/app/components/data/FacetStrip.vue +93 -0
- package/app/components/data/UiBarCell.vue +37 -0
- package/app/components/data/UiBarChart.vue +281 -0
- package/app/components/data/UiChartAnnotationControls.vue +74 -0
- package/app/components/data/UiChartAnnotations.vue +163 -0
- package/app/components/data/UiChartBrushOverlay.vue +96 -0
- package/app/components/data/UiChartDataReveal.vue +81 -0
- package/app/components/data/UiChartFrame.vue +123 -0
- package/app/components/data/UiChartTooltip.vue +81 -0
- package/app/components/data/UiCoverageNote.vue +90 -0
- package/app/components/data/UiCwvSparkline.vue +188 -0
- package/app/components/data/UiDataList.vue +276 -0
- package/app/components/data/UiDataTableSection.vue +233 -0
- package/app/components/data/UiDistributionBars.vue +164 -0
- package/app/components/data/UiDonutChart.vue +132 -0
- package/app/components/data/UiDoorwayColumn.vue +108 -0
- package/app/components/data/UiDoorwayGrid.vue +58 -0
- package/app/components/data/UiDoorwayRow.vue +83 -0
- package/app/components/data/UiEntitySummary.vue +261 -0
- package/app/components/data/UiFactsGrid.vue +107 -0
- package/app/components/data/UiImpactMeter.vue +33 -0
- package/app/components/data/UiIssueRow.vue +149 -0
- package/app/components/data/UiLineChart.vue +431 -0
- package/app/components/data/UiMetricBarPopover.vue +121 -0
- package/app/components/data/UiMetricLabel.vue +30 -0
- package/app/components/data/UiMetricStat.vue +73 -0
- package/app/components/data/UiMetricToggle.vue +156 -0
- package/app/components/data/UiMetricsRow.vue +64 -0
- package/app/components/data/UiPositionMetric.vue +53 -0
- package/app/components/data/UiRelatedFixes.vue +113 -0
- package/app/components/data/UiRichText.vue +325 -0
- package/app/components/data/UiScatterPlot.vue +144 -0
- package/app/components/data/UiSeverityMarker.vue +30 -0
- package/app/components/data/UiSparkColumns.vue +160 -0
- package/app/components/data/UiSparkline.vue +681 -0
- package/app/components/data/UiStat.vue +445 -0
- package/app/components/data/UiStats.vue +76 -0
- package/app/components/data/UiTable.vue +517 -0
- package/app/components/data/UiTableExport.vue +40 -0
- package/app/components/data/UiTableFrame.vue +149 -0
- package/app/components/data/UiTablePagination.vue +37 -0
- package/app/components/data/UiTableShell.vue +48 -0
- package/app/components/data/UiTableTd.vue +39 -0
- package/app/components/data/UiTableTh.vue +61 -0
- package/app/components/data/UiTopEntityStackChart.vue +393 -0
- package/app/components/data/cells/UiTableDash.vue +3 -0
- package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
- package/app/components/data/cells/UiTableMetricCell.vue +72 -0
- package/app/components/data/cells/UiTablePathCell.vue +76 -0
- package/app/components/data/cells/UiTableScoreTile.vue +49 -0
- package/app/components/data/cells/UiTableTrendCell.vue +41 -0
- package/app/components/element/UiAtmosphere.vue +64 -0
- package/app/components/element/UiAuthHeading.vue +24 -0
- package/app/components/element/UiAuthProviders.vue +46 -0
- package/app/components/element/UiBrandIcon.vue +39 -0
- package/app/components/element/UiButton.vue +549 -0
- package/app/components/element/UiCelebration.vue +65 -0
- package/app/components/element/UiChannelChips.vue +62 -0
- package/app/components/element/UiChatQuickLink.vue +319 -0
- package/app/components/element/UiChip.vue +101 -0
- package/app/components/element/UiConfirmDialog.vue +84 -0
- package/app/components/element/UiCopyButton.vue +90 -0
- package/app/components/element/UiDitherAvatar.vue +177 -0
- package/app/components/element/UiDotLabel.vue +19 -0
- package/app/components/element/UiEjectMenu.vue +181 -0
- package/app/components/element/UiEjectOverlay.vue +29 -0
- package/app/components/element/UiFavicon.vue +235 -0
- package/app/components/element/UiFaviconStack.vue +84 -0
- package/app/components/element/UiFilterMenu.vue +324 -0
- package/app/components/element/UiFocusedSurface.vue +129 -0
- package/app/components/element/UiHealthDot.vue +32 -0
- package/app/components/element/UiHelpLabel.vue +28 -0
- package/app/components/element/UiIcon.vue +22 -0
- package/app/components/element/UiInput.vue +35 -0
- package/app/components/element/UiLogoPlausible.vue +12 -0
- package/app/components/element/UiLogoPro.vue +25 -0
- package/app/components/element/UiLogoUmami.vue +12 -0
- package/app/components/element/UiNavIcon.vue +107 -0
- package/app/components/element/UiNavList.vue +156 -0
- package/app/components/element/UiOnboardingAnswers.vue +127 -0
- package/app/components/element/UiOverflowCount.vue +31 -0
- package/app/components/element/UiProBadge.vue +19 -0
- package/app/components/element/UiProgressCircle.vue +79 -0
- package/app/components/element/UiProgressPercent.vue +35 -0
- package/app/components/element/UiRelativeTime.vue +58 -0
- package/app/components/element/UiScrollFade.vue +16 -0
- package/app/components/element/UiSelect.vue +68 -0
- package/app/components/element/UiSeverityDot.vue +27 -0
- package/app/components/element/UiSiteFavicon.vue +24 -0
- package/app/components/element/UiSkeleton.vue +148 -0
- package/app/components/element/UiSlotText.vue +65 -0
- package/app/components/element/UiSourceLogos.vue +113 -0
- package/app/components/element/UiSourceRail.vue +71 -0
- package/app/components/element/UiStaticTerminal.vue +107 -0
- package/app/components/element/UiStatusBadge.vue +62 -0
- package/app/components/element/UiSyncDot.vue +46 -0
- package/app/components/element/UiTabs.vue +245 -0
- package/app/components/element/UiTogglePill.vue +86 -0
- package/app/components/element/UiTooltip.vue +390 -0
- package/app/components/element/UiTooltipGrid.vue +457 -0
- package/app/components/element/UiTrend.vue +184 -0
- package/app/components/element/UiTrialBenefits.vue +71 -0
- package/app/components/element/UiUrlLabel.vue +75 -0
- package/app/components/element/UiValenceDot.vue +37 -0
- package/app/components/element/UiWizardModal.vue +284 -0
- package/app/components/element/UiWizardNav.vue +59 -0
- package/app/components/element/UiWizardProgress.vue +78 -0
- package/app/components/element/UiWizardShell.vue +43 -0
- package/app/components/element/eject-menu.ts +359 -0
- package/app/components/element/eject-tracker.ts +34 -0
- package/app/components/element/onboarding-answers.ts +93 -0
- package/app/components/element/onboarding-intents.ts +19 -0
- package/app/composables/dataVizColors.ts +350 -0
- package/app/composables/formatting.ts +252 -0
- package/app/composables/semanticColors.ts +80 -0
- package/app/composables/useActivityOverlay.ts +55 -0
- package/app/composables/useAppShellNav.ts +21 -0
- package/app/composables/useChartBrush.ts +221 -0
- package/app/composables/useChartHover.ts +153 -0
- package/app/composables/useChartTickPlan.ts +186 -0
- package/app/composables/useCloseOnNavigation.ts +7 -0
- package/app/composables/useConfirm.ts +38 -0
- package/app/composables/useEdgeFade.ts +37 -0
- package/app/composables/useEject.ts +69 -0
- package/app/composables/useEntranceMotion.ts +22 -0
- package/app/composables/useFaviconBacking.ts +164 -0
- package/app/composables/useGridCols.ts +39 -0
- package/app/composables/useIssueOverlay.ts +43 -0
- package/app/composables/useOnboardingService.ts +106 -0
- package/app/composables/useOpenedOnce.ts +19 -0
- package/app/composables/useScrollSpy.ts +178 -0
- package/app/composables/useSquircleFallback.ts +21 -0
- package/app/composables/useWizardRoute.ts +123 -0
- package/app/plugins/favicon.ts +15 -0
- package/app/plugins/lazy-motion.ts +22 -0
- package/app/plugins/selection-highlight.client.ts +176 -0
- package/app/shared/cli.ts +10 -0
- package/app/shared/error-state.ts +150 -0
- package/app/shared/icons.ts +1 -0
- package/app/shared/motion.ts +1 -0
- package/app/shared/nav.ts +38 -0
- package/app/shared/navigation.ts +5 -0
- package/app/shared/static-terminal.ts +14 -0
- package/app/shared/table-export.ts +36 -0
- package/app/shared/table.ts +105 -0
- package/app/shared/ui-icons.ts +1 -0
- package/app/shared/view-state.ts +85 -0
- package/app/shared/widget-failure.ts +126 -0
- package/app/utils/app-favicon.ts +64 -0
- package/app/utils/chartAnnotations.ts +326 -0
- package/app/utils/countries.ts +337 -0
- package/app/utils/export-download.ts +100 -0
- package/app/utils/favicon-fallback.ts +12 -0
- package/app/utils/icon-color.ts +105 -0
- package/app/utils/number.ts +74 -0
- package/app/utils/render-link.ts +14 -0
- package/app/utils/slot-text.ts +18 -0
- package/app/utils/squircle.ts +154 -0
- package/app/utils/topEntityStack.ts +197 -0
- package/app/utils/vue.ts +57 -0
- package/build/design-system.ts +69 -0
- package/build/external-links.ts +38 -0
- package/build/fonts.ts +39 -0
- package/build/icon-server-bundle.ts +276 -0
- package/build/runtime/icon-server-bundle.ts +29 -0
- package/css/global.css +1896 -0
- package/nuxt.config.ts +143 -0
- package/package.json +93 -3
- package/public/fonts/README.md +33 -0
- package/public/fonts/fira-code-OFL.txt +93 -0
- package/public/fonts/fira-code-var-latin.woff2 +0 -0
- package/public/fonts/hubot-sans-OFL.txt +93 -0
- package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
- package/server/api/favicon.get.ts +122 -0
- package/server/utils/favicon-source.ts +51 -0
- package/shared/contracts/feedback.ts +42 -0
- package/shared/contracts/index.ts +1 -0
- package/shared/delivery-channels.ts +53 -0
- package/shared/icons/index.ts +143 -0
- package/shared/icons/registry.ts +347 -0
- package/shared/icons/ui-slots.ts +58 -0
- package/shared/markdown.ts +44 -0
- package/shared/motion.ts +120 -0
- package/shared/rangi.ts +60 -0
- package/shared/rich-text.ts +34 -0
- package/shared/ui-icons.ts +6 -0
- package/shared/utils/urls.ts +64 -0
- package/types/index.ts +1 -0
- 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
|
+
}
|