Lay the Almanacka design foundation: tokens, type, shell
New token system in global.css (near-white surfaces, one ink-blue accent, no sub-12px type), self-hosted Schibsted Grotesk, rewritten primitives (buttons, fields, modal shell, segmented, disclosure, skeleton), a borderless 56px header with an SV | EN language toggle, an Icon component replacing glyphs, page skeletons that keep the layout during loading, and leave-type colours moved out of TS into CSS variables. Feature blocks keep their structure for later phases. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+164
-130
@@ -3,112 +3,144 @@
|
||||
@import './components.css';
|
||||
@import './responsive.css';
|
||||
|
||||
/* Typeface — Schibsted Grotesk (OFL), self-hosted from /public/fonts so no
|
||||
* request ever leaves for a font CDN. Two subsets with Google's own
|
||||
* unicode-ranges: the browser only fetches latin-ext when a glyph needs it.
|
||||
* Variable weight axis 400–900; `swap` so text is never invisible while the
|
||||
* file loads (index.html preloads the latin file to keep that swap short). */
|
||||
@font-face {
|
||||
font-family: 'Schibsted Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 400 900;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SchibstedGrotesk-latin-ext-v7.woff2') format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
|
||||
U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
|
||||
U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Schibsted Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 400 900;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SchibstedGrotesk-latin-v7.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
|
||||
U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* Design Tokens
|
||||
*
|
||||
* One neutral ground + one accent; every other colour is semantic (danger/
|
||||
* warning/success) and exists as a solid + "-weak" tint pair. Everything
|
||||
* below is used app-wide, including the calendar and modal families that
|
||||
* used to hardcode their own hex values — see calendar.css/period-popover.css/
|
||||
* etc. Two values are load-bearing for e2e: --c-danger-weak (red day
|
||||
* background) and --c-weekend-bg (plain-Saturday background) — read
|
||||
* directly by frontend/e2e/tests/red-days.spec.ts. Do not repurpose either.
|
||||
* One neutral ground, one accent. Hierarchy comes from type and whitespace;
|
||||
* hairlines only where a boundary is unavoidable; one shadow, only on
|
||||
* floating layers. Leave types are told apart by fill / ink / outline, not
|
||||
* by hue, and day states by the numeral, not by a cell fill.
|
||||
*
|
||||
* Four names are load-bearing for e2e, which resolves them by name and
|
||||
* compares against painted elements: --c-danger-weak (red-day cell),
|
||||
* --c-weekend-bg (plain Saturday), --c-accent-hover and --c-surface
|
||||
* (header-nav-hover). Do not repurpose any of them.
|
||||
*/
|
||||
:root {
|
||||
/* Color - calm, trustworthy palette */
|
||||
--c-bg: #f7f8fa;
|
||||
--c-surface: #ffffff;
|
||||
--c-border: #e3e6eb;
|
||||
--c-text: #1c2733;
|
||||
--c-text-muted: #5b6675;
|
||||
--c-text-faint: #94a0ac;
|
||||
--c-accent: #2f6f4f; /* calm green — the app's one accent */
|
||||
--c-accent-weak: #e8f1ec;
|
||||
--c-accent-hover: #1f5a3b;
|
||||
--c-accent-active: #0f4a2f;
|
||||
--c-danger: #b3261e;
|
||||
--c-danger-weak: #fdeceb; /* also the red-day cell background — see note above */
|
||||
--c-danger-hover: #8a1a15;
|
||||
--c-success: #1e7d4f;
|
||||
--c-success-weak: #e7f4ec;
|
||||
--c-success-hover: #0d6a37;
|
||||
--c-warning: #8a6d1f;
|
||||
--c-warning-weak: #fef8e7;
|
||||
--font-sans: 'Schibsted Grotesk', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
|
||||
/* Calendar-specific semantics (klämdag/weekend/red-day borders + hover) —
|
||||
* kept distinct from the generic danger/warning pair above because a red
|
||||
* day and a klämdag are both visible on screen at once and need to read
|
||||
* as different hues, not different intensities of the same one. */
|
||||
--c-weekend-bg: #f0f0f0; /* plain Saturday — read directly by red-days.spec.ts */
|
||||
--c-weekend-border: #cfcfcf;
|
||||
--c-weekend-hover: #e8e8e8;
|
||||
--c-redday-border: #d9776e;
|
||||
--c-redday-hover: #fbdedb;
|
||||
--c-klamdag-bg: #fef8e7;
|
||||
--c-klamdag-border: #d9b23c;
|
||||
--c-klamdag-hover: #fcf0c8;
|
||||
/* The one deliberately loud color in the app — a "this just changed"
|
||||
* flash ring on a calendar band, never a static/resting state. */
|
||||
--c-flash: #ffd54f;
|
||||
/* Neutrals */
|
||||
--c-bg: #fcfcfd;
|
||||
--c-surface: #fff;
|
||||
--c-border: #ececf0;
|
||||
--c-text: #111318;
|
||||
--c-text-muted: #5e6472;
|
||||
--c-text-faint: #9297a3;
|
||||
|
||||
--overlay: rgba(16, 24, 40, 0.5);
|
||||
/* The one accent — CTA, focus, active nav, today, selection */
|
||||
--c-accent: #2f5bea;
|
||||
--c-accent-weak: #eef2fe;
|
||||
--c-accent-hover: #2549c4;
|
||||
--c-accent-active: #1d3ca3;
|
||||
|
||||
/* Translucent tints used only for subtle hover/shadow/scrim effects — kept
|
||||
* as tokens (rather than inlined rgba() elsewhere) because the style
|
||||
* guard test (styles.test.ts) forbids rgba() outside this file. */
|
||||
--c-row-hover: rgba(47, 111, 79, 0.02);
|
||||
--c-accent-tint: rgba(47, 111, 79, 0.04);
|
||||
--c-accent-shadow: rgba(47, 111, 79, 0.12);
|
||||
--c-scrim-strong: rgba(0, 0, 0, 0.4);
|
||||
--c-band-shadow: rgba(0, 0, 0, 0.1);
|
||||
--c-label-text-shadow: rgba(0, 0, 0, 0.35);
|
||||
--c-fill-light: rgba(255, 255, 255, 0.35);
|
||||
--c-badge-fill: rgba(255, 255, 255, 0.3);
|
||||
--c-scrim-soft: rgba(0, 0, 0, 0.2);
|
||||
/* Semantic */
|
||||
--c-danger: #d03a2f;
|
||||
--c-danger-weak: #fbeceb;
|
||||
--c-danger-hover: #b02f26;
|
||||
--c-warning: #b26a00;
|
||||
--c-warning-weak: #fdf3e3;
|
||||
--c-success: var(--c-accent);
|
||||
--c-success-weak: var(--c-accent-weak);
|
||||
--c-success-hover: var(--c-accent-hover);
|
||||
|
||||
/* Spacing scale (8px base) */
|
||||
/* Day states — a colour on the numeral, never a cell fill */
|
||||
--c-day-red: var(--c-danger);
|
||||
--c-day-klamdag: var(--c-warning);
|
||||
--c-day-saturday: var(--c-text-faint);
|
||||
--c-weekend-bg: var(--c-surface); /* legacy name; Saturday has no fill */
|
||||
|
||||
/* Leave types: fill / ink / outline */
|
||||
--c-leave-vacation: #eef2fe;
|
||||
--c-leave-vacation-ink: #2f5bea;
|
||||
--c-leave-sjukpenning: #2a2d35;
|
||||
--c-leave-sjukpenning-ink: #fff;
|
||||
--c-leave-lagsta: #fff;
|
||||
--c-leave-lagsta-ink: #2a2d35;
|
||||
--c-leave-lagsta-outline: #2a2d35;
|
||||
--c-leave-lagsta-hatch: rgba(42, 45, 53, 0.35);
|
||||
--c-band-gap: #f1f2f5;
|
||||
--c-band-cap-self: var(--c-accent);
|
||||
--c-band-cap-partner: #9297a3;
|
||||
|
||||
/* Interaction chrome */
|
||||
--c-flash: var(--c-accent);
|
||||
--overlay: rgba(17, 19, 24, 0.4);
|
||||
--c-focus-ring: rgba(47, 91, 234, 0.35);
|
||||
--c-row-hover: rgba(47, 91, 234, 0.04);
|
||||
|
||||
/* Spacing */
|
||||
--sp-1: 4px;
|
||||
--sp-2: 8px;
|
||||
--sp-3: 12px;
|
||||
--sp-4: 16px;
|
||||
--sp-5: 24px;
|
||||
--sp-6: 32px;
|
||||
--sp-8: 48px;
|
||||
--sp-7: 48px;
|
||||
--sp-8: 64px;
|
||||
|
||||
/* Type scale */
|
||||
--fs-5xs: 0.5rem;
|
||||
--fs-4xs: 0.5625rem;
|
||||
--fs-2xs: 0.6875rem;
|
||||
/* Type scale 12/14/16/20/28/40 — nothing below 12px anywhere */
|
||||
--fs-xs: 0.75rem;
|
||||
--fs-sm: 0.875rem;
|
||||
--fs-base: 1rem;
|
||||
--fs-lg: 1.25rem;
|
||||
--fs-xl: 1.5rem;
|
||||
--fs-2xl: 2rem;
|
||||
--fs-xl: 1.75rem;
|
||||
--fs-2xl: 2.5rem;
|
||||
--lh: 1.5;
|
||||
--lh-tight: 1.15;
|
||||
|
||||
/* Shape & depth */
|
||||
--radius-sm: 6px;
|
||||
--radius-sm: 4px;
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-full: 999px;
|
||||
/* Matches frontend/e2e/tests/calendar-band.spec.ts's literal 4px/0px
|
||||
* assertions on band corners — kept as its own token (not tied to
|
||||
* --radius-sm) so a future radius change there can't silently break it. */
|
||||
* assertions on band corners — kept as its own token so a future radius
|
||||
* change elsewhere can't silently break it. */
|
||||
--radius-band: 4px;
|
||||
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
|
||||
--shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
|
||||
--shadow-float: 0 8px 24px rgba(17, 19, 24, 0.1);
|
||||
|
||||
/* Content width */
|
||||
/* Sizes */
|
||||
--maxw: 960px;
|
||||
|
||||
/* Interaction chrome shared by every focusable control */
|
||||
--c-focus-ring: rgba(47, 111, 79, 0.32);
|
||||
--fs-3xs: 0.625rem;
|
||||
--control-h: 40px;
|
||||
--control-h-sm: 32px;
|
||||
--icon-sm: 16px;
|
||||
--icon-md: 20px;
|
||||
--icon-lg: 24px;
|
||||
|
||||
/* Icon glyph sizes (spinner, etc.) — a separate scale from --fs-* since
|
||||
* these size a single glyph, not running text. */
|
||||
--icon-lg: 48px;
|
||||
--icon-md: 36px;
|
||||
/* Layers */
|
||||
--z-header: 100;
|
||||
--z-popover: 900;
|
||||
--z-modal: 1000;
|
||||
|
||||
/* The one inline image in the system: a 1.5px chevron for native selects.
|
||||
* Lives here (not in a feature file) so the style guard can keep `url(`
|
||||
* and hex out of everything else. Colour = --c-text-muted. */
|
||||
--icon-chevron-down: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 8"><path d="M1 1l5 5 5-5" stroke="%235e6472" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>');
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
@@ -124,8 +156,6 @@ html,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
@@ -138,9 +168,7 @@ blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
@@ -153,16 +181,12 @@ q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
@@ -183,9 +207,7 @@ th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figure,
|
||||
figcaption,
|
||||
footer,
|
||||
@@ -193,49 +215,54 @@ header,
|
||||
hgroup,
|
||||
main,
|
||||
mark,
|
||||
meter,
|
||||
nav,
|
||||
output,
|
||||
progress,
|
||||
section,
|
||||
summary,
|
||||
time,
|
||||
video {
|
||||
time {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--c-bg);
|
||||
color: var(--c-text);
|
||||
line-height: var(--lh);
|
||||
font-size: var(--fs-base);
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/* Headings — tighter leading than body text, the usual editorial cue that a
|
||||
* line is a heading rather than another sentence of the paragraph below it. */
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--fs-base);
|
||||
line-height: var(--lh);
|
||||
background: var(--c-bg);
|
||||
color: var(--c-text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* Headings — tighter leading and a touch of negative tracking at 28px+ is
|
||||
* what makes a line read as a heading, not a heavier weight. */
|
||||
h1 {
|
||||
font-size: var(--fs-2xl);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.01em;
|
||||
font-size: var(--fs-xl);
|
||||
font-weight: 600;
|
||||
line-height: var(--lh-tight);
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: var(--sp-4);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--fs-xl);
|
||||
font-weight: 700;
|
||||
font-size: var(--fs-lg);
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: var(--sp-3);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--fs-lg);
|
||||
font-size: var(--fs-base);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin-bottom: var(--sp-3);
|
||||
margin-bottom: var(--sp-2);
|
||||
}
|
||||
|
||||
/* Text */
|
||||
@@ -297,10 +324,10 @@ button:disabled {
|
||||
|
||||
/* Inputs & Selects
|
||||
*
|
||||
* Matches `.field__control` in primitives.css (same border/radius/padding/
|
||||
* focus formula) so an unclassed input still looks right. Checkboxes and
|
||||
* Matches `.field__control` in primitives.css (hairline, 40px, 2px accent
|
||||
* ring on focus) so an unclassed input still looks right. Checkboxes and
|
||||
* radios are excluded — they keep their native intrinsic size and just pick
|
||||
* up the accent color, not the text-field chrome. */
|
||||
* up the accent color. */
|
||||
input:not([type='checkbox']):not([type='radio']),
|
||||
select,
|
||||
textarea {
|
||||
@@ -308,12 +335,23 @@ textarea {
|
||||
min-height: var(--control-h);
|
||||
font-family: inherit;
|
||||
font-size: var(--fs-base);
|
||||
line-height: 1.25;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
background: var(--c-surface);
|
||||
color: var(--c-text);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
padding-right: var(--sp-6);
|
||||
background-image: var(--icon-chevron-down);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right var(--sp-3) center;
|
||||
background-size: 12px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
@@ -321,26 +359,28 @@ select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--c-accent);
|
||||
box-shadow: 0 0 0 3px var(--c-focus-ring);
|
||||
box-shadow: 0 0 0 2px var(--c-accent);
|
||||
}
|
||||
|
||||
/* Same crisp keyboard-focus outline as `.field__control:focus-visible`
|
||||
* (primitives.css) for an unclassed input/select/textarea — see that rule's
|
||||
* comment. */
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 2px solid var(--c-accent);
|
||||
outline-offset: 1px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:disabled,
|
||||
select:disabled,
|
||||
textarea:disabled {
|
||||
background-color: var(--c-bg);
|
||||
color: var(--c-text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--c-text-faint);
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
width: auto;
|
||||
@@ -348,11 +388,8 @@ input[type='radio'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Number inputs — hide the native up/down spinner everywhere. It doubles
|
||||
* the field's effective click target with no matching `.field__control`
|
||||
* affordance and its width isn't accounted for by any of this app's `ch`-
|
||||
* based sizing. Callers still get numeric-only mobile keyboards via
|
||||
* `inputmode="numeric"` on the element itself. */
|
||||
/* Number inputs — hide the native up/down spinner everywhere. Callers still
|
||||
* get numeric-only mobile keyboards via `inputmode="numeric"`. */
|
||||
input[type='number'] {
|
||||
appearance: textfield;
|
||||
}
|
||||
@@ -363,9 +400,6 @@ input[type='number']::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Date inputs — same height/padding/font as `.field__control` by default
|
||||
* (the shared `input:not([type=checkbox]):not([type=radio])` rule above
|
||||
* already covers it), just with a visibly clickable calendar affordance. */
|
||||
input[type='date']::-webkit-calendar-picker-indicator {
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
@@ -379,7 +413,7 @@ label {
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
/* Tables — no header fill; one hairline per row. */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -387,15 +421,15 @@ table {
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
background-color: var(--c-bg);
|
||||
font-weight: 500;
|
||||
font-size: var(--fs-sm);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
color: var(--c-text-muted);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
padding: var(--sp-3) var(--sp-3);
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user