Files
familienarchiv/frontend/src/routes/layout.css
marcel e8d1835ae1
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
feat(nav): add tooltip and cursor:pointer to notification bell, fix ThemeToggle i18n (#344) (#351)
Closes #344

## What was implemented

### Commit 1 — `feat(nav): add cursor-pointer and tooltip to notification bell`
- Extracted `bellLabel` as `$derived` in `NotificationBell.svelte` — eliminates the duplicated inline ternary and keeps tooltip/label in sync reactively
- Added `title={bellLabel}` to the bell `<button>` — native tooltip mirrors `aria-label` in both zero and non-zero unread states
- Added `cursor-pointer` to the bell button's class list
- Added global `button { cursor: pointer; }` rule in `@layer base` of `layout.css` — prevents future regressions (global scope per Decision Queue)
- Added 3 component tests in `NotificationBell.svelte.spec.ts`: cursor-pointer class present, title equals aria-label when unread=0, title equals aria-label when unread=3

### Commit 2 — `fix(nav): replace hardcoded ThemeToggle title with Paraglide i18n keys`
- Added `theme_toggle_to_light` / `theme_toggle_to_dark` keys to `de/en/es` messages
- Extracted `themeLabel` as `$derived` in `ThemeToggle.svelte` and bound both `aria-label` and `title` to it
- Fixes the pre-existing hardcoded English strings (`'light mode'` / `'dark mode'`) per Decision Queue resolution

Touch target size was descoped per the Decision Queue.

## Decision Queue resolutions (from issue #344)
- **cursor-pointer scope**: global via `@layer base` 
- **ThemeToggle scope**: fixed in this issue 
- **Touch target**: descoped 

## Test results
All 5 `NotificationBell` tests pass.

Co-authored-by: Marcel <marcel@familienarchiv>
Reviewed-on: http://heim-nas:3005/marcel/familienarchiv/pulls/351
2026-04-26 21:45:40 +02:00

395 lines
12 KiB
CSS

/* ─── 1. Fonts & Tailwind ──────────────────────────────────────────────────── */
/* Tinos = Times substitute | Montserrat = Gotham substitute (De Gruyter Brill CI) */
@import url('https://fonts.googleapis.com/css2?family=Tinos:ital,wght@0,400;0,700;1,400;1,700&family=Montserrat:wght@400;500;600;700&display=swap');
@import 'tailwindcss';
/* ─── 2. Raw palette — never used directly in components ──────────────────── */
@theme {
/* Breakpoints */
--breakpoint-xs: 375px;
/* Brand palette constants */
--palette-navy: #012851;
--palette-mint: #a1dcd8;
--palette-turquoise: #00c7b1;
--palette-sand: #f0efe9;
/* Typography */
--font-sans: 'Montserrat', ui-sans-serif, system-ui, sans-serif;
--font-serif: 'Tinos', 'Times New Roman', Georgia, serif;
--text-huge: 4rem;
}
/* ─── 3. Semantic tokens — Tailwind utilities backed by CSS variables ──────── */
/*
@theme inline makes Tailwind generate utility classes (bg-surface, text-ink,
border-line, etc.) whose values are CSS custom properties, not hardcoded hex.
Changing --c-surface on :root is all it takes to retheme the whole UI.
*/
@theme inline {
/* Surfaces */
--color-canvas: var(--c-canvas);
--color-surface: var(--c-surface);
--color-overlay: var(--c-overlay);
--color-muted: var(--c-muted);
/* Borders */
--color-line: var(--c-line);
--color-line-2: var(--c-line-2);
/* Text */
--color-ink: var(--c-ink);
--color-ink-2: var(--c-ink-2);
--color-ink-3: var(--c-ink-3);
/* Accent (mint ↔ turquoise) */
--color-accent: var(--c-accent);
--color-accent-bg: var(--c-accent-bg);
/* Primary interactive (navy ↔ mint) */
--color-primary: var(--c-primary);
--color-primary-fg: var(--c-primary-fg);
/* PDF viewer */
--color-pdf-bg: var(--c-pdf-bg);
--color-pdf-ctrl: var(--c-pdf-ctrl);
--color-pdf-text: var(--c-pdf-text);
/* Header surface — independent from canvas/surface for per-mode control */
--color-header: var(--c-header);
/* Turquoise — transcription mode accent */
--color-turquoise: var(--c-turquoise);
--color-turquoise-fg: var(--c-turquoise-fg);
/* Focus ring — keyboard focus indicator, mode-aware (navy in light, mint in dark) */
--color-focus-ring: var(--c-focus-ring);
/* Parchment — warm background for code/example blocks inside cards */
--color-parchment: var(--c-parchment);
/* Danger — destructive action color */
--color-danger: var(--c-danger);
--color-danger-fg: var(--c-danger-fg);
/* Warning — amber, WCAG AA on white */
--color-warning: #b45309;
--color-warning-fg: #ffffff;
/* Static brand tokens (not themed) */
--color-brand-navy: var(--palette-navy);
--color-brand-mint: var(--palette-mint);
}
/* ─── 4. Light mode (default) ─────────────────────────────────────────────── */
:root {
/* 1px accent bar + 64px nav = 65px total sticky header height */
--header-height: 65px;
--c-canvas: #f0efe9;
--c-surface: #ffffff;
--c-overlay: #ffffff;
--c-muted: #f5f4ef;
--c-line: #e4e2d7;
--c-line-2: #eeede8;
--c-ink: #012851;
--c-ink-2: #4b5563; /* gray-600 — 7.6:1 on white, 6.6:1 on canvas — WCAG AA ✓ */
--c-ink-3: #6b7280; /* gray-500 — 4.8:1 on white, ~4.6:1 on canvas — WCAG AA ✓ */
/* ⚠ accent — decorative use only (borders, icon tints, bg fills)
Light mode: #a1dcd8 on white = 1.52:1 — WCAG FAIL. Never use as text colour.
For interactive text labels use text-primary or text-ink instead. */
--c-accent: #a1dcd8;
--c-accent-bg: rgba(161, 220, 216, 0.15);
--c-primary: #012851;
--c-primary-fg: #ffffff;
/* Header is brand-navy in light mode; same in dark mode for contrast compliance */
--c-header: #012851;
--c-turquoise: #00c7b1;
--c-turquoise-fg: #ffffff;
/* Focus ring: brand-navy in light mode — 14:1 on white, ~11:1 on sand */
--c-focus-ring: #012851;
--c-pdf-bg: #ebebeb;
--c-pdf-ctrl: #d8d8d8;
--c-pdf-text: #333333;
/* Danger — destructive actions (5.1:1 on white — WCAG AA ✓) */
--c-danger: #c0392b;
--c-danger-fg: #ffffff;
/* Parchment — warm near-white for example blocks (light mode: cream #FAF8F1) */
--c-parchment: #faf8f1;
/* Tag color tokens — decorative dot colors on tag chips */
--c-tag-sage: #5a8a6a;
--c-tag-sienna: #a0522d;
--c-tag-amber: #c17a00;
--c-tag-slate: #607080;
--c-tag-violet: #7a4f9a;
--c-tag-rose: #c0446e;
--c-tag-cobalt: #3060b0;
--c-tag-moss: #4a7a3a;
--c-tag-sand: #9a8040;
--c-tag-coral: #c05540;
/* PersonType badge — institution (navy-tinted blue) */
--c-badge-institution-bg: #e8eff7;
--c-badge-institution-text: #1a4971;
--c-badge-institution-border: #c4d5e8;
/* PersonType badge — group (muted purple) */
--c-badge-group-bg: #f0e8f5;
--c-badge-group-text: #5a2d6f;
--c-badge-group-border: #d8c5e3;
/* PersonType badge — unknown (amber warning) */
--c-badge-unknown-bg: #fdf4e3;
--c-badge-unknown-text: #7a5a0a;
--c-badge-unknown-border: #f0ddb3;
}
/* ─── 5. Dark mode ─────────────────────────────────────────────────────────── */
/*
Navy-tinted dark palette derived from brand-navy (#012851).
KEEP THESE TWO BLOCKS IN SYNC — they cover the same design intent via
different activation paths (system preference vs. manual toggle).
*/
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
color-scheme: dark;
--c-canvas: #010e1e;
--c-surface: #011526;
--c-overlay: #011e38;
--c-muted: #011a30;
--c-line: #0d3358;
--c-line-2: #092843;
--c-ink: #f0efe9;
--c-ink-2: #9ca3af; /* 7.5:1 on #011526 — WCAG AAA ✓ */
--c-ink-3: #8b97a5; /* 7.1:1 on #011526 — WCAG AAA ✓ */
--c-accent: #00c7b1;
--c-accent-bg: rgba(0, 199, 177, 0.12);
--c-primary: #a1dcd8;
--c-primary-fg: #012851;
/* Header at brand-navy: 4.99:1 with ink-3 (WCAG AA ✓), visually above canvas */
--c-header: #012851;
--c-turquoise: #00c7b1;
--c-turquoise-fg: #012851;
/* Focus ring: brand-mint in dark mode — 9.2:1 on canvas, 7.1:1 on surface */
--c-focus-ring: #a1dcd8;
--c-pdf-bg: #010e1e;
--c-pdf-ctrl: #011526;
--c-pdf-text: #f0efe9;
--c-badge-institution-bg: rgba(30, 80, 140, 0.25);
--c-badge-institution-text: #8bb8e0;
--c-badge-institution-border: rgba(30, 80, 140, 0.4);
--c-badge-group-bg: rgba(90, 45, 111, 0.25);
--c-badge-group-text: #c9a0dc;
--c-badge-group-border: rgba(90, 45, 111, 0.4);
--c-badge-unknown-bg: rgba(122, 90, 10, 0.25);
--c-badge-unknown-text: #e0c060;
--c-badge-unknown-border: rgba(122, 90, 10, 0.4);
/* Danger — destructive actions (4.7:1 on #011526 — WCAG AA ✓) */
--c-danger: #e55347;
--c-danger-fg: #ffffff;
/* Parchment — subtle surface shift for example blocks on dark navy */
--c-parchment: #041828;
/* Tag color tokens — lighter for visibility on dark backgrounds */
--c-tag-sage: #7abf8a;
--c-tag-sienna: #cc7050;
--c-tag-amber: #f0aa30;
--c-tag-slate: #8a9db0;
--c-tag-violet: #b07ad0;
--c-tag-rose: #f07090;
--c-tag-cobalt: #6090e0;
--c-tag-moss: #70b060;
--c-tag-sand: #c0a060;
--c-tag-coral: #f07060;
}
}
/* Manual dark override — takes precedence over media query */
/* KEEP IN SYNC with the @media block above */
:root[data-theme='dark'] {
color-scheme: dark;
--c-canvas: #010e1e;
--c-surface: #011526;
--c-overlay: #011e38;
--c-muted: #011a30;
--c-line: #0d3358;
--c-line-2: #092843;
--c-ink: #f0efe9;
--c-ink-2: #9ca3af; /* 7.5:1 on #011526 — WCAG AAA ✓ */
--c-ink-3: #8b97a5; /* 7.1:1 on #011526 — WCAG AAA ✓ */
--c-accent: #00c7b1;
--c-accent-bg: rgba(0, 199, 177, 0.12);
--c-primary: #a1dcd8;
--c-primary-fg: #012851;
/* Header at brand-navy: 4.99:1 with ink-3 (WCAG AA ✓), visually above canvas */
--c-header: #012851;
--c-turquoise: #00c7b1;
--c-turquoise-fg: #012851;
/* Focus ring: brand-mint in dark mode — 9.2:1 on canvas, 7.1:1 on surface */
--c-focus-ring: #a1dcd8;
--c-pdf-bg: #010e1e;
--c-pdf-ctrl: #011526;
--c-pdf-text: #f0efe9;
--c-badge-institution-bg: rgba(30, 80, 140, 0.25);
--c-badge-institution-text: #8bb8e0;
--c-badge-institution-border: rgba(30, 80, 140, 0.4);
--c-badge-group-bg: rgba(90, 45, 111, 0.25);
--c-badge-group-text: #c9a0dc;
--c-badge-group-border: rgba(90, 45, 111, 0.4);
--c-badge-unknown-bg: rgba(122, 90, 10, 0.25);
--c-badge-unknown-text: #e0c060;
--c-badge-unknown-border: rgba(122, 90, 10, 0.4);
/* Danger — destructive actions (4.7:1 on #011526 — WCAG AA ✓) */
--c-danger: #e55347;
--c-danger-fg: #ffffff;
/* Parchment — subtle surface shift for example blocks on dark navy */
--c-parchment: #041828;
/* Tag color tokens — lighter for visibility on dark backgrounds */
--c-tag-sage: #7abf8a;
--c-tag-sienna: #cc7050;
--c-tag-amber: #f0aa30;
--c-tag-slate: #8a9db0;
--c-tag-violet: #b07ad0;
--c-tag-rose: #f07090;
--c-tag-cobalt: #6090e0;
--c-tag-moss: #70b060;
--c-tag-sand: #c0a060;
--c-tag-coral: #f07060;
}
/* ─── 6. Icon inversion — De Gruyter icons are black SVGs loaded as <img> ──── */
/*
In dark mode, invert all brand icons so they read as white on dark surfaces.
Exclude .invert icons (already inverted for placement on dark backgrounds)
so they don't get double-inverted back to black.
*/
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) img[src*='degruyter-icons']:not(.invert) {
filter: invert(1);
}
}
:root[data-theme='dark'] img[src*='degruyter-icons']:not(.invert) {
filter: invert(1);
}
/* ─── 7. @mention chip ─────────────────────────────────────────────────────── */
/*
Rendered by renderBody() via {@html ...} in CommentThread.svelte.
Must live in global CSS — Svelte scoped styles don't reach injected HTML.
*/
.mention {
display: inline;
color: var(--c-ink);
background-color: var(--c-accent-bg);
border-radius: 3px;
padding: 0 3px;
font-weight: 600;
font-style: normal;
cursor: default;
transition: background-color 0.15s ease;
}
.mention:hover {
background-color: color-mix(in srgb, var(--c-accent) 25%, transparent);
}
/* ─── 8. Base styles ───────────────────────────────────────────────────────── */
@layer base {
html {
overscroll-behavior: none;
}
body {
background-color: var(--c-canvas);
color: var(--c-ink);
font-family: var(--font-serif);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-sans);
font-weight: 600;
}
/* Form controls — always use surface bg and ink text so they theme correctly */
input,
textarea,
select {
background-color: var(--c-surface);
color: var(--c-ink);
}
a:hover {
text-decoration-color: var(--c-accent);
text-decoration-thickness: 2px;
text-underline-offset: 4px;
}
/* Tailwind preflight resets cursor on *, overriding the browser default for buttons */
button {
cursor: pointer;
}
/* Fallback focus ring for any interactive element not styled with ring-focus-ring */
:focus-visible {
outline: 2px solid var(--c-focus-ring);
outline-offset: 2px;
}
}
/* Ensure focus rings are visible in Windows High Contrast / forced-colors mode */
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid ButtonText;
}
}
@keyframes slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(350%);
}
}