Something went wrong. Try again.
Comail's webmail client, based on Bulwark. Public source mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408// "Flat fields" - the bulwarkmail.org design system carried into the app.//// Neutral grounds, square panes, one regular-weight sans, and raspberry only// as a small mark: the primary button, the unread square, the 3px edge on the// active folder and the selected message, the focus ring and links. Errors,// warnings and success have their own colours so nothing destructive can be// mistaken for the brand.//// Contrast (WCAG 2.1) is checked for every foreground/background pair; the// tightest is white on raspberry at 4.65:1, so button labels on primary stay// at the base size and weight 500. Raspberry is never used as text: text// links and `text-primary` take --color-ring, which reaches 4.5:1 on the grey// surface in both variants.
export const flatFieldsCSS = `:root { --color-background: #ffffff; --color-foreground: #18181b; --color-card: #ffffff; --color-card-foreground: #18181b; --color-popover: #ffffff; --color-popover-foreground: #18181b; --color-secondary: #f4f4f5; --color-secondary-foreground: #18181b; --color-muted: #f4f4f5; --color-muted-foreground: #56565d; --color-accent: #f4f4f5; --color-accent-foreground: #18181b; --color-sidebar: #ffffff; --color-sidebar-foreground: #18181b; --color-sidebar-border: #dddde1; --color-sidebar-accent: #f4f4f5; --color-sidebar-accent-foreground: #18181b; --color-selection: #f4f4f5; --color-selection-foreground: #18181b; --color-border: #dddde1; --color-input: #7f7f87; --color-ring: #c01f46; --color-primary: #db2d54; --color-primary-foreground: #ffffff; --color-unread: #db2d54; --color-destructive: #b42318; --color-destructive-foreground: #ffffff; --color-success: #17784a; --color-success-foreground: #ffffff; --color-warning: #8a5d00; --color-warning-foreground: #ffffff; --color-info: #1f5f9e; --color-info-foreground: #ffffff; --color-chart-1: #1f5f9e; --color-chart-2: #17784a; --color-chart-3: #8a5d00; --color-chart-4: #db2d54; --color-chart-5: #6b5bb5;
/* Square system: 0 on surfaces, 2px on things you press or type into. */ --radius-xs: 0px; --radius-sm: 0px; --radius-md: 2px; --radius-lg: 2px; --radius-xl: 2px; --radius-2xl: 2px; --radius-3xl: 2px; --radius-4xl: 2px;
/* No frosted glass. */ --blur-xs: 0px; --blur-sm: 0px; --blur-md: 0px; --blur-lg: 0px; --blur-xl: 0px; --blur-2xl: 0px; --blur-3xl: 0px;}
.dark { --color-background: #131315; --color-foreground: #f2f2f3; --color-card: #1f1f22; --color-card-foreground: #f2f2f3; --color-popover: #1f1f22; --color-popover-foreground: #f2f2f3; --color-secondary: #1f1f22; --color-secondary-foreground: #f2f2f3; --color-muted: #1f1f22; --color-muted-foreground: #ababb2; /* A step above popover and muted so hover and selection stay visible inside menus and on the quiet surface. */ --color-accent: #2a2a2e; --color-accent-foreground: #f2f2f3; --color-sidebar: #131315; --color-sidebar-foreground: #f2f2f3; --color-sidebar-border: #36363b; --color-sidebar-accent: #2a2a2e; --color-sidebar-accent-foreground: #f2f2f3; --color-selection: #2a2a2e; --color-selection-foreground: #f2f2f3; --color-border: #36363b; --color-input: #85858d; --color-ring: #ff91a8; --color-primary: #db2d54; --color-primary-foreground: #ffffff; --color-unread: #f0486f; --color-destructive: #ff9b8f; --color-destructive-foreground: #131315; --color-success: #4cc38a; --color-success-foreground: #131315; --color-warning: #e2b341; --color-warning-foreground: #131315; --color-info: #7fb2e8; --color-info-foreground: #131315; --color-chart-1: #7fb2e8; --color-chart-2: #4cc38a; --color-chart-3: #e2b341; --color-chart-4: #f0486f; --color-chart-5: #a99bf0;}`;
// Component-level rules the tokens cannot express. Stable hooks only:// [data-tour], [role], data-* state attributes, and Tailwind utilities.// Nothing here moves, hides or restructures a component.export const flatFieldsSkin = `/* ── Type: Hanken Grotesk (self-hosted by the root layout as --font-hanken, defined on <body>), then a system stack for scripts it does not cover. ── */body[data-theme-skin="builtin-flat-fields"] { font-family: var(--font-hanken, "Hanken Grotesk"), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Noto Sans Thai", "Leelawadee UI", sans-serif;}
body[data-theme-skin="builtin-flat-fields"] .font-bold { font-weight: 600;}
/* Uppercase letter-spaced labels read as sentence-style small text. */body[data-theme-skin="builtin-flat-fields"] .uppercase:not(.font-mono) { text-transform: none; letter-spacing: 0;}
/* ── Flat: no shadows, glows or frosted glass. Clearing --tw-shadow keeps Tailwind's ring utilities working. ── */body[data-theme-skin="builtin-flat-fields"] [class*="shadow"] { --tw-shadow: 0 0 #0000;}
body[data-theme-skin="builtin-flat-fields"] [class*="backdrop-blur"] { -webkit-backdrop-filter: none; backdrop-filter: none;}
/* Grounds are flat; the sign-in page and empty states use soft washes. The image viewer's dark scrim keeps its controls legible and stays. */body[data-theme-skin="builtin-flat-fields"] [class*="bg-gradient-to-"]:not([class*="from-black"]) { background-image: none;}
/* ── Square. \`rounded\` and \`rounded-full\` compile to literals, not radius tokens. Pills, badges, toggles, counters, the compose button, swatches and status dots become 2px shapes; spinners stay round. ── */body[data-theme-skin="builtin-flat-fields"] .rounded,body[data-theme-skin="builtin-flat-fields"] .rounded-full:not(.animate-spin) { border-radius: var(--radius-md);}
/* Calendar event chips (a colour edge on the left, \`rounded-r\`) are tiles. */body[data-theme-skin="builtin-flat-fields"] :is(.rounded-r, .rounded-t) { border-radius: 0;}
/* Avatars are images, and images are square. Initials without a photo sit on the quiet surface instead of a random saturated hue (which also fails contrast with white initials for yellows and greens). The hue is an inline style, hence the one !important. */body[data-theme-skin="builtin-flat-fields"] .rounded-full.overflow-hidden.text-white[title] { border-radius: 0;}
body[data-theme-skin="builtin-flat-fields"] .rounded-full.overflow-hidden.text-white[title]:not(:has(img)) { background-color: var(--color-muted) !important; color: var(--color-muted-foreground); font-weight: 500; box-shadow: inset 0 0 0 1px var(--color-border);}
/* Icons take the text colour; the brand is reserved for marks. *//* Folder role icons take the text colour; tag icons (filled with the tag's own colour) are user data and keep it. */body[data-theme-skin="builtin-flat-fields"] [data-testid="folder-row"] svg:not([fill="currentColor"]) { color: var(--color-muted-foreground);}
body[data-theme-skin="builtin-flat-fields"] [data-testid="folder-row"][data-selected="true"] svg:not([fill="currentColor"]) { color: var(--color-foreground);}
/* Brand tints (bg-primary/5 to /20) are areas of pink: use the neutral selection grey. Strong fills (/40 and up) are progress and marks. */body[data-theme-skin="builtin-flat-fields"] :is([class~="bg-primary/5"], [class~="bg-primary/10"], [class~="bg-primary/15"], [class~="bg-primary/20"], [class~="dark:bg-primary/10"]),body[data-theme-skin="builtin-flat-fields"] :is([class~="hover:bg-primary/5"], [class~="hover:bg-primary/10"], [class~="hover:bg-primary/15"], [class~="hover:bg-primary/20"], [class~="hover:bg-primary/30"]):hover { background-color: var(--color-accent);}
/* Checked and dragged rows: grey, without the pink ring. */body[data-theme-skin="builtin-flat-fields"] :is([class~="ring-primary/20"], [class~="ring-primary/30"]) { --tw-ring-shadow: 0 0 #0000;}
/* Layers: dialogs, menus and toasts are square, separated by a 1px border. */body[data-theme-skin="builtin-flat-fields"] [role="dialog"],body[data-theme-skin="builtin-flat-fields"] [role="alertdialog"],body[data-theme-skin="builtin-flat-fields"] [role="menu"],body[data-theme-skin="builtin-flat-fields"] [role="listbox"],body[data-theme-skin="builtin-flat-fields"] .toast-item { border-radius: 0;}
body[data-theme-skin="builtin-flat-fields"] [role="alertdialog"],body[data-theme-skin="builtin-flat-fields"] [role="menu"],body[data-theme-skin="builtin-flat-fields"] .toast-item { border: 1px solid var(--color-input);}
body[data-theme-skin="builtin-flat-fields"] .toast-item { background-color: var(--color-popover);}
/* ── Panes sit on the page ground, split by 1px rules, so grey only ever means "selected" or "under the pointer". ── */body[data-theme-skin="builtin-flat-fields"] .w-14.bg-secondary,body[data-theme-skin="builtin-flat-fields"] .border-e.bg-secondary,body[data-theme-skin="builtin-flat-fields"] .flex-col.h-full.bg-secondary { background-color: var(--color-background);}
/* ── Selection is grey plus a 3px brand edge. The folder row asks for border-primary, but the global \`* { border-color }\` rule wins over utilities, so the edge colour is set here. ── */body[data-theme-skin="builtin-flat-fields"] [data-tour="sidebar"] [data-testid="folder-row"] { border-inline-start-color: transparent;}
body[data-theme-skin="builtin-flat-fields"] [data-tour="sidebar"] [data-selected="true"] { border-inline-start-width: 3px; border-inline-start-color: var(--color-primary); font-weight: 500;}
/* Side panes (settings, contacts, files, calendar) mark their active item only with bg-accent; give it the same edge. */body[data-theme-skin="builtin-flat-fields"] :is(.border-e.bg-secondary, .flex-col.h-full.bg-secondary) button.bg-accent { box-shadow: inset 3px 0 0 var(--color-primary);}
body[data-theme-skin="builtin-flat-fields"] [data-tour="contacts-list"] .bg-selection { box-shadow: inset 3px 0 0 var(--color-primary);}
body[data-theme-skin="builtin-flat-fields"] [data-tour^="nav-"][aria-current="page"] { background-color: var(--color-accent); color: var(--color-accent-foreground); box-shadow: inset 3px 0 0 var(--color-primary);}
/* The calendar's "now" line is drawn with the destructive colour; it marks time, not danger, so it takes the brand. */body[data-theme-skin="builtin-flat-fields"] .pointer-events-none > .flex.items-center > .bg-destructive:is(.h-px, .w-2) { background-color: var(--color-primary);}
/* Unread counters are brand marks, not alarm red. */body[data-theme-skin="builtin-flat-fields"] [data-tour^="nav-"] .bg-red-500 { background-color: var(--color-primary); color: var(--color-primary-foreground);}
body[data-theme-skin="builtin-flat-fields"] [data-tour="email-list"] [aria-current="true"]::before { content: ""; position: absolute; inset-block: 0; inset-inline-start: 0; width: 3px; background-color: var(--color-primary); z-index: 1; pointer-events: none;}
/* The reading pane is one white ground; no grey band below short messages. */body[data-theme-skin="builtin-flat-fields"] [data-tour="email-viewer"] .overflow-auto.bg-muted\\/30 { background-color: var(--color-background);}
/* Toggles: off is an outlined control with a grey thumb, on is a brand track with a white thumb. */body[data-theme-skin="builtin-flat-fields"] [role="switch"][aria-checked="false"] { background-color: var(--color-background); box-shadow: inset 0 0 0 1px var(--color-input);}
body[data-theme-skin="builtin-flat-fields"] [role="switch"][aria-checked="false"] > span { background-color: var(--color-input);}
/* Things you type into or press carry the control border (3:1 on every ground). The app asks for border-input, but the global \`* { border-color }\` rule outranks utilities, so it is set here. Borderless fields are unaffected because they have no border width. */body[data-theme-skin="builtin-flat-fields"] :is(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, textarea, .border-input) { border-color: var(--color-input);}
body[data-theme-skin="builtin-flat-fields"] :is(input[type="checkbox"], input[type="radio"]) { accent-color: var(--color-primary);}
/* Unread is a small square and a 600 weight, not a tinted row. */body[data-theme-skin="builtin-flat-fields"] [data-tour="email-list"] .bg-accent\\/30:not(:hover) { background-color: var(--color-background);}
body[data-theme-skin="builtin-flat-fields"] [data-tour="email-list"] svg.fill-unread { color: transparent; fill: transparent; background-color: var(--color-unread); width: 7px; height: 7px;}
/* The square sits clear of the 3px selection edge. */body[data-theme-skin="builtin-flat-fields"] [data-tour="email-list"] div.absolute:has(> svg.fill-unread) { inset-inline-start: 6px;}
/* In the split layout the app anchors the mark at "padding + half an avatar", but the avatar is centred in the row, so the mark lined up with nothing. Put it on the sender line it marks (text-sm in a 20-24px row). The other layouts already centre it on their single line. The offset is an inline style, hence !important. */body[data-theme-skin="builtin-flat-fields"] [data-tour="email-list"] div.absolute[style*="+ 1.25rem"]:has(> svg.fill-unread) { top: calc(var(--density-item-py) + 0.75rem) !important;}
/* ── Raspberry is too light for text on grey: text uses the link colour. ── */body[data-theme-skin="builtin-flat-fields"] .text-primary { color: var(--color-ring);}
body[data-theme-skin="builtin-flat-fields"] .email-content a,body[data-theme-skin="builtin-flat-fields"] .email-content-text a { color: var(--color-ring); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;}
/* ── Primary hover goes darker, never lighter or pinker. ── */body[data-theme-skin="builtin-flat-fields"] .bg-primary:is(button, a):hover,body[data-theme-skin="builtin-flat-fields"] [data-tour="compose-button"]:hover { background-color: color-mix(in srgb, var(--color-primary) 84%, #000);}
/* Disabled is grey, not a faded pink. */body[data-theme-skin="builtin-flat-fields"] button.bg-primary:disabled { background-color: var(--color-muted); color: var(--color-muted-foreground); opacity: 1;}
/* A chosen option in a segmented control is filled with the text colour, as on the website's edition switch: a choice, not a call to action. */body[data-theme-skin="builtin-flat-fields"] button[aria-pressed="true"].bg-primary,body[data-theme-skin="builtin-flat-fields"] [role="radio"][aria-checked="true"].bg-primary,body[data-theme-skin="builtin-flat-fields"] .flex.border.overflow-hidden > button.bg-primary { background-color: var(--color-foreground); color: var(--color-background);}
/* The thread count stays a quiet label even when the thread has unread mail; the unread square already says so. */body[data-theme-skin="builtin-flat-fields"] [data-tour="email-list"] span.bg-primary.text-primary-foreground { background-color: var(--color-muted); color: var(--color-foreground); font-weight: 600;}
/* ── Icons follow the website's rule: 1.5 stroke, square caps, mitred joins. ── */body[data-theme-skin="builtin-flat-fields"] svg.tabler-icon { stroke-width: 1.5; stroke-linecap: square; stroke-linejoin: miter;}
/* ── Focus: one ring, 2px solid, 2px off the element. ── */body[data-theme-skin="builtin-flat-fields"] :focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; --tw-ring-shadow: 0 0 #0000; --tw-ring-offset-shadow: 0 0 #0000;}
/* Text fields keep focus for as long as someone types, so the ring hugs the field instead of floating a box around it. */body[data-theme-skin="builtin-flat-fields"] :is(input, textarea, select, [contenteditable="true"]):focus-visible { outline-offset: 0;}`;