Otter's web app ships no dark mode, and heavy users read transcripts for hours at night. This proposes three shades rather than one, so the choice survives changing lighting instead of being wrong half the day. Contents: - otter-dark.css token set, one block per tier, scoped to :root[data-theme] - preview.html self-contained demo, no build, no network - PREVIEW.png rendered screenshot of the demo - userscript/ Tampermonkey stopgap that works today Contrast figures in the README are measured from the token values with the WCAG 2.1 formula, not estimated. The notable finding is that Otter's brand blue #0b57d0 fails as accent text on every dark surface (2.59:1 to 3.29:1) while remaining correct as a solid fill (6.39:1), so the tokens split accent from accent-fill. MIT licensed. No affiliation with AISense Inc. Co-authored-by: CLU <clu@binaryranch.com>
102 lines
4.2 KiB
CSS
102 lines
4.2 KiB
CSS
/* =============================================================================
|
|
Otter dark theme tokens
|
|
Proposal by Christian Sass (csass@binaryranch.com), Binary Ranch. 2026-10-27.
|
|
|
|
Three tiers, one token set. Nothing here filters, inverts or transforms the
|
|
page, so avatars, screen captures and video keep their exact original colour.
|
|
|
|
Usage: set data-theme on <html>. That is the whole contract.
|
|
<html data-theme="dusk">
|
|
Values in comments are the measured WCAG 2.1 contrast ratio against --ot-canvas
|
|
unless stated otherwise.
|
|
============================================================================= */
|
|
|
|
/* ---- Midnight: OLED panels and late-night reading ---- */
|
|
:root[data-theme="midnight"] {
|
|
--ot-canvas: #000000;
|
|
--ot-surface-1: #0B0D10; /* sidebars, nav rails, player bar */
|
|
--ot-surface-2: #12151A; /* conversation cards, list rows, panels */
|
|
--ot-surface-3: #1A1E24; /* hover, selected row, menus */
|
|
--ot-border: #22272E; /* hairlines, card outlines (decorative) */
|
|
--ot-border-2: #303740; /* focused input, split-pane divider */
|
|
|
|
--ot-text: #E8EBEF; /* 17.56:1 on canvas */
|
|
--ot-text-2: #A0A8B4; /* 8.75:1 on canvas */
|
|
--ot-text-3: #6B7480; /* 4.44:1 on canvas */
|
|
|
|
--ot-accent: #6FA8FF; /* 8.72:1, links and active nav */
|
|
--ot-accent-fill:#6FA8FF; /* solid button / active pill background */
|
|
--ot-on-accent: #0A1220; /* 7.78:1 on the fill */
|
|
|
|
--ot-danger: #FF6B6B; /* 7.57:1 */
|
|
--ot-warn: #FFB020;
|
|
--ot-success: #4ADE80;
|
|
|
|
color-scheme: dark;
|
|
}
|
|
|
|
/* ---- Dusk: default, mixed lighting ---- */
|
|
:root[data-theme="dusk"] {
|
|
--ot-canvas: #101216;
|
|
--ot-surface-1: #16191E; /* sidebars, nav rails, player bar */
|
|
--ot-surface-2: #1C2027; /* conversation cards, list rows, panels */
|
|
--ot-surface-3: #242931; /* hover, selected row, menus */
|
|
--ot-border: #2A3038; /* hairlines, card outlines (decorative) */
|
|
--ot-border-2: #3A424C; /* focused input, split-pane divider */
|
|
|
|
--ot-text: #E6E9ED; /* 15.39:1 on canvas */
|
|
--ot-text-2: #A6ADB8; /* 8.29:1 on canvas */
|
|
--ot-text-3: #737B86; /* 4.38:1 on canvas */
|
|
|
|
--ot-accent: #7FAEFF; /* 8.39:1, links and active nav */
|
|
--ot-accent-fill:#7FAEFF; /* solid button / active pill background */
|
|
--ot-on-accent: #0A1220; /* 8.39:1 on the fill */
|
|
|
|
--ot-danger: #FF7B72; /* 7.43:1 */
|
|
--ot-warn: #FFC14D;
|
|
--ot-success: #5BD98A;
|
|
|
|
color-scheme: dark;
|
|
}
|
|
|
|
/* ---- Slate: bright rooms and daytime ---- */
|
|
:root[data-theme="slate"] {
|
|
--ot-canvas: #1B1F24;
|
|
--ot-surface-1: #22272D; /* sidebars, nav rails, player bar */
|
|
--ot-surface-2: #282E35; /* conversation cards, list rows, panels */
|
|
--ot-surface-3: #30373F; /* hover, selected row, menus */
|
|
--ot-border: #363E47; /* hairlines, card outlines (decorative) */
|
|
--ot-border-2: #48515C; /* focused input, split-pane divider */
|
|
|
|
--ot-text: #ECEEF1; /* 14.25:1 on canvas */
|
|
--ot-text-2: #B4BAC4; /* 8.49:1 on canvas */
|
|
--ot-text-3: #8A919C; /* 5.21:1 on canvas */
|
|
|
|
--ot-accent: #8FB8FF; /* 8.26:1, links and active nav */
|
|
--ot-accent-fill:#8FB8FF; /* solid button / active pill background */
|
|
--ot-on-accent: #0A1220; /* 9.34:1 on the fill */
|
|
|
|
--ot-danger: #FF8A80; /* 7.25:1 */
|
|
--ot-warn: #FFCB66;
|
|
--ot-success: #6FE39B;
|
|
|
|
color-scheme: dark;
|
|
}
|
|
|
|
/* Opt in to the system preference, defaulting to Dusk. */
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme]) {
|
|
--ot-canvas: #101216; --ot-surface-1: #16191E; --ot-surface-2: #1C2027; --ot-surface-3: #242931;
|
|
--ot-border: #2A3038; --ot-border-2: #3A424C;
|
|
--ot-text: #E6E9ED; --ot-text-2: #A6ADB8; --ot-text-3: #737B86;
|
|
--ot-accent: #7FAEFF; --ot-accent-fill: #7FAEFF; --ot-on-accent: #0A1220;
|
|
--ot-danger: #FF7B72; --ot-warn: #FFC14D; --ot-success: #5BD98A;
|
|
color-scheme: dark;
|
|
}
|
|
}
|
|
|
|
/* Reference wiring. Otter's own selectors replace the generic ones; the token
|
|
names are the part worth keeping. */
|
|
[data-theme] body { background: var(--ot-canvas); color: var(--ot-text); }
|
|
[data-theme] a { color: var(--ot-accent); }
|
|
[data-theme] ::selection { background: color-mix(in srgb, var(--ot-accent) 30%, transparent); }
|