diff --git a/frontend/auth/admin/index.html b/frontend/auth/admin/index.html
index 1901170..937350c 100644
--- a/frontend/auth/admin/index.html
+++ b/frontend/auth/admin/index.html
@@ -3,7 +3,8 @@
- Admin
+ Admin
+
diff --git a/frontend/auth/index.html b/frontend/auth/index.html
index ca11fef..057f33b 100644
--- a/frontend/auth/index.html
+++ b/frontend/auth/index.html
@@ -4,6 +4,7 @@
Auth Profile
+
diff --git a/frontend/auth/restricted/index.html b/frontend/auth/restricted/index.html
index a7d03bf..defa255 100644
--- a/frontend/auth/restricted/index.html
+++ b/frontend/auth/restricted/index.html
@@ -3,6 +3,7 @@
+
diff --git a/frontend/auth/restricted/theme.js b/frontend/auth/restricted/theme.js
index 7308ce1..78858e6 100644
--- a/frontend/auth/restricted/theme.js
+++ b/frontend/auth/restricted/theme.js
@@ -1,11 +1,11 @@
// Early theme for restricted app - first URL param wins, then localStorage
-import { themeColors, applyTheme, getCachedTheme } from '@/utils/theme.js'
+import { applyTheme, getCachedTheme } from '@/utils/theme.js'
function getTheme() {
const params = new URLSearchParams(location.hash.slice(1))
return params.get('theme') || getCachedTheme() || ''
}
-// Use .surface selector to preserve transparent background
-applyTheme(getTheme(), '.surface')
-addEventListener('hashchange', () => applyTheme(getTheme(), '.surface'))
+// Apply theme class to document root
+applyTheme(getTheme())
+addEventListener('hashchange', () => applyTheme(getTheme()))
diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css
index f387ea1..7701bc3 100644
--- a/frontend/src/assets/style.css
+++ b/frontend/src/assets/style.css
@@ -1,28 +1,37 @@
+@property --hue {
+ syntax: '';
+ inherits: true;
+ initial-value: 0.72turn;
+}
+
:root {
--font-sans: "Inter", "Inter var", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
--font-mono: "DM Mono", "JetBrains Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
- --color-canvas: #ffffff;
- --color-surface: #eff6ff;
- --color-surface-subtle: #dbeafe;
- --color-border: #2563eb;
- --color-border-strong: #1e40af;
- --color-heading: #1e3a8a;
- --color-text: #1e293b;
- --color-text-muted: #475569;
- --color-link: #1d4ed8;
- --color-link-hover: #1e40af;
- --color-accent: #2563eb;
- --color-accent-strong: #1e40af;
- --color-accent-contrast: #ffffff;
- --color-success-text: #166534;
- --color-success-bg: #dcfce7;
- --color-error-text: #b91c1c;
- --color-error-bg: #fee2e2;
- --color-info-text: #1e40af;
- --color-info-bg: #dbeafe;
- --color-danger: #dc2626;
- --shadow-soft: 0 10px 30px rgba(30, 64, 175, 0.15);
+ --color-canvas: white;
+ --color-surface: oklch(0.97 0.005 var(--hue));
+ --color-surface-subtle: oklch(0.94 0.01 var(--hue));
+ --color-dialog: white;
+ --color-border: oklch(0.8 0.02 var(--hue));
+ --color-border-strong: oklch(0.55 0.15 var(--hue));
+ --color-heading: oklch(0.3 0.05 var(--hue));
+ --color-text: oklch(0.25 0.02 var(--hue));
+ --color-text-muted: oklch(0.5 0.02 var(--hue));
+ --color-link: oklch(0.5 0.18 var(--hue));
+ --color-link-hover: oklch(0.45 0.2 var(--hue));
+ --color-accent: oklch(0.55 0.2 var(--hue));
+ --color-accent-strong: oklch(0.45 0.2 var(--hue));
+ --color-accent-contrast: white;
+ --color-secondary: oklch(0.55 0.05 var(--hue));
+ --color-secondary-strong: oklch(0.45 0.05 var(--hue));
+ --color-success-text: oklch(0.4 0.15 0.4turn);
+ --color-success-bg: oklch(0.95 0.03 0.4turn);
+ --color-error-text: oklch(0.45 0.2 0.07turn);
+ --color-error-bg: oklch(0.95 0.03 0.07turn);
+ --color-info-text: oklch(0.45 0.15 var(--hue));
+ --color-info-bg: oklch(0.95 0.02 var(--hue));
+ --color-danger: oklch(0.55 0.22 0.07turn);
+ --shadow-soft: 0 0 .2rem black;
--radius-none: 0;
--radius-sm: 4px;
--radius-md: 6px;
@@ -39,30 +48,31 @@
--focus-ring: 0 0 0 2px var(--color-accent);
}
-@media (prefers-color-scheme: dark) {
- :root {
- --color-canvas: #0f172a;
- --color-surface: #141b2f;
- --color-surface-subtle: #1b243b;
- --color-border: #25304a;
- --color-border-strong: #3d4d6b;
- --color-heading: #fff;
- --color-text: #e2e8f0;
- --color-text-muted: #94a3b8;
- --color-link: #60a5fa;
- --color-link-hover: #93c5fd;
- --color-accent: #60a5fa;
- --color-accent-strong: #3b82f6;
- --color-accent-contrast: #0b1120;
- --color-success-text: #34d399;
- --color-success-bg: #1a4d2e;
- --color-error-text: #fca5a5;
- --color-error-bg: #4a1f1f;
- --color-info-text: #bae6fd;
- --color-info-bg: #1e3a5f;
- --color-danger: #f87171;
- --shadow-soft: 0 0 0 #000000;
- }
+:root.dark {
+ --color-canvas: oklch(0.15 0.03 var(--hue));
+ --color-surface: oklch(0.18 0.03 var(--hue));
+ --color-surface-subtle: oklch(0.22 0.03 var(--hue));
+ --color-dialog: oklch(0.22 0.03 var(--hue));
+ --color-border: oklch(0.3 0.03 var(--hue));
+ --color-border-strong: oklch(0.4 0.04 var(--hue));
+ --color-heading: white;
+ --color-text: oklch(0.9 0.01 var(--hue));
+ --color-text-muted: oklch(0.7 0.02 var(--hue));
+ --color-link: oklch(0.7 0.15 var(--hue));
+ --color-link-hover: oklch(0.8 0.12 var(--hue));
+ --color-accent: oklch(0.7 0.15 var(--hue));
+ --color-accent-strong: oklch(0.6 0.18 var(--hue));
+ --color-accent-contrast: oklch(0.12 0.03 var(--hue));
+ --color-secondary: oklch(0.6 0.05 var(--hue));
+ --color-secondary-strong: oklch(0.5 0.05 var(--hue));
+ --color-success-text: oklch(0.75 0.15 0.4turn);
+ --color-success-bg: oklch(0.3 0.08 0.4turn);
+ --color-error-text: oklch(0.8 0.12 0.07turn);
+ --color-error-bg: oklch(0.3 0.08 0.07turn);
+ --color-info-text: oklch(0.8 0.1 var(--hue));
+ --color-info-bg: oklch(0.3 0.05 var(--hue));
+ --color-danger: oklch(0.7 0.18 0.07turn);
+ --shadow-soft: 0 0 0 black;
}
*,
@@ -229,6 +239,7 @@ button {
gap: 0.4rem;
background: var(--color-surface);
color: var(--color-text);
+ transition: box-shadow var(--transition-base), background var(--transition-base), border-color var(--transition-base);
}
button:disabled {
@@ -237,43 +248,41 @@ button:disabled {
}
.btn-primary {
- background: var(--color-accent);
+ background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-accent);
color: var(--color-accent-contrast);
border-color: var(--color-accent);
+}
+
+.btn-primary:hover:not(:disabled),
+.btn-primary:focus-visible {
+ background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-accent-strong);
+ border-color: var(--color-accent-strong);
box-shadow: var(--shadow-soft);
}
-.btn-primary:hover:not(:disabled) {
- background: var(--color-accent-strong);
- border-color: var(--color-accent-strong);
-}
-
.btn-secondary {
- background: transparent;
- color: var(--color-text);
- border-color: var(--color-border);
-}
-
-.btn-secondary:hover:not(:disabled) {
- border-color: var(--color-border-strong);
- background: var(--color-surface-subtle);
-}
-
-.btn-danger {
- background: var(--color-danger);
+ background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-secondary);
color: var(--color-accent-contrast);
border-color: transparent;
}
-.btn-danger:hover:not(:disabled) {
- filter: brightness(0.92);
+.btn-secondary:hover:not(:disabled),
+.btn-secondary:focus-visible {
+ background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-secondary-strong);
+ box-shadow: var(--shadow-soft);
}
-/* Focus-visible outlines for buttons */
-.btn-primary:focus-visible,
-.btn-secondary:focus-visible,
+.btn-danger {
+ background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-danger);
+ color: var(--color-accent-contrast);
+ border-color: transparent;
+}
+
+.btn-danger:hover:not(:disabled),
.btn-danger:focus-visible {
- outline: 1px solid var(-webkit-focus-ring-color);
+ background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-danger);
+ filter: brightness(0.92);
+ box-shadow: var(--shadow-soft);
}
input[type="text"],
@@ -407,7 +416,7 @@ th {
.device-dialog,
.modal {
- background: var(--color-surface);
+ background: var(--color-dialog);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
width: min(520px, 100%);
@@ -429,7 +438,7 @@ th {
.qr-code {
padding: 1rem;
- background: #fff;
+ background: white;
box-shadow: var(--shadow-soft);
}
@@ -567,7 +576,7 @@ th {
}
.btn-card-delete { background: transparent; border: none; color: var(--color-danger); padding: 0.35rem 0.5rem; font-size: 1.05rem; line-height: 1; border-radius: var(--radius-sm); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
-.btn-card-delete:hover:not(:disabled) { background: #fee; }
+.btn-card-delete:hover:not(:disabled) { filter: brightness(0.85); }
.btn-card-delete:disabled { filter: opacity(0.4); cursor: not-allowed; }
@@ -692,8 +701,8 @@ th {
width: 100vw;
height: 100vh;
background: transparent;
- backdrop-filter: blur(.1rem) brightness(0.7);
- -webkit-backdrop-filter: blur(.1rem) brightness(0.7);
+ backdrop-filter: blur(.2rem) brightness(0.7);
+ -webkit-backdrop-filter: blur(.2rem) brightness(0.7);
display: flex;
align-items: center;
justify-content: center;
@@ -711,7 +720,7 @@ th {
width: 100%;
max-width: 480px;
padding: 2rem;
- background: var(--color-surface);
+ background: var(--color-dialog);
border-radius: var(--radius-lg);
box-shadow: 0 20px 60px #1e293b;
border: 1px solid var(--color-border);
diff --git a/frontend/src/utils/theme.js b/frontend/src/utils/theme.js
index a4dee13..51958cb 100644
--- a/frontend/src/utils/theme.js
+++ b/frontend/src/utils/theme.js
@@ -1,61 +1,11 @@
// Theme override utilities - shared across apps
// User preference or URL hash can force light/dark mode
-export const themeColors = {
- light: {
- 'color-canvas': '#ffffff',
- 'color-surface': '#eff6ff',
- 'color-surface-subtle': '#dbeafe',
- 'color-border': '#2563eb',
- 'color-border-strong': '#1e40af',
- 'color-heading': '#1e3a8a',
- 'color-text': '#1e293b',
- 'color-text-muted': '#475569',
- 'color-link': '#1d4ed8',
- 'color-link-hover': '#1e40af',
- 'color-accent': '#2563eb',
- 'color-accent-strong': '#1e40af',
- 'color-accent-contrast': '#ffffff',
- 'color-success-text': '#166534',
- 'color-success-bg': '#dcfce7',
- 'color-error-text': '#b91c1c',
- 'color-error-bg': '#fee2e2',
- 'color-info-text': '#1e40af',
- 'color-info-bg': '#dbeafe',
- 'color-danger': '#dc2626',
- 'shadow-soft': '0 10px 30px rgba(30, 64, 175, 0.15)',
- },
- dark: {
- 'color-canvas': '#0f172a',
- 'color-surface': '#141b2f',
- 'color-surface-subtle': '#1b243b',
- 'color-border': '#25304a',
- 'color-border-strong': '#3d4d6b',
- 'color-heading': '#fff',
- 'color-text': '#e2e8f0',
- 'color-text-muted': '#94a3b8',
- 'color-link': '#60a5fa',
- 'color-link-hover': '#93c5fd',
- 'color-accent': '#60a5fa',
- 'color-accent-strong': '#3b82f6',
- 'color-accent-contrast': '#0b1120',
- 'color-success-text': '#34d399',
- 'color-success-bg': '#1a4d2e',
- 'color-error-text': '#fca5a5',
- 'color-error-bg': '#4a1f1f',
- 'color-info-text': '#bae6fd',
- 'color-info-bg': '#1e3a5f',
- 'color-danger': '#f87171',
- 'shadow-soft': '0 0 0 #000000',
- }
-}
-
-const STYLE_ID = 'theme-override'
const TRANSITION_ID = 'theme-transition'
const STORAGE_KEY = 'paskia-theme'
-/** Apply theme override CSS - selector targets .surface for restricted app, :root for main apps */
-export function applyTheme(theme, selector = ':root', animate = false) {
+/** Apply theme by setting class on documentElement */
+export function applyTheme(theme, element = document.documentElement, animate = false) {
// Add temporary transition for smooth theme change
if (animate) {
let transitionStyle = document.getElementById(TRANSITION_ID)
@@ -67,14 +17,9 @@ export function applyTheme(theme, selector = ':root', animate = false) {
}
setTimeout(() => document.getElementById(TRANSITION_ID)?.remove(), 350)
}
- document.getElementById(STYLE_ID)?.remove()
- if (theme && themeColors[theme]) {
- const css = `${selector} { ${Object.entries(themeColors[theme]).map(([k, v]) => `--${k}: ${v}`).join('; ')}; }`
- const style = document.createElement('style')
- style.id = STYLE_ID
- style.textContent = css
- document.head.appendChild(style)
- }
+ // If no explicit theme, check system preference
+ const isDark = theme === 'dark' || (theme !== 'light' && matchMedia('(prefers-color-scheme:dark)').matches)
+ element.classList.toggle('dark', isDark)
}
/** Get theme from localStorage cache */
@@ -97,5 +42,5 @@ export function initThemeFromCache() {
export function updateThemeFromSession(ctx, animate = false) {
const theme = ctx?.user?.theme || ''
setCachedTheme(theme)
- applyTheme(theme, ':root', animate)
+ applyTheme(theme, document.documentElement, animate)
}
diff --git a/paskia-js/src/overlay.ts b/paskia-js/src/overlay.ts
index 3890caf..61d6274 100644
--- a/paskia-js/src/overlay.ts
+++ b/paskia-js/src/overlay.ts
@@ -14,8 +14,8 @@ body::before {
transition: all 0.2s ease-out;
}
body.paskia-backdrop::before {
- -webkit-backdrop-filter: blur(.2rem) brightness(0.5);
- backdrop-filter: blur(.2rem) brightness(0.5);
+ backdrop-filter: blur(.2rem) brightness(0.7);
+ -webkit-backdrop-filter: blur(.2rem) brightness(0.7);
visibility: visible;
}
body.paskia-backdrop {