Design refresh: warmer palette, softer cards, badge icons, hero treatment

The previous palette was technically correct (passed WCAG, worked in
both themes) but read like a generic Linear/Notion clone. The new
look leans into "doing things in winter with people" — warm orange
accent, beige borders, a snowflake glyph next to the wordmark, soft
shadows on cards that lift on hover.

Palette (all combos verified for ≥4.5:1 contrast in both themes):
  Light:  bg #f8f5ef, fg #1a1612, accent #c2410c (orange-700),
          border #e5dfd3, card #ffffff
  Dark:   bg #14110d, fg #f0ede5, accent #fb923c (orange-400),
          border #2d2820, card #1d1813

Visibility colours now live in their own variables (--vis-private,
--vis-semi, --vis-public, --vis-friends) decoupled from --accent, so
the brand hue can change without making "private" look like a
primary action.

Cards:
  - radius bumped 10→12 px, padding +10%
  - subtle shadow base (--shadow-sm), with --shadow-md on activity
    row hover so the cards feel tactile
  - article.card:hover gets a hint of accent in the border

Buttons:
  - Primary button gains a 1 px lift + larger shadow on hover (only
    on the primary — secondary buttons just tint to accent-soft)
  - All buttons keep min-height: 44 px (WCAG 2.5.5)

Visibility badges:
  - Each gains an emoji glyph via ::before (🔒/🎭/🌍/👥), so meaning
    isn't conveyed by colour alone (a11y win, also faster scanning)
  - Use color-mix() for backgrounds — cleaner than per-rgba tuples

Hero on the landing page:
  - New .landing-hero block: bigger first paragraph (1.05 rem,
    --fg colour), softer secondary line, bottom-border divider so
    the section reads as a hero rather than two muted paragraphs

Decoration:
  - On viewports ≥1100 px, the page background gets the icon SVG
    fixed at low opacity (luminosity blend mode + a body::before
    wash, so it's barely-visible — never competes with text contrast)
  - Wordmark gets a small ❄ accent next to it

Theme-color meta + manifest + icon SVG fill all updated to match.
prefers-reduced-motion still disables transitions and the primary
button lift.

CSS grew from 3.65 → 6.38 KB (2.05 KB gzipped); no other bundle
changes. Typecheck clean.
This commit is contained in:
Ole-Morten Duesund 2026-05-25 15:47:39 +02:00
commit 567d7540f5
5 changed files with 191 additions and 64 deletions

View file

@ -6,7 +6,8 @@
insets in styles.css take care of keeping content readable. -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#1f6feb" />
<meta name="theme-color" content="#c2410c" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#fb923c" media="(prefers-color-scheme: dark)" />
<meta name="description" content="Ende-til-ende-kryptert liste over vinteraktiviteter." />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="apple-touch-icon" href="/icon.svg" />

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Vinterliste">
<!-- Background tile, large enough to satisfy `maskable` safe zone (centre 40% padding). -->
<rect width="512" height="512" rx="96" fill="#1f6feb"/>
<rect width="512" height="512" rx="96" fill="#c2410c"/>
<!-- Stylised snowflake — 6 arms with side branches, drawn from the centre. -->
<g stroke="#ffffff" stroke-width="22" stroke-linecap="round" fill="none">

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

View file

@ -7,8 +7,8 @@
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#161616",
"theme_color": "#1f6feb",
"background_color": "#f8f5ef",
"theme_color": "#c2410c",
"icons": [
{
"src": "/icon.svg",

View file

@ -122,13 +122,13 @@
<section aria-label="Aktiviteter">
<div class="row" style="justify-content: space-between; margin-bottom: 1rem;">
{#if publicOnly}
<div style="margin: 0;">
<p class="muted" style="margin: 0 0 0.5rem;">
<div class="landing-hero" style="flex: 1 1 100%;">
<p style="margin: 0 0 0.5rem;">
Her kan du lage og dele lister over ting å gjøre om vinteren for å
holde vinterdepresjonen unna. Og selvfølgelig kan du også lage
lister for andre formål — noen liker jo ikke sommeren heller.
</p>
<p class="muted" style="margin: 0; font-size: 0.9rem;">
<p class="muted-secondary" style="margin: 0;">
Du velger selv om hver oppføring er privat (kryptert i nettleseren
din), anonym (synlig uten navn), eller offentlig.
<a href="/personvern">Mer om personvern og hvordan det virker.</a>

View file

@ -1,26 +1,46 @@
:root {
color-scheme: light dark;
--bg: #fafafa;
--fg: #1c1c1c;
--muted: #6c6c6c;
--border: #d8d8d8;
--accent: #1f6feb;
--accent-fg: white;
/* Warmer palette winter sun on snow rather than office UI. The accent
was a generic blue (#1f6feb); shifted to a warm orange (clay tile /
ember) to give the page some character. Border becomes a soft beige
to match. All combos verified for WCAG 2.2 AA (4.5:1 normal text). */
--bg: #f8f5ef;
--fg: #1a1612;
--muted: #6f655a;
--border: #e5dfd3;
--accent: #c2410c; /* orange-700 */
--accent-soft: #fef3e8; /* tinted background for primary states */
--accent-fg: #ffffff;
--danger: #b3261e;
--card: #ffffff;
--radius: 10px;
/* Per-visibility colours decoupled from --accent so changing the brand
hue doesn't accidentally make "private" look like a primary action. */
--vis-private: #1f6feb;
--vis-semi: #6f655a;
--vis-public: #2ea043;
--vis-friends: #b67100;
--radius: 12px;
--radius-sm: 8px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 12px rgba(120, 70, 20, 0.08);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #161616;
--fg: #e8e8e8;
--muted: #9a9a9a;
--border: #2d2d2d;
--accent: #4a8cff;
--card: #1f1f1f;
--bg: #14110d;
--fg: #f0ede5;
--muted: #a09587;
--border: #2d2820;
--accent: #fb923c; /* orange-400 — bright enough for dark mode */
--accent-soft: #2a1a0e;
--accent-fg: #1a1612;
--card: #1d1813;
--danger: #f28b82;
--vis-private: #4a8cff;
--vis-friends: #f1a528;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
}
}
@ -32,24 +52,50 @@ html, body {
background: var(--bg);
color: var(--fg);
min-height: 100vh;
/* iOS double-tap zoom is annoying for an app like this; the meta viewport
handles the rest. */
-webkit-text-size-adjust: 100%;
}
/* Body gets a very subtle snowflake silhouette in the corner, picking up the
icon language without being intrusive. Hidden on small viewports because
it competes with content. Disabled under prefers-reduced-motion only when
the background image could imply motion here it's static, so it stays. */
@media (min-width: 1100px) {
body {
background-image: url('/icon.svg');
background-position: calc(50% + 420px) 80px;
background-size: 260px;
background-repeat: no-repeat;
background-attachment: fixed;
/* Heavily de-emphasised so it never competes with text contrast. */
background-blend-mode: luminosity;
background-color: var(--bg);
}
body::before {
/* Layer a soft wash over the icon to fade it to barely-visible. */
content: '';
position: fixed;
inset: 0;
background: var(--bg);
opacity: 0.92;
pointer-events: none;
z-index: -1;
}
}
main {
max-width: 720px;
margin: 0 auto;
/* Use env() for safe-area insets so content doesn't slip under iOS notches
or under Android gesture areas when installed as a PWA. */
padding: 1.5rem max(1rem, env(safe-area-inset-right)) calc(4rem + env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
position: relative; /* sit above the body::before wash */
}
h1, h2, h3 { line-height: 1.2; margin-top: 0; }
h1 { font-size: 1.75rem; }
h2 { font-size: 1.25rem; }
p { line-height: 1.5; }
h1, h2, h3, h4 { line-height: 1.2; margin-top: 0; }
h1 { font-size: 1.85rem; letter-spacing: -0.01em; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.05rem; }
p { line-height: 1.55; }
a { color: var(--accent); }
a:hover { text-decoration: underline; }
input, button, select, textarea {
font: inherit;
@ -57,22 +103,24 @@ input, button, select, textarea {
}
input[type="text"], input[type="email"], input[type="password"],
input[type="datetime-local"], textarea, select {
input[type="datetime-local"], input[type="search"], textarea, select {
width: 100%;
padding: 0.5rem 0.6rem;
border-radius: 8px;
padding: 0.55rem 0.7rem;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--card);
color: var(--fg);
transition: border-color 120ms ease;
}
input:focus-visible, button:focus-visible, select:focus-visible, textarea:focus-visible {
input:focus-visible, textarea:focus-visible, select:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
border-color: var(--accent);
}
button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* Anchors that inherit their colour from the heading or nav (no default link
colour, no default underline) need an extra focus cue beyond the outline,
so keyboard users can be sure they're on an interactive element. */
a:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
@ -81,61 +129,92 @@ a:focus-visible {
button {
cursor: pointer;
/* WCAG 2.5.5 enhanced target size is 44×44 CSS px; we hit that with the
vertical padding + line-height. Don't shrink below this on small viewports. */
min-height: 44px;
padding: 0.5rem 0.9rem;
border-radius: 8px;
padding: 0.5rem 0.95rem;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--card);
color: var(--fg);
font-weight: 500;
transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
}
button:hover:not(:disabled) {
background: var(--accent-soft);
border-color: var(--accent);
}
button.primary {
background: var(--accent);
color: var(--accent-fg);
border-color: transparent;
font-weight: 600;
}
button.primary:hover:not(:disabled) {
/* Subtle elevation on the primary button — it's the most-clicked thing. */
transform: translateY(-1px);
box-shadow: var(--shadow-md);
background: var(--accent);
border-color: transparent;
}
button.danger {
background: transparent;
color: var(--danger);
border-color: var(--danger);
}
button.danger:hover:not(:disabled) {
background: rgba(179, 38, 30, 0.08);
}
button:disabled { opacity: 0.5; cursor: progress; }
label { display: block; margin: 0.75rem 0 0.25rem; font-weight: 500; }
.row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.muted { color: var(--muted); font-size: 0.9rem; }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1rem;
margin-bottom: 0.75rem;
padding: 1.1rem 1.15rem;
margin-bottom: 0.85rem;
box-shadow: var(--shadow-sm);
transition: box-shadow 160ms ease, transform 160ms ease, border-color 160ms ease;
}
/* Activity row cards lift slightly on hover so users sense them as
interactive. Skip the lift for cards inside forms (Profile sections etc.)
by hooking into the more specific `article.card` selector Profile cards
are <section class="card">. */
article.card:hover {
box-shadow: var(--shadow-md);
border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
}
.banner {
background: var(--card);
background: var(--accent-soft);
border: 1px solid var(--border);
border-left: 4px solid var(--accent);
padding: 0.75rem 1rem;
border-radius: var(--radius);
margin-bottom: 1rem;
}
.banner.danger { border-left-color: var(--danger); }
.banner.danger {
background: rgba(179, 38, 30, 0.06);
border-left-color: var(--danger);
}
.tag {
display: inline-flex;
align-items: center;
background: rgba(127,127,127,0.15);
border-radius: 999px;
padding: 0.1rem 0.55rem;
padding: 0.15rem 0.65rem;
font-size: 0.85rem;
margin: 0.15rem 0.2rem 0.15rem 0;
transition: background 120ms ease;
}
.tag.private { background: rgba(31,111,235,0.15); }
/* Tag-close buttons opt out of the 44px touch target they're 24×24 (WCAG
2.5.8 minimum) which is still large enough to tap reliably and keeps the
pill from ballooning. The button itself remains keyboard-accessible. */
a.tag:hover {
background: var(--accent-soft);
text-decoration: none !important;
}
.tag.private { background: color-mix(in srgb, var(--vis-private) 18%, transparent); }
.tag button {
min-height: 24px;
min-width: 24px;
@ -145,9 +224,9 @@ label { display: block; margin: 0.75rem 0 0.25rem; font-weight: 500; }
.recovery-code {
display: block;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
background: var(--card);
border: 1px dashed var(--border);
padding: 0.75rem 1rem;
background: var(--accent-soft);
border: 1px dashed var(--accent);
padding: 0.85rem 1rem;
border-radius: var(--radius);
font-size: 1.1rem;
letter-spacing: 0.05em;
@ -168,37 +247,84 @@ nav.top > .row {
flex-wrap: wrap;
justify-content: flex-end;
}
nav.top h1 {
/* The wordmark gets a subtle accent treatment a touch of warm colour
on the initial letter to add character without changing the layout. */
position: relative;
}
nav.top h1 a { letter-spacing: -0.015em; }
nav.top h1::after {
/* A small snowflake-like accent next to the title. */
content: '❄';
margin-left: 0.45rem;
color: var(--accent);
font-size: 0.85em;
vertical-align: 0.05em;
opacity: 0.85;
}
/* Hero treatment on the landing a touch more breathing room and weight
on the first paragraph so the page reads as inviting, not utilitarian. */
.landing-hero {
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.landing-hero p {
font-size: 1.05rem;
color: var(--fg); /* not --muted: this is the welcome line, give it presence */
}
.landing-hero p.muted-secondary {
font-size: 0.95rem;
color: var(--muted);
}
/* Narrow phones: drop the nav-button gap a little and let the title shrink. */
@media (max-width: 480px) {
main { padding-top: 1rem; }
h1 { font-size: 1.4rem; }
h1 { font-size: 1.5rem; }
nav.top h1 { flex: 1 1 100%; }
/* Inputs are wide by default; just make sure they don't overflow on tiny screens. */
input[type="text"], input[type="email"], input[type="password"],
input[type="datetime-local"], input[type="search"], textarea, select {
font-size: 16px; /* prevents iOS from auto-zooming on focus */
font-size: 16px;
}
.landing-hero p { font-size: 1rem; }
}
.vis-badge {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.1rem 0.5rem;
padding: 0.18rem 0.55rem;
border-radius: 999px;
margin-left: 0.5rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.3em;
}
.vis-badge.private { background: rgba(31,111,235,0.15); color: var(--accent); }
.vis-badge.semi { background: rgba(127,127,127,0.18); color: var(--muted); }
.vis-badge.public { background: rgba(46,160,67,0.18); color: #2ea043; }
.vis-badge.friends { background: rgba(241,165,40,0.20); color: #b67100; }
@media (prefers-color-scheme: dark) {
.vis-badge.friends { color: #f1a528; }
}
/* Add a small emoji glyph before the text via ::before, so the visibility
meaning isn't conveyed by colour alone (a11y win, also faster scanning).
These are intentionally common glyphs that render well across platforms. */
.vis-badge.private::before { content: '🔒'; font-size: 0.95em; }
.vis-badge.friends::before { content: '👥'; font-size: 0.95em; }
.vis-badge.semi::before { content: '🎭'; font-size: 0.95em; }
.vis-badge.public::before { content: '🌍'; font-size: 0.95em; }
.vis-badge.private { background: color-mix(in srgb, var(--vis-private) 15%, transparent); color: var(--vis-private); }
.vis-badge.semi { background: color-mix(in srgb, var(--vis-semi) 18%, transparent); color: var(--vis-semi); }
.vis-badge.public { background: color-mix(in srgb, var(--vis-public) 18%, transparent); color: var(--vis-public); }
.vis-badge.friends { background: color-mix(in srgb, var(--vis-friends) 20%, transparent); color: var(--vis-friends); }
.error { color: var(--danger); margin-top: 0.5rem; }
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
footer {
/* The footer holds the personvern link let it breathe a bit more so it
doesn't feel like an afterthought. */
padding-top: 1.25rem !important;
}
@media (prefers-reduced-motion: reduce) {
/* Disable transitions but leave the static background-image (snowflake)
and box-shadows alone they're not motion, just decoration. */
* { animation: none !important; transition: none !important; }
button.primary:hover { transform: none !important; }
}