Files
webmail/app/accessibility.css
T

403 lines
17 KiB
CSS

/*
* Settings > Accessibility, the CSS half (lib/accessibility-settings.ts writes
* the data attributes on <html>).
*
* Each "follow the system" option has two parts: a media query that applies
* unless the user overrode it (`:root:not([data-x="..."])`), and an attribute
* rule for an explicit choice. Overrides use `:root[...]`, which outranks the
* `:root` and `.dark` blocks themes compile to (lib/theme-compiler.ts).
*/
/* ---------- Bundled fonts (SIL Open Font License, see NOTICE) ---------- */
@font-face {
font-family: "Atkinson Hyperlegible Next";
font-style: normal;
font-display: swap;
font-weight: 200 800;
src: url("./fonts/atkinson-hyperlegible-next-latin-ext-wght-normal.woff2") format("woff2-variations");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Atkinson Hyperlegible Next";
font-style: normal;
font-display: swap;
font-weight: 200 800;
src: url("./fonts/atkinson-hyperlegible-next-latin-wght-normal.woff2") format("woff2-variations");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Lexend";
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url("./fonts/lexend-latin-ext-wght-normal.woff2") format("woff2-variations");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Lexend";
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url("./fonts/lexend-latin-wght-normal.woff2") format("woff2-variations");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "OpenDyslexic";
font-style: normal;
font-display: swap;
font-weight: 400;
src: url("./fonts/opendyslexic-latin-400-normal.woff2") format("woff2");
}
@font-face {
font-family: "OpenDyslexic";
font-style: normal;
font-display: swap;
font-weight: 700;
src: url("./fonts/opendyslexic-latin-700-normal.woff2") format("woff2");
}
/* The fonts cover Latin script; other scripts fall through to the stack. */
:root[data-app-font="atkinson"] {
--font-sans: "Atkinson Hyperlegible Next", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
}
:root[data-app-font="lexend"] {
--font-sans: "Lexend", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
}
:root[data-app-font="opendyslexic"] {
--font-sans: "OpenDyslexic", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
}
:root[data-app-font] body {
font-family: var(--font-sans);
}
/* ---------- Text spacing (WCAG 1.4.8, 1.4.12) ---------- */
:root[data-line-spacing="relaxed"] { --a11y-line-height: 1.6; }
:root[data-line-spacing="loose"] { --a11y-line-height: 1.9; }
:root[data-line-spacing] body *:not(svg, svg *) {
line-height: var(--a11y-line-height) !important;
}
:root[data-letter-spacing="wide"] { --a11y-letter-spacing: 0.05em; }
:root[data-letter-spacing="wider"] { --a11y-letter-spacing: 0.12em; }
:root[data-letter-spacing] body *:not(svg, svg *) {
letter-spacing: var(--a11y-letter-spacing) !important;
}
:root[data-word-spacing="wide"] body *:not(svg, svg *) {
word-spacing: 0.16em !important;
}
:root[data-paragraph-spacing="wide"] body p {
margin-block-end: 1.5em !important;
}
/* ---------- Links ---------- */
:root[data-underline-links="on"] a[href]:not([role]),
:root[data-underline-links="on"] .a11y-link {
text-decoration-line: underline !important;
text-underline-offset: 0.15em;
}
/* ---------- Focus (WCAG 2.4.7, 2.4.11, 2.4.13) ----------
*
* One outline for every focusable element, drawn as a real outline rather
* than a box-shadow ring: forced-colors mode removes box-shadows, and an
* outline also keeps its width at any zoom. It outranks the utilities that
* used to switch the outline off (outline-none plus a ring).
*
* Containers that only take focus from script (tabindex="-1" on a list, a
* pane, a dialog, <main>, a heading focused after navigation) skip it: the
* focus there is a starting point, not a control. Controls with tabindex="-1"
* keep it, because roving-focus widgets (menu items, options, grid cells,
* tabs, rows) are focused while their tabindex is -1.
*/
:root {
--focus-ring-width: 2px;
--focus-ring-offset: 2px;
--focus-ring-color: var(--color-ring);
}
:root[data-focus-ring="thick"] {
--focus-ring-width: 3px;
}
:root[data-focus-ring="extra"] {
--focus-ring-width: 5px;
--focus-ring-offset: 3px;
}
:focus-visible:not([tabindex="-1"]:not(a[href], button, input, select, textarea, summary, [contenteditable="true"], [role="button"], [role="link"], [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"], [role="gridcell"], [role="row"], [role="treeitem"], [role="tab"], [role="radio"], [role="checkbox"], [role="switch"], [role="slider"], [role="spinbutton"], [role="columnheader"], [role="rowheader"])),
:root[data-focus-always="on"] :focus:not([tabindex="-1"]:not(a[href], button, input, select, textarea, summary, [contenteditable="true"], [role="button"], [role="link"], [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"], [role="gridcell"], [role="row"], [role="treeitem"], [role="tab"], [role="radio"], [role="checkbox"], [role="switch"], [role="slider"], [role="spinbutton"], [role="columnheader"], [role="rowheader"])) {
outline: var(--focus-ring-width) solid var(--focus-ring-color) !important;
outline-offset: var(--focus-ring-offset) !important;
}
/* Inside a scroll container a positive offset is clipped; draw it inside.
`focus-inset` does the same for any control that sits flush against the
edge of a clipping container. */
:is([role="option"], [role="row"], [role="treeitem"], [role="gridcell"], [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="tab"], .focus-inset):focus-visible {
outline-offset: calc(var(--focus-ring-width) * -1) !important;
}
/* The focused element scrolls clear of sticky headers and the bottom bar. */
:root {
scroll-padding-block: 4rem;
}
/* ---------- More contrast (WCAG 1.4.6 as a setting) ---------- */
@media (prefers-contrast: more) {
:root:not([data-contrast="normal"]) {
--color-foreground: #000000;
--color-muted-foreground: #1f2937;
--color-border: #4b5563;
--color-input: #4b5563;
--color-ring: #000000;
--color-sidebar-border: #4b5563;
--color-primary: #1d4ed8;
--color-unread: #1d4ed8;
--color-destructive: #b91c1c;
--color-accent-foreground: #1e3a8a;
--color-selection-foreground: #1e3a8a;
--color-popover-foreground: #000000;
--color-card-foreground: #000000;
--color-sidebar-foreground: #000000;
--focus-ring-color: #000000;
}
.dark:not([data-contrast="normal"]) {
--color-foreground: #ffffff;
--color-muted-foreground: #e5e7eb;
--color-border: #9ca3af;
--color-input: #9ca3af;
--color-ring: #ffffff;
--color-sidebar-border: #9ca3af;
--color-primary: #93c5fd;
--color-primary-foreground: #000000;
--color-unread: #93c5fd;
--color-destructive: #fca5a5;
--color-accent-foreground: #dbeafe;
--color-selection-foreground: #dbeafe;
--color-popover-foreground: #ffffff;
--color-card-foreground: #ffffff;
--color-sidebar-foreground: #ffffff;
--focus-ring-color: #ffffff;
}
:root:not([data-contrast="normal"]) a[href]:not([role]) {
text-decoration-line: underline;
}
}
:root[data-contrast="more"] {
--color-foreground: #000000;
--color-muted-foreground: #1f2937;
--color-border: #4b5563;
--color-input: #4b5563;
--color-ring: #000000;
--color-sidebar-border: #4b5563;
--color-primary: #1d4ed8;
--color-unread: #1d4ed8;
--color-destructive: #b91c1c;
--color-accent-foreground: #1e3a8a;
--color-selection-foreground: #1e3a8a;
--color-popover-foreground: #000000;
--color-card-foreground: #000000;
--color-sidebar-foreground: #000000;
--focus-ring-color: #000000;
}
html.dark[data-contrast="more"] {
--color-foreground: #ffffff;
--color-muted-foreground: #e5e7eb;
--color-border: #9ca3af;
--color-input: #9ca3af;
--color-ring: #ffffff;
--color-sidebar-border: #9ca3af;
--color-primary: #93c5fd;
--color-primary-foreground: #000000;
--color-unread: #93c5fd;
--color-destructive: #fca5a5;
--color-accent-foreground: #dbeafe;
--color-selection-foreground: #dbeafe;
--color-popover-foreground: #ffffff;
--color-card-foreground: #ffffff;
--color-sidebar-foreground: #ffffff;
--focus-ring-color: #ffffff;
}
:root[data-contrast="more"] a[href]:not([role]) {
text-decoration-line: underline;
}
/* ---------- Reduced transparency ---------- */
@media (prefers-reduced-transparency: reduce) {
:root:not([data-transparency="full"]) * {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
:root:not([data-transparency="full"]) :is([class*="bg-background/"], [class*="bg-card/"], [class*="bg-popover/"], [class*="bg-sidebar/"], [class*="bg-muted/"]) {
background-color: var(--color-background) !important;
}
}
:root[data-transparency="reduce"] * {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
:root[data-transparency="reduce"] :is([class*="bg-background/"], [class*="bg-card/"], [class*="bg-popover/"], [class*="bg-sidebar/"], [class*="bg-muted/"]) {
background-color: var(--color-background) !important;
}
/* ---------- Large targets (WCAG 2.5.5) ----------
*
* On a touch screen the components already size themselves (TOUCH_HIT_AREA
* and the pointer-coarse: utilities, through the large-targets variant in
* theme-inline.css). With "On" and a mouse, the rules below do the same
* for controls that carry no such class: buttons, menu items, options,
* tabs, tree items and list rows grow to 44px, switches keep their drawn
* size and get a 44px hit area around them, checkboxes and radios grow to
* 24px and take the rest from their label.
*/
:root[data-large-targets="on"] {
--touch-row-min-h: 44px;
}
:root[data-large-targets="on"] :is(button, [role="button"], [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"], [role="tab"], [role="treeitem"], [role="row"], [role="checkbox"]:not(input), summary, select):not([role="switch"], [role="separator"], [aria-hidden="true"], [disabled]) {
min-height: 44px;
}
:root[data-large-targets="on"] :is(button, [role="button"], [role="checkbox"]:not(input)):not([role="switch"], [role="separator"], [aria-hidden="true"]) {
min-width: 44px;
}
:root[data-large-targets="on"] [role="switch"] {
position: relative;
}
:root[data-large-targets="on"] [role="switch"]::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: max(100%, 44px);
height: max(100%, 44px);
transform: translate(-50%, -50%);
}
:root[data-large-targets="on"] input:is([type="checkbox"], [type="radio"]) {
min-width: 24px;
min-height: 24px;
}
/* ---------- Large cursor ---------- */
:root[data-large-cursor="on"],
:root[data-large-cursor="on"] body {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cpath d='M4 2 L4 32 L12 25 L18 38 L24 35 L18 23 L29 23 Z' fill='black' stroke='white' stroke-width='2.5' stroke-linejoin='round'/%3E%3C/svg%3E") 4 2, auto;
}
:root[data-large-cursor="on"] :is(a[href], button:not(:disabled), [role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"], [role="checkbox"], label, summary, select) {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cpath d='M13 3 a3 3 0 0 1 6 0 V17 l1-1 a3 3 0 0 1 5 2 l1-1 a3 3 0 0 1 5 2 l1-1 a3 3 0 0 1 5 2 V28 c0 5-4 10-10 10 h-5 c-4 0-6-2-8-5 L6 25 a3 3 0 0 1 5-4 l2 2 Z' fill='white' stroke='black' stroke-width='2.5' stroke-linejoin='round'/%3E%3C/svg%3E") 15 2, pointer;
}
:root[data-large-cursor="on"] :is(input:not([type="checkbox"], [type="radio"], [type="range"], [type="button"], [type="submit"]), textarea, [contenteditable="true"]) {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cpath d='M14 4 h12 M20 4 V36 M14 36 h12' fill='none' stroke='white' stroke-width='7' stroke-linecap='round'/%3E%3Cpath d='M14 4 h12 M20 4 V36 M14 36 h12' fill='none' stroke='black' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E") 20 20, text;
}
/* ---------- Forced colours (Windows contrast themes) ----------
*
* Forced colours replace every background with Canvas and drop box-shadows,
* so state drawn only as a tint or a ring disappears. Each state gets a
* system colour here instead: selected, pressed and current items take
* Highlight with HighlightText (and so does everything inside them, whose own
* colours would otherwise be forced back to CanvasText on Highlight), today
* gets a frame, switches a border and a solid knob, dots CanvasText. A colour
* swatch keeps its own colour (forced-color-adjust on the swatch only),
* because the colour is what it shows.
*/
@media (forced-colors: active) {
:focus-visible:not([tabindex="-1"]:not(a[href], button, input, select, textarea, summary, [contenteditable="true"], [role="button"], [role="link"], [role="menuitem"], [role="menuitemradio"], [role="menuitemcheckbox"], [role="option"], [role="gridcell"], [role="row"], [role="treeitem"], [role="tab"], [role="radio"], [role="checkbox"], [role="switch"], [role="slider"], [role="spinbutton"], [role="columnheader"], [role="rowheader"])) {
outline-color: Highlight !important;
}
:is(
[aria-selected="true"]:not([role="gridcell"]:has(> button)),
[role="gridcell"][aria-selected="true"] > button,
[aria-pressed="true"],
[aria-current="page"],
[aria-current="true"],
[aria-current="step"],
[aria-current="location"],
[data-selected="true"],
[data-active="true"],
[role="menuitemradio"][aria-checked="true"],
[role="radio"][aria-checked="true"]:not(input)
) {
/* Not forced, so text inside gets no Canvas backplate; the rules here
and below set every colour instead. */
forced-color-adjust: none;
background-color: Highlight !important;
color: HighlightText !important;
border-color: HighlightText !important;
}
:is(
[aria-selected="true"]:not([role="gridcell"]:has(> button)),
[role="gridcell"][aria-selected="true"] > button,
[aria-pressed="true"],
[aria-current="page"],
[aria-current="true"],
[aria-current="step"],
[aria-current="location"],
[data-selected="true"],
[data-active="true"],
[role="menuitemradio"][aria-checked="true"],
[role="radio"][aria-checked="true"]:not(input)
) *:not(svg *) {
color: HighlightText !important;
background-color: transparent !important;
border-color: HighlightText !important;
fill: currentColor;
}
/* Today, this month, this year: a frame, so it stays apart from the
selection. */
[aria-current="date"] {
outline: 2px solid CanvasText;
outline-offset: -2px;
}
/* Switches: a track border, and a knob that keeps its fill. On is
Highlight with a HighlightText knob on the far side. */
[role="switch"] {
border: 1px solid ButtonText !important;
}
[role="switch"] > span {
forced-color-adjust: none;
background-color: ButtonText !important;
}
[role="switch"][aria-checked="true"] {
background-color: Highlight !important;
border-color: Highlight !important;
}
[role="switch"][aria-checked="true"] > span {
background-color: HighlightText !important;
}
/* Unread and status dots, drawn as an empty tinted circle or a filled
icon. */
.rounded-full:empty:is(.bg-primary, .bg-unread, .bg-success, .bg-destructive, .bg-warning, .bg-info, [class*="bg-red-"], [class*="bg-green-"], [class*="bg-amber-"], [class*="bg-emerald-"], [class*="bg-blue-"], [class*="bg-yellow-"], [class*="bg-orange-"]) {
forced-color-adjust: none;
background-color: CanvasText !important;
}
:is(.fill-unread, .fill-primary, .fill-current) {
fill: CanvasText;
}
/* Colour swatches: an empty element whose colour is set inline (calendar,
tag and theme colours). */
:is(span, div, i):empty[style*="background"] {
forced-color-adjust: none;
}
/* Calendar events lose their fill; a frame keeps each one apart. */
[data-calendar-event] {
outline: 1px solid CanvasText;
outline-offset: -1px;
}
/* Count badges drawn as a tinted pill (the rail's unread count). */
.rounded-full:not(:empty):is(.bg-primary, .bg-destructive, [class*="bg-red-"]) {
border: 1px solid CanvasText;
}
}
/* ---------- Simple mode: no side panels ---------- */
:root[data-simple-mode="on"] [data-simple-hide] {
display: none !important;
}