Files
verdande/web/src/app.css
T
kristianwindandClaude Opus 4.8 8a5dfef41a fix(theme): lighter day separators in Charcoal
In Charcoal the note-list day separators used --ground, the darkest colour
in the theme, so they read as dark bars on the lighter list. They take the
menu's own resting grey (#2d3133) now — the colour "Projekter" sits on —
via a --note-group-bg fallback token, so every other theme keeps --ground.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 20:46:51 +02:00

613 lines
18 KiB
CSS

/*
* 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;
/* The sidebar's own text size, its own knob (Settings → Appearance) so the menu
can be made easier to read without dragging the whole list with it. Default is
a step up from the old --text-sm the nav used, at medium weight, so the menu
items read as menu items. [data-menu-size] on <html> overrides it. */
--menu-size: 0.9375rem;
/* 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 <html> — 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);
}
/* Kul — the greys of Apple Notes' dark mode, to Kristian's own sampled values.
Three panel greys, lightest sidebar to darkest editor — the reverse of the usual
sunken-is-darkest order, which is why the editor gets its own --editor-bg rather
than sharing --surface-raised with the list. The accent turns amber, and the
selected menu item, selected note and section headings each get a fallback token
so this theme can hit its colours without touching any other. */
[data-theme='charcoal'] {
--ground: #1e1e1e;
--surface: #2a3033;
--surface-raised: #242b2e; /* the note list */
--surface-sunken: #2d3133; /* the menu / sidebar — lightest here */
--line: #34393b;
--line-strong: #414749;
--ink: #ffffff;
--ink-muted: #dcdcdc;
--ink-faint: #9ba0a2;
/* Amber, Apple's Notes accent. The bright value is the selected menu item; the
dark text rides on top of it. */
--accent: #ffce35;
--accent-hover: #ffd75a;
--accent-sunken: #ffce35;
--accent-ink: #1e1e1e;
/* Per-element, and only here — every other theme falls back to the shared token
in the var() calls that read these. */
--editor-bg: #1e1e1e; /* the text field, a shade below the list */
--note-on-bg: #a78c26; /* a selected note in the list — gold */
--note-on-ink: #1e1e1e; /* dark text on the gold */
--nav-active-ink: #1e1e1e; /* dark text on the amber menu item */
--heading-ink: #8a8e90; /* PROJEKTER and the group names — quiet grey */
--note-group-bg: #2d3133; /* day separators take the menu's resting grey, not the darker ground */
--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;
/* Ingen radius her. En omrids følger elementets egen afrunding af sig selv, og
at tvinge en anden på tegner en firkantet bue uden om en rund kant — to
streger, hvor der skulle være én. Det ses tydeligst om et pilleformet
søgefelt. */
}
/* A translucent wash of the accent, not a solid block and not the old near-invisible
tint. --accent-sunken sat a hair off the editor's own surface in dark, so a
selection could not be seen; a solid accent could, but read as a heavy slab. Mixing
the accent part-way into transparent lays a clear green film over the text while it
keeps its own colour — visible on white and on the dark surfaces alike. */
::selection {
background: color-mix(in oklab, var(--accent) 42%, transparent);
}
/* 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;
}
/* Body text size — its own control in Settings → Appearance, for reading the list
and the notes larger (or smaller) without touching the menu. It re-scales the
shared type tokens the same way a look does; placed after the looks so that an
explicit size choice wins the tokens they both set. The menu has its own knob
below and does not move with this one. */
[data-text-size='small'] {
--text-base: 0.875rem;
--text-sm: 0.75rem;
--text-lg: 1rem;
}
[data-text-size='large'] {
--text-base: 1.0625rem;
--text-sm: 0.9375rem;
--text-lg: 1.1875rem;
}
[data-text-size='xl'] {
--text-base: 1.1875rem;
--text-sm: 1.0625rem;
--text-lg: 1.3125rem;
}
/* Menu (sidebar) font size — independent of the body size above, because the two
are separate questions: a menu you can read at a glance, and a list you can read
a paragraph of. */
[data-menu-size='small'] {
--menu-size: 0.8125rem;
}
[data-menu-size='large'] {
--menu-size: 1.0625rem;
}
[data-menu-size='xl'] {
--menu-size: 1.1875rem;
}