From ceb014e9b5e7567b66ca367b337ae20a336ef097 Mon Sep 17 00:00:00 2001 From: vyakymenko Date: Thu, 23 Apr 2026 10:46:03 +0000 Subject: [PATCH] Light/dark theme system: CSS vars, ThemeToggle, anti-flash, Apple-clean light surfaces Co-Authored-By: Claude Sonnet 4.6 --- app/components/Nav.jsx | 3 + app/components/ThemeToggle.jsx | 58 ++++++++++++++ app/globals.css | 141 +++++++++++++++++++++++++++++++-- app/layout.jsx | 4 + app/page.jsx | 106 ++++++++++++------------- 5 files changed, 252 insertions(+), 60 deletions(-) create mode 100644 app/components/ThemeToggle.jsx diff --git a/app/components/Nav.jsx b/app/components/Nav.jsx index 0c6bf05..c9be536 100644 --- a/app/components/Nav.jsx +++ b/app/components/Nav.jsx @@ -1,5 +1,6 @@ 'use client' import { useState } from 'react' +import ThemeToggle from './ThemeToggle' const LINKS = [ { href: '#manifesto', label: 'Why Fibe' }, @@ -43,6 +44,7 @@ export default function Nav() { {/* Desktop CTA */}
+ @@ -57,6 +59,7 @@ export default function Nav() { {/* Mobile: CTA + hamburger */}
+ diff --git a/app/components/ThemeToggle.jsx b/app/components/ThemeToggle.jsx new file mode 100644 index 0000000..8242c7b --- /dev/null +++ b/app/components/ThemeToggle.jsx @@ -0,0 +1,58 @@ +'use client' +import { useEffect, useState } from 'react' + +export default function ThemeToggle() { + const [dark, setDark] = useState(true) + + useEffect(() => { + // Read from localStorage (set by anti-flash script in layout) + const saved = localStorage.getItem('fibe-theme') + setDark(saved !== 'light') + }, []) + + const toggle = () => { + const next = !dark + setDark(next) + if (next) { + document.documentElement.removeAttribute('data-theme') + localStorage.setItem('fibe-theme', 'dark') + } else { + document.documentElement.setAttribute('data-theme', 'light') + localStorage.setItem('fibe-theme', 'light') + } + } + + return ( + + ) +} diff --git a/app/globals.css b/app/globals.css index 941398d..34814c4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -30,6 +30,47 @@ --shadow-elevated: 0 8px 30px -4px rgb(0 0 0 / 0.5), 0 4px 10px -4px rgb(0 0 0 / 0.4); --transition-spring: cubic-bezier(0.34, 1.56, 0.64, 1); --transition-smooth: cubic-bezier(0.4, 0, 0.2, 1); + + /* Semantic text scale — dark defaults */ + --ft-primary: rgba(250,250,250,0.92); + --ft-muted: rgba(161,161,170,0.75); + --ft-subtle: rgba(161,161,170,0.48); + --ft-dimmed: rgba(161,161,170,0.32); + --ft-ghost: rgba(161,161,170,0.2); + --ft-white: rgba(255,255,255,0.9); + + /* Section / surface */ + --section-alt: rgba(139,92,246,0.025); + } + + /* ─── LIGHT THEME ─── */ + [data-theme="light"] { + --background: #f8f8f6; + --foreground: #18181b; + --card: #ffffff; + --card-foreground: #18181b; + --muted: #f1f0ee; + --muted-foreground: #71717a; + --accent: #f5f3ff; + --accent-foreground: #7c3aed; + --border: rgba(124, 58, 237, 0.18); + --border-subtle: rgba(124, 58, 237, 0.09); + --primary: #7c3aed; + --primary-dark: #6d28d9; + --primary-light: #8b5cf6; + --ring: #7c3aed; + --shadow-glass: 0 2px 24px 0 rgb(124 58 237 / 0.08); + --shadow-elevated: 0 2px 16px -2px rgb(0 0 0 / 0.1); + + /* Text scale — light */ + --ft-primary: rgba(24,24,27,0.92); + --ft-muted: rgba(39,39,42,0.68); + --ft-subtle: rgba(39,39,42,0.48); + --ft-dimmed: rgba(39,39,42,0.35); + --ft-ghost: rgba(39,39,42,0.2); + --ft-white: rgba(24,24,27,0.9); + + --section-alt: rgba(124,58,237,0.03); } /* Phoenix logo animation */ @@ -89,6 +130,7 @@ body { background: var(--background); color: var(--foreground); min-height: 100vh; + transition: background 0.3s ease, color 0.3s ease; } /* Display font for headings */ @@ -96,14 +138,20 @@ body { font-family: var(--font-display); } -/* Animated purple-indigo gradient background */ +/* ─── ANIMATED GRADIENT BACKGROUND ─── */ .bg-fibe { background: linear-gradient(135deg, #0b0916 0%, #0f0c29 25%, #1a1540 50%, #261f5a 75%, #1e1a42 100%); background-size: 400% 400%; animation: gradientShift 18s ease infinite; } -/* Subtle grid */ +[data-theme="light"] .bg-fibe { + background: linear-gradient(135deg, #ffffff 0%, #fdf9ff 25%, #f5efff 50%, #fdf9ff 75%, #ffffff 100%); + background-size: 400% 400%; + animation: gradientShift 18s ease infinite; +} + +/* ─── GRID OVERLAY ─── */ .grid-overlay { background-image: linear-gradient(rgba(139,92,246,0.04) 1px, transparent 1px), @@ -111,7 +159,13 @@ body { background-size: 52px 52px; } -/* Glass card */ +[data-theme="light"] .grid-overlay { + background-image: + linear-gradient(rgba(124,58,237,0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(124,58,237,0.05) 1px, transparent 1px); +} + +/* ─── GLASS CARDS ─── */ .glass { background: rgba(255,255,255,0.03); border: 1px solid var(--border); @@ -119,6 +173,14 @@ body { -webkit-backdrop-filter: blur(16px); } +[data-theme="light"] .glass { + background: rgba(255,255,255,0.72); + border: 1px solid rgba(124,58,237,0.14); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + box-shadow: 0 1px 4px rgba(0,0,0,0.05), 0 4px 20px rgba(124,58,237,0.06); +} + .glass-strong { background: rgba(26,21,37,0.7); border: 1px solid rgba(167,139,250,0.18); @@ -127,7 +189,15 @@ body { box-shadow: var(--shadow-glass); } -/* Glows */ +[data-theme="light"] .glass-strong { + background: rgba(255,255,255,0.9); + border: 1px solid rgba(124,58,237,0.18); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + box-shadow: 0 2px 32px rgba(0,0,0,0.08), 0 0 0 1px rgba(124,58,237,0.08); +} + +/* ─── GLOW ORBS ─── */ .glow-orb { position: absolute; border-radius: 50%; @@ -135,7 +205,12 @@ body { filter: blur(1px); } -/* Gradient text — display font hero */ +[data-theme="light"] .glow-orb { + opacity: 0.3; + filter: blur(2px); +} + +/* ─── GRADIENT TEXT ─── */ .gradient-text { background: linear-gradient(135deg, #c4b5fd 0%, #a78bfa 40%, #818cf8 80%, #93c5fd 100%); -webkit-background-clip: text; @@ -143,7 +218,14 @@ body { background-clip: text; } -/* Tags / pills */ +[data-theme="light"] .gradient-text { + background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 50%, #4f46e5 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ─── TAGS / PILLS ─── */ .tag { background: rgba(139,92,246,0.1); border: 1px solid rgba(139,92,246,0.2); @@ -156,7 +238,52 @@ body { border-radius: 999px; } -/* Scrollbar */ +[data-theme="light"] .tag { + background: rgba(124,58,237,0.07); + border-color: rgba(124,58,237,0.18); + color: #7c3aed; +} + +/* ─── text-white override in light mode ─── */ +[data-theme="light"] .text-white { + color: #18181b !important; +} + +/* ─── Terminal — always dark regardless of theme ─── */ +.terminal-mockup { + background: rgba(15,12,26,0.95); +} +[data-theme="light"] .terminal-mockup { + box-shadow: 0 8px 40px rgba(0,0,0,0.18), 0 0 0 1px rgba(124,58,237,0.15) !important; +} +[data-theme="light"] .terminal-mockup * { + /* Terminal internals stay dark — do NOT override */ +} + +/* ─── NAV border ─── */ +[data-theme="light"] nav { + background: rgba(248,248,246,0.85) !important; + border-bottom-color: rgba(124,58,237,0.12) !important; + box-shadow: 0 1px 0 rgba(0,0,0,0.05); +} + +/* ─── Section border dividers ─── */ +[data-theme="light"] [style*="rgba(139,92,246,0.08)"] { + /* handled via CSS variable approach */ +} + +/* ─── Comparison table ─── */ +[data-theme="light"] .comparison-row:nth-child(even) { + background: rgba(124,58,237,0.02); +} + +/* ─── Pulse glow — light adaptation ─── */ +[data-theme="light"] .animate-pulse-glow { + animation: none; + box-shadow: 0 4px 32px rgba(124,58,237,0.12); +} + +/* ─── Scrollbar ─── */ ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-track { background: var(--background); } ::-webkit-scrollbar-thumb { background: rgba(139,92,246,0.25); border-radius: 3px; } diff --git a/app/layout.jsx b/app/layout.jsx index b82bc67..9ae1f90 100644 --- a/app/layout.jsx +++ b/app/layout.jsx @@ -50,6 +50,10 @@ export default function RootLayout({ children }) { return ( + {/* Anti-flash: apply saved theme before React hydrates */} +