Files
verdande/web/src/app.css
T
Kristian WindandClaude Opus 5 a4b12f3601
Release / Build and publish image (push) Failing after 9m52s
feat(notes): listen som Apple Noters, ⌘K finder noter, og de kan trækkes
Rækken har nu titel i samme størrelse som en opgavetitel — en note og en opgave er
to ting af samme slags, og hver sin størrelse får den ene til at se mindre vigtig
ud. Under den står dato og begyndelse på samme linje, som Apple Noter gør det: to
linjer pr. note frem for tre, og datoen først, fordi det er den, man skanner efter.
Ligger noten i et projekt, står det nedenunder.

⌘K søgte kun i opgaver og projekter. Feltet ser ud til at kunne finde alt, så det
er svært at gætte, at halvdelen af programmet ikke var med. Og mens jeg var der:
"Projekt" og "Opgave" stod hårdkodet på dansk i paletten.

En note kan trækkes ind på et projekt i sidebjælken — samme mål som en opgave,
fordi det er samme gestus: læg denne her.

To fejl i søgefeltet, begge Safaris egen type="search":
Fokusringen tvang sin egen hjørneradius og tegnede en firkantet bue uden om en
rund kant — to streger, hvor der skulle være én. Den følger nu elementets form,
overalt i programmet. Og feltet skiftede udseende ved fokus: forstørrelsesglasset
forsvandt og pilleformen blev firkantet. Det tegnes nu af os.

Trækket og palettens noter er ikke fastholdt i en prøve. ⌘K, tastning og en
debounce oven på hinanden gjorde den upålidelig, og en flakkende prøve siger
mindre end ingen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-19 20:46:05 +02:00

519 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;
/* 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. */
}
::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;
}