/* * verdande — design tokens. * * Nordic minimalism: a near-neutral ground, generous space, and one accent that * does all the signalling. The greys are not pure grey — they carry a trace of * green, which is what stops a dark interface from reading as cold blue-black and * is most of the difference between "calm" and "unfinished". * * Everything is a token. A component that reaches for a raw hex is a component * that will be wrong in one of the two themes. */ :root { /* Neutrals, dark theme. Named by role, not by lightness, so the light theme below can invert the scale without every name becoming a lie. */ --ground: #111312; --surface: #171a19; --surface-raised: #1d211f; --surface-sunken: #0d0f0e; --line: #262b29; --line-strong: #343a37; --ink: #e8ebe9; --ink-muted: #9aa39e; /* The quiet tier, and the one that decides whether the interface is readable: dates, counts and hints all live here. Lifted from #6b736f, which sat at 3.8:1 against the ground — under AA, and it showed. */ --ink-faint: #828a84; /* Nordlys-grøn. One accent, used for the checkmark, focus rings, and the single primary action on a screen — and for nothing else, which is what keeps it meaning "this one". */ --accent: #5fbf8f; --accent-hover: #6fcf9f; --accent-sunken: #1c3329; --accent-ink: #0d1411; /* Priorities. Red is reserved for P1, so it never appears decoratively — if something is red on this screen, it is urgent. */ --p1: #cf6b6b; --p2: #d69f5c; --p3: #6b9ecf; --p4: var(--ink-faint); --danger: #cf6b6b; --danger-sunken: #33201f; /* Marks for projects, groups, labels and filters. * * The one set of colours in this file that is *not* redefined per theme, and * on purpose. Everything else here is text, a ground or a border, and has to * be measured against whatever sits behind it. These are none of those: they * are small solid dots beside a label that carries the meaning, so what they * owe is to be told apart from one another and to be visible on both a * near-black and a white ground. All of them sit in the mid-tone band where * that is true, which is why none is very dark or very pale. * * It also keeps them out of the trap the themes are in: a colour added to four * blocks and forgotten in the fifth looks finished and falls back silently. * There is no fifth block to forget. * * The ids are what the database stores. web/src/lib/colors.js lists them for * the picker, and a Go test reads that file to check the server's list of * accepted names has not drifted from it. */ --color-graphite: #8a8f98; --color-tomato: #d9614f; --color-amber: #d9a04f; --color-lime: #a3b74d; --color-green: #4fae7a; --color-teal: #45b0ab; --color-blue: #5b9bd9; --color-indigo: #7f83d6; --color-violet: #a86fd0; --color-magenta: #d46aa8; /* Type. System stack: it renders instantly, matches the platform, and has proper Danish glyphs on every device — a webfont would cost a round trip and a flash of unstyled text on the one screen people open fifty times a day. */ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', system-ui, sans-serif; --font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace; --text-xs: 0.75rem; --text-sm: 0.8125rem; --text-base: 0.9375rem; --text-lg: 1.0625rem; --text-xl: 1.375rem; --text-2xl: 1.75rem; /* Spacing on a 4px grid. */ --s1: 0.25rem; --s2: 0.5rem; --s3: 0.75rem; --s4: 1rem; --s5: 1.5rem; --s6: 2rem; --s7: 3rem; --s8: 4rem; --radius-sm: 4px; --radius: 8px; --radius-lg: 12px; --radius-full: 999px; /* Shadows are nearly absent by design. A flat surface separated by a hairline reads as calmer than the same surface floating on a drop shadow. */ --shadow: 0 1px 2px rgb(0 0 0 / 0.2); --shadow-lg: 0 8px 32px rgb(0 0 0 / 0.35); /* Motion. Fast enough to feel instant, slow enough to be seen. The complete-animation is the one place a longer curve earns its keep. */ --ease: cubic-bezier(0.2, 0, 0.2, 1); --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --fast: 120ms; --medium: 200ms; --slow: 400ms; --sidebar-width: 268px; --content-max: 720px; } /* * The dark palette again, by name. * * :root carries it as the default, and :root only ever matches — so a * swatch in the settings picker asking for data-theme="dark" would otherwise * inherit whatever the page is wearing. Repeated rather than restructured: the * alternative is splitting the token block in two and leaving somebody to work * out which half a new token belongs in. */ [data-theme='dark'] { --ground: #111312; --surface: #171a19; --surface-raised: #1d211f; --surface-sunken: #0d0f0e; --line: #262b29; --line-strong: #343a37; --ink: #e8ebe9; --ink-muted: #9aa39e; /* The quiet tier, and the one that decides whether the interface is readable: dates, counts and hints all live here. Lifted from #6b736f, which sat at 3.8:1 against the ground — under AA, and it showed. */ --ink-faint: #828a84; --accent: #5fbf8f; --accent-hover: #6fcf9f; --accent-sunken: #1c3329; --accent-ink: #0d1411; --p1: #cf6b6b; --p2: #d69f5c; --p3: #6b9ecf; --p4: var(--ink-faint); --danger: #cf6b6b; --danger-sunken: #33201f; --shadow: 0 1px 2px rgb(0 0 0 / 0.2); --shadow-lg: 0 8px 32px rgb(0 0 0 / 0.35); } /* * The other themes. * * Each one redefines colour and nothing else: no spacing, no type, no radii. * That is what keeps them themes rather than five separate designs, and it is * why a component that reaches for a raw hex breaks in four places at once * instead of one. * * Every theme states its full palette rather than inheriting the dark one and * patching it. A partial theme looks finished until somebody adds a token, at * which point four themes silently fall back to a colour from a fifth. */ /* Skumring — the same dark, turned warm, with amber where the green was. */ [data-theme='dusk'] { --ground: #14110f; --surface: #1b1714; --surface-raised: #221d19; --surface-sunken: #100d0b; --line: #2b2521; --line-strong: #3a322c; --ink: #ece6df; --ink-muted: #a3988c; /* Light enough to clear 4.5:1 against the ground. The quiet tier is where dates and counts live, and it is the first thing to disappear. */ --ink-faint: #8d8377; --accent: #d99a4e; --accent-hover: #e8a95c; --accent-sunken: #33261480; --accent-ink: #16110a; --p1: #cf6b6b; --p2: #d69f5c; --p3: #6b9ecf; --p4: var(--ink-faint); --danger: #cf6b6b; --danger-sunken: #33201f; --shadow: 0 1px 2px rgb(0 0 0 / 0.25); --shadow-lg: 0 8px 32px rgb(0 0 0 / 0.4); } /* Papir — warm light, closer to paper than to a screen. */ [data-theme='paper'] { --ground: #f6f1e7; --surface: #fffcf6; --surface-raised: #fffcf6; --surface-sunken: #ece5d7; --line: #ded5c3; --line-strong: #c8bda6; --ink: #241f18; --ink-muted: #665c4c; --ink-faint: #756a55; --accent: #8a5a2b; --accent-hover: #74491f; --accent-sunken: #ebdfcb; --accent-ink: #fffcf6; --p1: #b0452f; --p2: #96701f; --p3: #3f6b8a; --p4: var(--ink-faint); --danger: #b0452f; --danger-sunken: #f3e2dc; --shadow: 0 1px 2px rgb(60 40 10 / 0.08); --shadow-lg: 0 8px 32px rgb(60 40 10 / 0.14); } /* * Kontrast — for reading in bright sun, or for eyes that the quiet greys of the * other four do not serve. Pure black on white, borders that are actually * visible, and an accent dark enough to pass against white text. * * The muted inks are deliberately much darker than elsewhere: "secondary text" * at 60% grey is the first thing to disappear, and it is where dates live. */ [data-theme='contrast'] { --ground: #ffffff; --surface: #ffffff; --surface-raised: #f2f2f2; --surface-sunken: #e8e8e8; --line: #767676; --line-strong: #1a1a1a; --ink: #000000; --ink-muted: #2b2b2b; --ink-faint: #4a4a4a; --accent: #005c2e; --accent-hover: #00753a; --accent-sunken: #cfe8da; --accent-ink: #ffffff; --p1: #a10000; --p2: #7a4a00; --p3: #00417a; --p4: var(--ink-faint); --danger: #a10000; --danger-sunken: #f7dcdc; --shadow: 0 1px 2px rgb(0 0 0 / 0.3); --shadow-lg: 0 8px 32px rgb(0 0 0 / 0.35); } [data-theme='light'] { --ground: #faf9f7; --surface: #ffffff; --surface-raised: #ffffff; --surface-sunken: #f2f1ee; --line: #e5e3de; --line-strong: #d3d0c9; --ink: #1c201e; --ink-muted: #5f6763; /* Was #8b918d at 3.1:1 — the worst of the five, and the theme most likely to be read in daylight. */ --ink-faint: #6e756f; --accent: #257552; --accent-hover: #1f6a49; --accent-sunken: #e2f0e9; --accent-ink: #ffffff; --p1: #c04a4a; --p2: #b57a2b; --p3: #3d7ab5; --p4: var(--ink-faint); --danger: #c04a4a; --danger-sunken: #f7e6e5; --shadow: 0 1px 2px rgb(0 0 0 / 0.06); --shadow-lg: 0 8px 32px rgb(0 0 0 / 0.12); } *, *::before, *::after { box-sizing: border-box; } html, body { margin: 0; padding: 0; height: 100%; } body { background: var(--ground); color: var(--ink); font-family: var(--font); font-size: var(--text-base); line-height: 1.5; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; /* The app owns its own scrolling regions; the document itself never scrolls, which is what stops iOS rubber-banding the whole interface. */ overflow: hidden; overscroll-behavior: none; } h1, h2, h3, h4 { margin: 0; font-weight: 560; letter-spacing: -0.015em; line-height: 1.25; } button, input, textarea, select { font: inherit; color: inherit; } button { cursor: pointer; background: none; border: none; padding: 0; } /* One focus treatment everywhere, and only for keyboard users: a focus ring that also appears on mouse clicks is noise that people learn to ignore. */ :focus { outline: none; } :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); } ::selection { background: var(--accent-sunken); color: var(--ink); } /* Scrollbars, made quiet. The default on Windows is a wide grey slab that dominates a minimal layout. */ * { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; } *::-webkit-scrollbar { width: 10px; height: 10px; } *::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: var(--radius-full); border: 3px solid transparent; background-clip: content-box; } *::-webkit-scrollbar-track { background: transparent; } /* Somebody who has asked for less motion gets none of it. The complete-animation is satisfying; it is not worth making a person nauseous. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } /* Form fields are 16px on a touch device, whatever the component asked for. * * Below 16px, iOS Safari zooms the page in when a field takes focus — and it does * not zoom back out when the field is blurred. The person is left with the whole * interface too large and has to pinch it back themselves, once per field they * tap. The type scale here tops out at 15px, so every input in the app did it: * quick add, the sign-in form, and the task drawer worst of all, which is four * fields deep. * * `!important` on purpose. This is a browser workaround that has to beat every * component's own typography, and each of those is a scoped class that outranks * any selector this file could write. Narrowing it further would mean editing the * sixty-nine places that set a field's font size and adding a rule none of them * can be allowed to break. * * Touch rather than a width: an iPad in Safari does the same thing at a width no * phone breakpoint would catch. */ @media (hover: none) and (pointer: coarse) { input, textarea, select { font-size: 16px !important; } } .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } /* --- looks ----------------------------------------------------------------- * * A second axis, beside the colour themes. * * A theme answers "how bright"; a look answers "what does it feel like to read". * Typeface, how tight the rows sit, how round the corners are. They are separate * because they are separate questions: somebody who wants a serif face at night * should not have to give up the dark palette to get it. * * It is also the honest answer to looking like something else. The default is * deliberately familiar — that is what was asked for — but a program with one * possible appearance has borrowed a shape it cannot give back. With looks, the * resemblance is one setting among several rather than the only thing it can be. */ /* Serif, roomier, softer. For reading and writing rather than scanning — which is what a program with notes in it is half the time. */ [data-look='rolig'] { --font: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, 'Times New Roman', serif; --text-base: 1.0625rem; --text-sm: 0.9375rem; --radius: 10px; --radius-lg: 16px; --s2: 0.5625rem; --s3: 0.875rem; --s4: 1.25rem; } [data-look='rolig'] body { line-height: 1.65; letter-spacing: 0.006em; } /* Tighter. More rows on a screen, squarer corners, less air — for a long list read at a glance rather than a page read a paragraph at a time. */ [data-look='taet'] { --text-base: 0.875rem; --text-sm: 0.78125rem; --text-xs: 0.6875rem; --radius: 5px; --radius-lg: 8px; --radius-sm: 3px; --s1: 0.1875rem; --s2: 0.375rem; --s3: 0.5625rem; --s4: 0.75rem; } [data-look='taet'] body { line-height: 1.4; } /* One face for everything, including the prose. Not a joke setting: a monospaced list of tasks lines up in a way a proportional one never does, and some people read code all day and want the same shapes here. */ [data-look='terminal'] { --font: var(--font-mono); --text-base: 0.875rem; --text-sm: 0.8125rem; --radius: 3px; --radius-lg: 4px; --radius-sm: 2px; } [data-look='terminal'] body { line-height: 1.5; letter-spacing: -0.01em; }