diff --git a/.agent/workflows/high-contrast-implementation.md b/.agent/workflows/high-contrast-implementation.md new file mode 100644 index 0000000000..d862d282a0 --- /dev/null +++ b/.agent/workflows/high-contrast-implementation.md @@ -0,0 +1,44 @@ +--- +description: Implementation plan for High Contrast Mode feature (Issue #4853) +--- + +# High Contrast Mode Implementation Plan + +## Overview +This implementation adds high contrast versions of both light and dark modes to Openverse, following accessibility best practices (WCAG 1.4.6 Enhanced Contrast - 7:1 ratio for normal text). + +## Components to Create/Modify + +### 1. CSS Variables (`src/styles/tailwind.css`) +- Add high contrast color palettes for both light and dark modes +- Add `@media (prefers-contrast: more)` media queries +- Add `.high-contrast-light-mode` and `.high-contrast-dark-mode` class selectors + +### 2. Type Definitions (`stores/ui.ts`) +- Add `ContrastMode` type: `"normal" | "high" | "system"` +- Add `contrastMode` to `UiState` +- Add `setContrastMode()` action +- Update cookie handling + +### 3. Composable (`composables/use-high-contrast.ts`) +- Create new composable similar to `use-dark-mode.ts` +- Detect OS preference using `prefers-contrast` media query +- Return effective contrast mode and CSS class + +### 4. Feature Flag (`feat/feature-flags.json`) +- Add `high_contrast_ui_toggle` feature flag + +### 5. UI Component (`components/VContrastSelect/VContrastSelect.vue`) +- Create dropdown similar to `VThemeSelect` +- Options: Normal, High Contrast, System + +### 6. Integration +- Update `VFooter` to include contrast selector +- Update `app.vue` to apply contrast CSS class to body + +### 7. Translations +- Add i18n keys for contrast mode labels + +### 8. Tests +- Unit tests for composable +- Unit tests for store actions diff --git a/frontend/feat/feature-flags.json b/frontend/feat/feature-flags.json index 296616d1d6..3ad2fe2dbc 100644 --- a/frontend/feat/feature-flags.json +++ b/frontend/feat/feature-flags.json @@ -39,12 +39,23 @@ "defaultState": "on", "description": "Display the UI toggle to change the site color theme and respect system preferences.", "storage": "cookie" + }, + "high_contrast_ui_toggle": { + "status": { + "staging": "switchable", + "production": "switchable" + }, + "defaultState": "on", + "description": "Display the UI toggle to enable high contrast mode and respect OS prefers-contrast preferences.", + "storage": "cookie" } }, "groups": [ { "title": "analytics", - "features": ["analytics"] + "features": [ + "analytics" + ] } ] -} +} \ No newline at end of file diff --git a/frontend/i18n/data/en.json5 b/frontend/i18n/data/en.json5 index 5f092e47be..015d175c56 100644 --- a/frontend/i18n/data/en.json5 +++ b/frontend/i18n/data/en.json5 @@ -760,6 +760,10 @@ _{link}_ part of "The translation for English locale is incomplete. Help us get "theme.choices.dark": "Dark", "theme.choices.light": "Light", "theme.choices.system": "System", + "contrast.contrast": "Contrast", + "contrast.choices.normal": "Normal", + "contrast.choices.high": "High", + "contrast.choices.system": "System", "recentSearches.heading": "Recent searches", "recentSearches.clear.text": "Clear", "recentSearches.clear.label": "Clear recent searches", diff --git a/frontend/shared/types/cookies.ts b/frontend/shared/types/cookies.ts index 06ec392ac2..31769cc96d 100644 --- a/frontend/shared/types/cookies.ts +++ b/frontend/shared/types/cookies.ts @@ -15,6 +15,7 @@ export const persistentCookieOptions = { export const defaultPersistientCookieState: OpenverseCookieState = { ui: { colorMode: "system", + contrastMode: "system", }, } diff --git a/frontend/src/app.vue b/frontend/src/app.vue index 169fb113aa..74aef19d9c 100644 --- a/frontend/src/app.vue +++ b/frontend/src/app.vue @@ -13,6 +13,7 @@ import { useUiStore } from "~/stores/ui" import { useFeatureFlagStore } from "~/stores/feature-flag" import { useLayout } from "~/composables/use-layout" import { useDarkMode } from "~/composables/use-dark-mode" +import { useHighContrast } from "~/composables/use-high-contrast" import VSkipToContentButton from "~/components/VSkipToContentButton.vue" @@ -22,6 +23,12 @@ const featureFlagStore = useFeatureFlagStore() const uiStore = useUiStore() const darkMode = useDarkMode() +const highContrast = useHighContrast() + +// Combined CSS classes for body: dark mode class + high contrast class +const bodyClasses = computed(() => { + return [darkMode.cssClass.value, highContrast.cssClass.value].filter(Boolean).join(' ') +}) /* UI store */ const isDesktopLayout = computed(() => uiStore.isDesktopLayout) @@ -63,7 +70,7 @@ const meta = computed(() => { useHead({ htmlAttrs: htmlI18nProps, - bodyAttrs: { class: darkMode.cssClass, style: headerHeight }, + bodyAttrs: { class: bodyClasses, style: headerHeight }, title: "Openly Licensed Images, Audio and More | Openverse", meta, link, diff --git a/frontend/src/components/VContrastSelect/VContrastSelect.vue b/frontend/src/components/VContrastSelect/VContrastSelect.vue new file mode 100644 index 0000000000..f7595d5b84 --- /dev/null +++ b/frontend/src/components/VContrastSelect/VContrastSelect.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/components/VFooter/VFooter.vue b/frontend/src/components/VFooter/VFooter.vue index a4c1488fa0..5177decc6b 100644 --- a/frontend/src/components/VFooter/VFooter.vue +++ b/frontend/src/components/VFooter/VFooter.vue @@ -15,6 +15,7 @@ import VLink from "~/components/VLink.vue" import VBrand from "~/components/VBrand/VBrand.vue" import VLanguageSelect from "~/components/VLanguageSelect/VLanguageSelect.vue" import VThemeSelect from "~/components/VThemeSelect/VThemeSelect.vue" +import VContrastSelect from "~/components/VContrastSelect/VContrastSelect.vue" import VPageLinks from "~/components/VHeader/VPageLinks.vue" import VWordPressLink from "~/components/VHeader/VWordPressLink.vue" @@ -96,6 +97,7 @@ const linkColumnHeight = computed(() => ({ class="language max-w-full border-secondary" /> + diff --git a/frontend/src/composables/use-high-contrast.ts b/frontend/src/composables/use-high-contrast.ts new file mode 100644 index 0000000000..76c2076ffa --- /dev/null +++ b/frontend/src/composables/use-high-contrast.ts @@ -0,0 +1,124 @@ +import { computed, ref, onMounted, watchEffect } from "#imports" + +import { useFeatureFlagStore } from "~/stores/feature-flag" +import { useUiStore } from "~/stores/ui" +import { useDarkMode } from "~/composables/use-dark-mode" + +export const HIGH_CONTRAST_LIGHT_MODE_CLASS = "high-contrast-light-mode" +export const HIGH_CONTRAST_DARK_MODE_CLASS = "high-contrast-dark-mode" + +/** + * Determines the high contrast mode setting based on user preference or OS setting. + * + * When the "high_contrast_ui_toggle" flag is enabled, the site will respect + * the user's system preference for high contrast by default (using prefers-contrast media query). + * + * The high contrast mode works in combination with the color mode (light/dark) to provide + * four possible visual themes: + * - Light mode (normal contrast) + * - Light mode (high contrast) + * - Dark mode (normal contrast) + * - Dark mode (high contrast) + */ +export function useHighContrast() { + const uiStore = useUiStore() + const featureFlagStore = useFeatureFlagStore() + const { effectiveColorMode } = useDarkMode() + + const highContrastToggleable = computed(() => + featureFlagStore.isOn("high_contrast_ui_toggle") + ) + + /** + * Detect OS-level preference for high contrast using the prefers-contrast media query. + * Returns true if the user has requested more contrast at the OS level. + */ + const osHighContrast = ref(false) + + onMounted(() => { + // Check if the browser supports the prefers-contrast media query + const mediaQuery = window.matchMedia("(prefers-contrast: more)") + osHighContrast.value = mediaQuery.matches + + // Listen for changes to the OS high contrast preference + const handleChange = (e: MediaQueryListEvent) => { + osHighContrast.value = e.matches + } + mediaQuery.addEventListener("change", handleChange) + + // Note: cleanup is handled by Vue's lifecycle + }) + + /** + * The contrast mode setting for the app. + * + * This can be one of "normal", "high", or "system". If the toggle + * feature is disabled, we default to "normal". + */ + const contrastMode = computed(() => { + if (highContrastToggleable.value) { + return uiStore.contrastMode + } + return "normal" + }) + + /** + * The effective contrast mode of the app. + * + * This can be one of "normal" or "high". This is a combination of the + * toggle feature flag, the user's preference at the app and OS levels, + * and the default value of "normal". + */ + const effectiveContrastMode = computed(() => { + if (!highContrastToggleable.value) { + return "normal" + } + if (contrastMode.value === "system") { + return osHighContrast.value ? "high" : "normal" + } + return contrastMode.value + }) + + /** + * Whether high contrast mode is currently active (either explicitly set or via OS preference). + */ + const isHighContrast = computed(() => effectiveContrastMode.value === "high") + + /** + * The CSS class to apply to the body element based on the contrast and color mode. + * Returns a class that combines both the color mode and contrast mode. + * + * - high-contrast-light-mode: High contrast + light theme + * - high-contrast-dark-mode: High contrast + dark theme + * - Empty string: Normal contrast (handled by color mode classes) + */ + const cssClass = computed(() => { + if (effectiveContrastMode.value !== "high") { + return "" + } + + // When high contrast is enabled, we need to combine it with the current color mode + return effectiveColorMode.value === "dark" + ? HIGH_CONTRAST_DARK_MODE_CLASS + : HIGH_CONTRAST_LIGHT_MODE_CLASS + }) + + /** + * The server does not have access to media queries, so the `system` contrast mode + * defaults to "normal" on the server. + */ + const serverContrastMode = computed(() => { + return !highContrastToggleable.value || contrastMode.value === "system" + ? "normal" + : contrastMode.value + }) + + return { + contrastMode, + osHighContrast, + effectiveContrastMode, + isHighContrast, + serverContrastMode, + cssClass, + } +} diff --git a/frontend/src/error.vue b/frontend/src/error.vue index c15eca5c3b..860c583096 100644 --- a/frontend/src/error.vue +++ b/frontend/src/error.vue @@ -6,6 +6,7 @@ import { meta as commonMeta } from "#shared/constants/meta" import { favicons } from "#shared/constants/favicons" import { useUiStore } from "~/stores/ui" import { useDarkMode } from "~/composables/use-dark-mode" +import { useHighContrast } from "~/composables/use-high-contrast" import { useLayout } from "~/composables/use-layout" import VFourOhFour from "~/components/VFourOhFour.vue" @@ -30,6 +31,12 @@ onMounted(() => { }) const darkMode = useDarkMode() +const highContrast = useHighContrast() + +// Combined CSS classes for body: dark mode class + high contrast class +const bodyClasses = computed(() => { + return [darkMode.cssClass.value, highContrast.cssClass.value].filter(Boolean).join(' ') +}) const head = useLocaleHead({ dir: true, key: "id", seo: true }) const htmlI18nProps = computed(() => ({ @@ -64,7 +71,7 @@ const meta = computed(() => { useHead({ htmlAttrs: htmlI18nProps, - bodyAttrs: { class: darkMode.cssClass, style: headerHeight }, + bodyAttrs: { class: bodyClasses, style: headerHeight }, title: "Openly Licensed Images, Audio and More | Openverse", meta, link, diff --git a/frontend/src/stores/ui.ts b/frontend/src/stores/ui.ts index fe3bdec05f..f98f74946a 100644 --- a/frontend/src/stores/ui.ts +++ b/frontend/src/stores/ui.ts @@ -17,6 +17,7 @@ const desktopBreakpoints: RealBreakpoint[] = ["2xl", "xl", "lg"] export type SnackbarState = "not_shown" | "visible" | "dismissed" export type ColorMode = "dark" | "light" | "system" +export type ContrastMode = "normal" | "high" | "system" export function isColorMode(value: undefined | string): value is ColorMode { return ( @@ -24,6 +25,12 @@ export function isColorMode(value: undefined | string): value is ColorMode { ) } +export function isContrastMode(value: undefined | string): value is ContrastMode { + return ( + typeof value === "string" && ["normal", "high", "system"].includes(value) + ) +} + export interface UiState { /** * whether to show the instructions snackbar. @@ -65,6 +72,8 @@ export interface UiState { colorMode: ColorMode /** whether the user has seen the dark mode toggle */ isDarkModeSeen: boolean + /* The user-chosen contrast mode of the site. */ + contrastMode: ContrastMode } export const breakpoints = Object.keys(ALL_SCREEN_SIZES) @@ -81,6 +90,7 @@ export const defaultUiState: UiState = { headerHeight: 80, colorMode: "system", isDarkModeSeen: false, + contrastMode: "system", } export const useUiStore = defineStore("ui", { @@ -95,6 +105,7 @@ export const useUiStore = defineStore("ui", { dismissedBanners: Array.from(this.dismissedBanners), colorMode: state.colorMode, isDarkModeSeen: state.isDarkModeSeen, + contrastMode: state.contrastMode, } }, areInstructionsVisible(state): boolean { @@ -197,6 +208,10 @@ export const useUiStore = defineStore("ui", { this.setIsDarkModeSeen(cookies.isDarkModeSeen, false) } + if (isContrastMode(cookies.contrastMode)) { + this.setContrastMode(cookies.contrastMode, false) + } + this.writeToCookie() }, /** @@ -318,5 +333,19 @@ export const useUiStore = defineStore("ui", { setHeaderHeight(height: number) { this.headerHeight = Math.max(height, 80) }, + + /** + * Update the user's preferred contrast mode. + * + * @param contrastMode - the user's preferred contrast mode. + * @param saveToCookie - whether to save the new setting in the cookie. + */ + setContrastMode(contrastMode: ContrastMode, saveToCookie = true) { + this.contrastMode = contrastMode + + if (saveToCookie) { + this.writeToCookie() + } + }, }, }) diff --git a/frontend/src/styles/tailwind.css b/frontend/src/styles/tailwind.css index 6610194a0b..d04fec3082 100644 --- a/frontend/src/styles/tailwind.css +++ b/frontend/src/styles/tailwind.css @@ -389,6 +389,256 @@ --color-skeleton-secondary: var(--color-gray-10); } + /* High Contrast Light Mode - OS preference */ + @media (prefers-color-scheme: light) and (prefers-contrast: more) { + :root { + /* Backgrounds - Higher contrast with pure white/black */ + --color-bg: var(--color-white); + --color-bg-surface: var(--color-white); + --color-bg-overlay: var(--color-white); + --color-bg-primary: var(--color-pink-10); + --color-bg-primary-hover: var(--color-pink-11); + --color-bg-secondary: var(--color-gray-1); + --color-bg-secondary-hover: var(--color-black); + --color-bg-tertiary: var(--color-black); + --color-bg-tertiary-hover: var(--color-gray-12); + --color-bg-transparent-hover: var(--color-gray-12-20); + --color-bg-complementary: var(--color-yellow-3); + --color-bg-warning: var(--color-warning-3); + --color-bg-info: var(--color-info-3); + --color-bg-success: var(--color-success-3); + --color-bg-error: var(--color-error-3); + --color-bg-disabled: var(--color-gray-6); + --color-bg-blur: var(--color-white); + + /* Borders - Stronger, more visible borders */ + --color-border: var(--color-black); + --color-border-hover: var(--color-black); + --color-border-secondary: var(--color-gray-12-40); + --color-border-secondary-hover: var(--color-black); + --color-border-tertiary: var(--color-black); + --color-border-transparent-hover: var(--color-gray-12-40); + --color-border-overlay: var(--color-black); + --color-border-focus: var(--color-pink-10); + --color-border-bg-ring: var(--color-white); + --color-border-disabled: var(--color-gray-7); + --color-focus-ring: var(--color-pink-10); + + /* Text - Maximum contrast with pure black */ + --color-text: var(--color-black); + --color-text-secondary: var(--color-gray-11); + --color-text-disabled: var(--color-gray-7); + --color-text-link: var(--color-pink-10); + --color-text-over-dark: var(--color-white); + --color-text-over-negative: var(--color-white); + --color-text-secondary-over-dark: var(--color-gray-3); + + /* Icons */ + --color-icon-warning: var(--color-warning-10); + --color-icon-info: var(--color-info-10); + --color-icon-success: var(--color-success-10); + --color-icon-error: var(--color-error-10); + + /* Waveform */ + --color-wave-active: var(--color-yellow-10); + --color-wave-inactive: var(--color-gray-12-40); + + /* Misc */ + --color-modal-layer: var(--color-black-72); + --color-new-highlight: var(--color-pink-8); + --color-gray-new-highlight: var(--color-gray-12-40); + --color-skeleton-base: var(--color-gray-3); + --color-skeleton-secondary: var(--color-gray-4); + } + } + + /* High Contrast Dark Mode - OS preference */ + @media (prefers-color-scheme: dark) and (prefers-contrast: more) { + :root { + /* Backgrounds - Higher contrast with pure black */ + --color-bg: var(--color-black); + --color-bg-surface: var(--color-gray-13); + --color-bg-overlay: var(--color-gray-12); + --color-bg-primary: var(--color-yellow-3); + --color-bg-primary-hover: var(--color-yellow-2); + --color-bg-secondary: var(--color-gray-12); + --color-bg-secondary-hover: var(--color-white); + --color-bg-tertiary: var(--color-white); + --color-bg-tertiary-hover: var(--color-gray-1); + --color-bg-transparent-hover: var(--color-gray-1-20); + --color-bg-complementary: var(--color-pink-8); + --color-bg-warning: var(--color-warning-12); + --color-bg-info: var(--color-info-12); + --color-bg-success: var(--color-success-12); + --color-bg-error: var(--color-error-12); + --color-bg-disabled: var(--color-gray-9); + --color-bg-blur: var(--color-black); + + /* Borders - Stronger, more visible borders */ + --color-border: var(--color-white); + --color-border-hover: var(--color-white); + --color-border-secondary: var(--color-gray-1-40); + --color-border-secondary-hover: var(--color-white); + --color-border-tertiary: var(--color-white); + --color-border-transparent-hover: var(--color-gray-1-40); + --color-border-overlay: var(--color-white); + --color-border-focus: var(--color-yellow-3); + --color-border-bg-ring: var(--color-black); + --color-border-disabled: var(--color-gray-7); + --color-focus-ring: var(--color-yellow-3); + + /* Text - Maximum contrast with pure white */ + --color-text: var(--color-white); + --color-text-secondary: var(--color-gray-2); + --color-text-disabled: var(--color-gray-6); + --color-text-link: var(--color-yellow-3); + --color-text-over-dark: var(--color-black); + --color-text-over-negative: var(--color-black); + --color-text-secondary-over-dark: var(--color-gray-10); + + /* Icons */ + --color-icon-warning: var(--color-warning-4); + --color-icon-info: var(--color-info-4); + --color-icon-success: var(--color-success-4); + --color-icon-error: var(--color-error-4); + + /* Waveform */ + --color-wave-active: var(--color-pink-3); + --color-wave-inactive: var(--color-gray-1-40); + + /* Misc */ + --color-modal-layer: var(--color-black-72); + --color-new-highlight: var(--color-yellow-2); + --color-gray-new-highlight: var(--color-gray-1-40); + --color-skeleton-base: var(--color-gray-12); + --color-skeleton-secondary: var(--color-gray-11); + } + } + + /* High Contrast Light Mode - Class-based override */ + :is(.high-contrast-light-mode), + :is(.high-contrast-light-mode *) { + /* Backgrounds - Higher contrast with pure white/black */ + --color-bg: var(--color-white); + --color-bg-surface: var(--color-white); + --color-bg-overlay: var(--color-white); + --color-bg-primary: var(--color-pink-10); + --color-bg-primary-hover: var(--color-pink-11); + --color-bg-secondary: var(--color-gray-1); + --color-bg-secondary-hover: var(--color-black); + --color-bg-tertiary: var(--color-black); + --color-bg-tertiary-hover: var(--color-gray-12); + --color-bg-transparent-hover: var(--color-gray-12-20); + --color-bg-complementary: var(--color-yellow-3); + --color-bg-warning: var(--color-warning-3); + --color-bg-info: var(--color-info-3); + --color-bg-success: var(--color-success-3); + --color-bg-error: var(--color-error-3); + --color-bg-disabled: var(--color-gray-6); + --color-bg-blur: var(--color-white); + + /* Borders - Stronger, more visible borders */ + --color-border: var(--color-black); + --color-border-hover: var(--color-black); + --color-border-secondary: var(--color-gray-12-40); + --color-border-secondary-hover: var(--color-black); + --color-border-tertiary: var(--color-black); + --color-border-transparent-hover: var(--color-gray-12-40); + --color-border-overlay: var(--color-black); + --color-border-focus: var(--color-pink-10); + --color-border-bg-ring: var(--color-white); + --color-border-disabled: var(--color-gray-7); + --color-focus-ring: var(--color-pink-10); + + /* Text - Maximum contrast with pure black */ + --color-text: var(--color-black); + --color-text-secondary: var(--color-gray-11); + --color-text-disabled: var(--color-gray-7); + --color-text-link: var(--color-pink-10); + --color-text-over-dark: var(--color-white); + --color-text-over-negative: var(--color-white); + --color-text-secondary-over-dark: var(--color-gray-3); + + /* Icons */ + --color-icon-warning: var(--color-warning-10); + --color-icon-info: var(--color-info-10); + --color-icon-success: var(--color-success-10); + --color-icon-error: var(--color-error-10); + + /* Waveform */ + --color-wave-active: var(--color-yellow-10); + --color-wave-inactive: var(--color-gray-12-40); + + /* Misc */ + --color-modal-layer: var(--color-black-72); + --color-new-highlight: var(--color-pink-8); + --color-gray-new-highlight: var(--color-gray-12-40); + --color-skeleton-base: var(--color-gray-3); + --color-skeleton-secondary: var(--color-gray-4); + } + + /* High Contrast Dark Mode - Class-based override */ + :is(.high-contrast-dark-mode *), + :is(.high-contrast-dark-mode) { + /* Backgrounds - Higher contrast with pure black */ + --color-bg: var(--color-black); + --color-bg-surface: var(--color-gray-13); + --color-bg-overlay: var(--color-gray-12); + --color-bg-primary: var(--color-yellow-3); + --color-bg-primary-hover: var(--color-yellow-2); + --color-bg-secondary: var(--color-gray-12); + --color-bg-secondary-hover: var(--color-white); + --color-bg-tertiary: var(--color-white); + --color-bg-tertiary-hover: var(--color-gray-1); + --color-bg-transparent-hover: var(--color-gray-1-20); + --color-bg-complementary: var(--color-pink-8); + --color-bg-warning: var(--color-warning-12); + --color-bg-info: var(--color-info-12); + --color-bg-success: var(--color-success-12); + --color-bg-error: var(--color-error-12); + --color-bg-disabled: var(--color-gray-9); + --color-bg-blur: var(--color-black); + + /* Borders - Stronger, more visible borders */ + --color-border: var(--color-white); + --color-border-hover: var(--color-white); + --color-border-secondary: var(--color-gray-1-40); + --color-border-secondary-hover: var(--color-white); + --color-border-tertiary: var(--color-white); + --color-border-transparent-hover: var(--color-gray-1-40); + --color-border-overlay: var(--color-white); + --color-border-focus: var(--color-yellow-3); + --color-border-bg-ring: var(--color-black); + --color-border-disabled: var(--color-gray-7); + --color-focus-ring: var(--color-yellow-3); + + /* Text - Maximum contrast with pure white */ + --color-text: var(--color-white); + --color-text-secondary: var(--color-gray-2); + --color-text-disabled: var(--color-gray-6); + --color-text-link: var(--color-yellow-3); + --color-text-over-dark: var(--color-black); + --color-text-over-negative: var(--color-black); + --color-text-secondary-over-dark: var(--color-gray-10); + + /* Icons */ + --color-icon-warning: var(--color-warning-4); + --color-icon-info: var(--color-info-4); + --color-icon-success: var(--color-success-4); + --color-icon-error: var(--color-error-4); + + /* Waveform */ + --color-wave-active: var(--color-pink-3); + --color-wave-inactive: var(--color-gray-1-40); + + /* Misc */ + --color-modal-layer: var(--color-black-72); + --color-new-highlight: var(--color-yellow-2); + --color-gray-new-highlight: var(--color-gray-1-40); + --color-skeleton-base: var(--color-gray-12); + --color-skeleton-secondary: var(--color-gray-11); + } + body { @apply m-0 font-normal leading-loose text-default; -webkit-font-smoothing: antialiased;