13H · Modes
Light & Dark Mode Design
Dark mode is NOT "light mode with inverted colours." Each mode is
independently designed. Light leans warm-paper with gold accent;
dark leans warm-ink with a brighter gold. The accent colour
literally swaps intensity between modes. That deliberate asymmetry
is what makes each mode feel designed rather than
auto-generated.
Core principle: one class, cascaded everywhere
/* Set once on <html> — every descendant resolves automatically */
:root { /* light */
--surface-bg: #f7f2e9;
--surface-container: #f0e8d8;
--text-primary: #1a1410;
--text-secondary: #6b5d4f;
--accent: #c8862a;
--accent-bright: #d9a441;
--glass-bg: rgba(247, 242, 233, 0.82);
--glass-border: rgba(156, 59, 27, 0.18);
--glass-shadow: 0 6px 28px rgba(156, 59, 27, 0.15);
--glow-primary: rgba(200, 134, 42, 0.16);
--glow-secondary: rgba(47, 93, 68, 0.14);
--glow-tertiary: rgba(156, 59, 27, 0.08);
}
.dark { /* independently designed, NOT auto-inverted */
--surface-bg: #1a1410;
--surface-container: #2d2118;
--text-primary: #f7f2e9;
--text-secondary: #b9a888;
--accent: #d4943a; /* BRIGHTER gold on dark */
--accent-bright: #e0b25a;
--glass-bg: rgba(26, 20, 16, 0.78);
--glass-border: rgba(212, 148, 58, 0.15);
--glass-shadow: 0 4px 24px rgba(212, 148, 58, 0.10);
--glow-primary: rgba(212, 148, 58, 0.07); /* HALF intensity */
--glow-secondary: rgba(90, 155, 120, 0.06);
--glow-tertiary: rgba(194, 90, 53, 0.05);
}
Theme toggle: draw from inspiration, not icon fonts
| Aspect |
Implementation |
| Sun icon |
Inline SVG, 22×22 viewBox. Àṣẹ-inspired: gold core + 8
radiating ray lines. Rays rotate 18s, core breathes (scale
1→1.08 over 4s). Two slow independent animations = organic.
|
| Moon icon |
Inline SVG, 22×22. Bolga-spiral-inspired: fat crescent path
+ coil detail inside. 6s glow-and-scale pulse.
|
| Glow |
Three stacked drop-shadow filters at increasing
radius/decreasing opacity. overflow: visible on the
SVG (critical — without it glow clips at bounding box).
|
| Convention |
Icon shown = mode you switch TO. aria-label updates
dynamically (“Switch to dark mode”).
|
| Motion gate |
All animation inside
@media (prefers-reduced-motion: no-preference).
Static glowing icon for reduced-motion users.
|
| State source |
Driven off the same class that themes everything. Toggle class +
icon + animation + page theme never disagree.
|
/* Toggle glow — layered for believable light bloom */
.toggle-sun {
overflow: visible;
filter:
drop-shadow(0 0 2px rgba(200,134,42,0.85))
drop-shadow(0 0 5px rgba(200,134,42,0.45))
drop-shadow(0 0 12px rgba(200,134,42,0.2));
}
@media (prefers-reduced-motion: no-preference) {
.toggle-sun .rays {
animation: ray-spin 18s linear infinite;
transform-origin: center;
}
.toggle-sun .core {
animation: core-breathe 4s ease-in-out infinite;
}
@keyframes ray-spin { to { transform: rotate(360deg); } }
@keyframes core-breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
}
Nav bar: gradient surface, not flat
| Property |
Light |
Dark |
| background |
linear-gradient(180deg, #f7f2e9, #f0e8d8) |
linear-gradient(180deg, #1a1410, #231a12) |
| border-bottom |
1px solid rgba(200,134,42,0.12) (gold tint) |
1px solid rgba(212,148,58,0.1) (brighter gold) |
| box-shadow |
0 2px 8px rgba(200,134,42,0.06) |
0 2px 8px rgba(0,0,0,0.2) |
| shimmer |
24s sweep: 200%-wide transparent→white-3%→transparent
gradient. Almost subliminal. Gated by
prefers-reduced-motion.
|
/* Nav bar with ambient shimmer */
.top-nav {
background: linear-gradient(180deg, var(--surface-bg), var(--surface-container));
border-bottom: 1px solid rgba(200,134,42,0.12);
box-shadow: 0 2px 8px rgba(200,134,42,0.06);
position: sticky; top: 0; isolation: isolate;
}
.dark .top-nav {
border-bottom-color: rgba(212,148,58,0.1);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* 24s shimmer — barely perceptible, adds polish */
@media (prefers-reduced-motion: no-preference) {
.top-nav::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(90deg,
transparent 0%, rgba(255,255,255,0.03) 50%, transparent 100%);
background-size: 200% 100%;
animation: shimmer 24s linear infinite;
pointer-events: none;
}
@keyframes shimmer {
from { background-position: -100% 0; }
to { background-position: 200% 0; }
}
}
/* Nav link hover: inset accent bar + soft glow */
.nav-link:hover {
box-shadow: inset 3px 0 0 var(--accent), 0 0 12px rgba(200,134,42,0.1);
padding-left: calc(1rem + 3px); /* tactile nudge */
}
Logo: inline SVG with theme-adaptive fills
| Property |
Light |
Dark |
| letter stroke |
currentColor (ink) |
currentColor (paper) |
| crossbar / border |
var(--accent) (#c8862a) |
var(--accent) (#d4943a) |
| filter |
drop-shadow(0 2px 4px rgba(26,20,16,0.2)) |
drop-shadow(0 0 6px rgba(212,148,58,0.35)) |
| sizing |
height: calc(100% - 10px) constrained to nav, with
min/max clamps
|
/* Logo reads CSS vars — one component, two modes */
.brand-mark {
--accent: var(--md-primary, #c8862a);
height: calc(100% - 10px);
min-height: 28px; max-height: 40px;
}
/* light: crisp shadow */
.brand-mark { filter: drop-shadow(0 2px 4px rgba(26,20,16,0.2)); }
/* dark: coloured glow */
.dark .brand-mark { filter: drop-shadow(0 0 6px rgba(212,148,58,0.35)); }
/* SVG fills reference vars, never baked hex */
<svg>
<rect stroke="var(--accent)" .../>
<path stroke="currentColor" .../> <!-- inherits text color -->
<path stroke="var(--accent)" .../> <!-- crossbar, always gold -->
</svg>
Backgrounds: layered radial gradients
/* A flat background = "template" feel. Layer ambient depth. */
.app-shell {
background-color: var(--surface-bg);
background-image:
radial-gradient(ellipse at 8% 12%, var(--glow-primary) 0%, transparent 35%),
radial-gradient(ellipse at 92% 88%, var(--glow-secondary) 0%, transparent 35%),
radial-gradient(ellipse at 50% 40%, var(--glow-tertiary) 0%, transparent 50%);
}
/* In dark mode the glow vars are already halved — no extra rule needed */
Dark-mode heading glow (light mode: no glow)
/* Glows read as "premium" on dark and as "smudge" on light — don't apply in light */
.dark h1, .dark h2 {
text-shadow: 0 0 12px rgba(212, 148, 58, 0.4);
}
/* Wordmark: layered extrusion shadows + ground blur */
.brand-wordmark {
text-shadow:
1px 1px 0 #5a3e12,
2px 2px 0 #3d2600,
3px 3px 0 #2d2118,
4px 4px 6px rgba(26,20,16,0.3);
}
Ambient floating orbs
/* Two big blurred circles behind everything — life without distraction */
@media (prefers-reduced-motion: no-preference) {
.orb {
position: fixed; z-index: -1;
width: 40vw; height: 40vw;
border-radius: 50%;
filter: blur(80px);
opacity: 0.035;
animation: drift 28s ease-in-out infinite alternate;
}
.orb-gold { background: var(--accent); top: 10%; left: 5%; }
.orb-green { background: #2f5d44; bottom: 10%; right: 5%; animation-delay: -14s; }
@keyframes drift {
from { transform: translate(0, 0); }
to { transform: translate(3vw, 2vh); }
}
}
Entrance stagger
/* Cards assemble in sequence, not all-at-once pop */
@media (prefers-reduced-motion: no-preference) {
.card { animation: fade-up 0.4s ease-out both; }
.card:nth-child(1) { animation-delay: 0.05s; }
.card:nth-child(2) { animation-delay: 0.10s; }
.card:nth-child(3) { animation-delay: 0.15s; }
@keyframes fade-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
}
Themed micro-details
/* Scrollbars */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 9999px; }
/* Text selection */
::selection { background: var(--accent-container); color: var(--text-primary); }
/* Focus ring — accessibility + polish */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Gradient sheen divider (replaces <hr>) */
.sheen-hr {
height: 2px; border: none;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
}
/* Gradient button with mode-specific direction */
.cta-btn {
background: linear-gradient(135deg, var(--accent), var(--accent-bright));
box-shadow: 0 4px 14px rgba(200,134,42,0.3);
}
.dark .cta-btn {
background: linear-gradient(135deg, var(--accent-bright), var(--accent));
box-shadow: 0 4px 14px rgba(212,148,58,0.2);
}
/* Hover: shadow grows + lift */
@media (hover: hover) {
.cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(200,134,42,0.4);
}
}
Dark-mode failure modes (audit these)
| Common miss |
Fix |
| Section headers illegible |
Explicitly set color: var(--text-primary) on every
heading level
|
| Link text washed out |
Use var(--accent) for links in both modes (it’s
brighter in dark)
|
| Dropdown items invisible |
Chase every text role: label, body-secondary, menu-item,
table-cell
|
| Input text black-on-dark |
Set color: var(--text-primary) on all form controls
|
| Disabled state indistinguishable |
Use opacity: 0.5 + var(--text-secondary)
|
| Double-glass muddy panels |
Neutralize inner glass (background: transparent; border: none) when nested inside a glass wrapper
|
The thread tying it together
Independent per-mode design (not auto-inversion), layered cheap
effects (stacked drop-shadows, gradient surfaces, slow ambient
motion), and every animation gated behind
prefers-reduced-motion. The toggle, nav, and logo all
read the same theme state so they can never visually disagree. Always
audit both modes on a real device — the failures hide in text
contrast, not the parts that look obviously broken.
Drawn from: Light mode = the warm paper surface of
unfired clay, bright afternoon in Ga Mashie. Dark mode = the workshop
interior where Paa Joe carves by lamplight—warm-dark, not cold-dark.
The àṣẹ sun/moon toggle mirrors the dual nature of the life-force:
radiant by day, concentrated by night.