Fire udseender ved siden af de fem temaer: Verdande, Rolig (serif, mere luft), Tæt (flere rækker, mindre luft) og Terminal (én bredde til det hele). De ganges ud til tyve udseender fra otte små blokke CSS frem for at kræve en blok hver. To akser, fordi det er to spørgsmål. Den, der vil have en serif om natten, skal ikke opgive den mørke palet for at få den — og prøven holder netop det fast: at vælge en skrift må ikke trække temaet med sig. Udseendet sættes før første optegning, ligesom temaet. Ellers bliver siden sat op i én skrift og derefter i en anden, og det er et synligt spring. Det er også det ærlige svar på at ligne noget andet. Standarden er bevidst genkendelig — det var det, der blev bedt om — men et program med ét muligt udseende har lånt en form, det ikke kan give tilbage. Med udseender er ligheden en indstilling blandt flere frem for det eneste, det kan være. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
516 lines
14 KiB
CSS
516 lines
14 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;
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* 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;
|
|
}
|