Add dark theme: CSS variables, prefers-color-scheme auto-detect, sun/moon toggle in nav with localStorage
This commit is contained in:
parent
3165ff0f9b
commit
e8be79edb0
161
index.html
161
index.html
@ -7,25 +7,103 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
// Apply theme ASAP to avoid flash of wrong theme. Runs before <body> parses.
|
||||
(function() {
|
||||
try {
|
||||
const saved = localStorage.getItem('vibe.theme');
|
||||
if (saved === 'dark' || saved === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
/* Brand */
|
||||
--blue: #0066cc;
|
||||
--blue-hover: #0055b3;
|
||||
--blue-link: #0058b3;
|
||||
--bg: #efefef;
|
||||
/* Surfaces */
|
||||
--page-bg: #fff;
|
||||
--bg: #efefef; /* nav / main background */
|
||||
--surface: #fff; /* cards */
|
||||
--card-neutral: #e3e3e3;
|
||||
/* Text */
|
||||
--text: #000;
|
||||
--text-on-blue: #fff;
|
||||
--text-65: rgba(0,0,0,0.65);
|
||||
--text-55: rgba(0,0,0,0.55);
|
||||
--text-45: rgba(0,0,0,0.45);
|
||||
/* Lines + hovers */
|
||||
--border: rgba(0,0,0,0.08);
|
||||
--border-strong: rgba(0,0,0,0.12);
|
||||
--hover: rgba(0,0,0,0.06);
|
||||
--hover-light: rgba(0,0,0,0.03);
|
||||
/* Status */
|
||||
--positive: #059669;
|
||||
--positive-bg: #dcfce7;
|
||||
--negative: #dc2626;
|
||||
/* Decorative */
|
||||
--dot-color: rgba(0,0,0,0.05);
|
||||
--code-bg: #1a1a1a; /* code blocks stay dark in both themes */
|
||||
--code-text: #e5e5e5;
|
||||
/* Misc */
|
||||
--modal-scrim: rgba(0,0,0,0.4);
|
||||
--shadow-card: 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Dark theme — applied via [data-theme="dark"] OR auto when no explicit preference */
|
||||
[data-theme="dark"] {
|
||||
--page-bg: #16171c;
|
||||
--bg: #1c1d22;
|
||||
--surface: #212229;
|
||||
--card-neutral: #2a2c33;
|
||||
--text: #e8e8e8;
|
||||
--text-65: rgba(255,255,255,0.72);
|
||||
--text-55: rgba(255,255,255,0.55);
|
||||
--text-45: rgba(255,255,255,0.40);
|
||||
--border: rgba(255,255,255,0.08);
|
||||
--border-strong: rgba(255,255,255,0.14);
|
||||
--hover: rgba(255,255,255,0.06);
|
||||
--hover-light: rgba(255,255,255,0.03);
|
||||
--blue: #3b82f6;
|
||||
--blue-hover: #60a5fa;
|
||||
--blue-link: #60a5fa;
|
||||
--positive: #34d399;
|
||||
--positive-bg: rgba(52,211,153,0.15);
|
||||
--negative: #f87171;
|
||||
--dot-color: rgba(255,255,255,0.05);
|
||||
--modal-scrim: rgba(0,0,0,0.65);
|
||||
--shadow-card: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--page-bg: #16171c;
|
||||
--bg: #1c1d22;
|
||||
--surface: #212229;
|
||||
--card-neutral: #2a2c33;
|
||||
--text: #e8e8e8;
|
||||
--text-65: rgba(255,255,255,0.72);
|
||||
--text-55: rgba(255,255,255,0.55);
|
||||
--text-45: rgba(255,255,255,0.40);
|
||||
--border: rgba(255,255,255,0.08);
|
||||
--border-strong: rgba(255,255,255,0.14);
|
||||
--hover: rgba(255,255,255,0.06);
|
||||
--hover-light: rgba(255,255,255,0.03);
|
||||
--blue: #3b82f6;
|
||||
--blue-hover: #60a5fa;
|
||||
--blue-link: #60a5fa;
|
||||
--positive: #34d399;
|
||||
--positive-bg: rgba(52,211,153,0.15);
|
||||
--negative: #f87171;
|
||||
--dot-color: rgba(255,255,255,0.05);
|
||||
--modal-scrim: rgba(0,0,0,0.65);
|
||||
--shadow-card: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.3);
|
||||
}
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
@ -39,12 +117,12 @@ html { scroll-behavior: smooth; }
|
||||
.tool-card:hover, .create-card:hover, .plan-step:hover,
|
||||
.gloss-item:hover, .prompt-card:hover, .eq-box:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
/* project-card already has its own hover (border + ring) — strengthen with lift */
|
||||
.project-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@ -73,7 +151,7 @@ html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #fff;
|
||||
background: var(--page-bg);
|
||||
min-height: 100dvh;
|
||||
padding: 12px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@ -182,6 +260,31 @@ nav {
|
||||
|
||||
/* ── AUTH (nav button + modal) ── */
|
||||
.nav-cta-group { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
||||
|
||||
.theme-toggle {
|
||||
width: 36px; height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--text-65);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
background: var(--hover);
|
||||
color: var(--text);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
.theme-toggle svg { width: 16px; height: 16px; }
|
||||
.theme-toggle .icon-moon { display: none; }
|
||||
[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
|
||||
[data-theme="dark"] .theme-toggle .icon-moon { display: block; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .theme-toggle .icon-sun { display: none; }
|
||||
:root:not([data-theme="light"]) .theme-toggle .icon-moon { display: block; }
|
||||
}
|
||||
.nav-cdn-chip {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-55);
|
||||
@ -197,7 +300,7 @@ nav {
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.4);
|
||||
background: var(--modal-scrim);
|
||||
display: none; align-items: center; justify-content: center;
|
||||
z-index: 100; padding: 16px;
|
||||
animation: fadeIn 150ms ease;
|
||||
@ -206,7 +309,7 @@ nav {
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
.modal-card {
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border-radius: 16px;
|
||||
padding: 32px 28px 28px;
|
||||
max-width: 420px; width: 100%;
|
||||
@ -250,7 +353,7 @@ nav {
|
||||
font-size: 0.9375rem;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
.modal-input:focus {
|
||||
@ -319,7 +422,7 @@ nav {
|
||||
}
|
||||
|
||||
.plan-step {
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px 18px;
|
||||
@ -368,7 +471,7 @@ nav {
|
||||
}
|
||||
.create-card-icon {
|
||||
width: 44px; height: 44px;
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--blue);
|
||||
@ -417,7 +520,7 @@ nav {
|
||||
border: 1px solid rgba(0,102,204,0.2);
|
||||
}
|
||||
.eq-box.result {
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.eq-box-label {
|
||||
@ -453,7 +556,7 @@ nav {
|
||||
|
||||
.eq-note {
|
||||
margin-top: 32px;
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--blue);
|
||||
border-radius: 8px;
|
||||
@ -492,7 +595,7 @@ nav {
|
||||
}
|
||||
|
||||
.prompt-card {
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
@ -642,7 +745,7 @@ main {
|
||||
text-align: center;
|
||||
/* subtle dot-grid backdrop, fades to clean at the edges */
|
||||
background-image:
|
||||
radial-gradient(ellipse at center, rgba(0,0,0,0.05) 1px, transparent 1.5px);
|
||||
radial-gradient(ellipse at center, var(--dot-color) 1px, transparent 1.5px);
|
||||
background-size: 22px 22px;
|
||||
background-position: center top;
|
||||
mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
|
||||
@ -792,7 +895,7 @@ main {
|
||||
}
|
||||
|
||||
.ba-card.after {
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@ -877,7 +980,7 @@ main {
|
||||
@media (min-width: 640px) { .tools-grid { grid-template-columns: repeat(3, 1fr); } }
|
||||
|
||||
.tool-card {
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
@ -1028,7 +1131,7 @@ main {
|
||||
.gloss-item {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scroll-snap-align: start;
|
||||
@ -1051,7 +1154,7 @@ main {
|
||||
.gloss-nav-btn {
|
||||
width: 40px; height: 40px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
@ -1136,7 +1239,7 @@ main {
|
||||
@media (min-width: 640px) { .projects-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
|
||||
.project-card {
|
||||
background: #fff;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
@ -1309,6 +1412,10 @@ footer {
|
||||
<a href="#telegram" class="nav-link">Сообщество</a>
|
||||
</div>
|
||||
<div class="nav-cta-group">
|
||||
<button id="theme-toggle" type="button" class="theme-toggle" aria-label="Переключить тему">
|
||||
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
|
||||
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
</button>
|
||||
<span id="nav-cdn" class="nav-cdn-chip" hidden title="Выйти"></span>
|
||||
<button id="nav-auth" type="button" class="nav-cta">Войти</button>
|
||||
</div>
|
||||
@ -2462,6 +2569,24 @@ const SCHEDULE = [
|
||||
|
||||
refreshHeader();
|
||||
|
||||
/* ── THEME TOGGLE ───────────────────────────────────────
|
||||
Manual override stored in localStorage. Falls back to system
|
||||
(prefers-color-scheme) when no explicit choice. */
|
||||
(function initThemeToggle() {
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (!btn) return;
|
||||
function currentTheme() {
|
||||
const explicit = document.documentElement.getAttribute('data-theme');
|
||||
if (explicit) return explicit;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
btn.addEventListener('click', () => {
|
||||
const next = currentTheme() === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
try { localStorage.setItem('vibe.theme', next); } catch (e) {}
|
||||
});
|
||||
})();
|
||||
|
||||
/* ── POLISH: scroll-triggered fade-in ────────────────────
|
||||
Marks all direct content blocks inside <section class="section">
|
||||
with .reveal, then toggles .in on intersection.
|
||||
|
||||
Reference in New Issue
Block a user