feat: Dan Mode (third theme), Pico CSS notes, cycle theme toggle
Dan Mode: pure white backgrounds, electric blue text (#0000ff), nuclear yellow accents (#ffff00), hot magenta borders (#ff00ff), red labels, glowing box-shadows. Maximum saturation. Hostile to human vision by design. Theme toggle now cycles light → dark → dan → light (icons: 🌙 😎 ☀️). PICO_CSS.md: documents [role=button] color clobber, palette scoping rules, and theme toggling pattern for future reference.
This commit is contained in:
62
PICO_CSS.md
Normal file
62
PICO_CSS.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Pico CSS — Gotchas & Notes
|
||||||
|
|
||||||
|
## [role=button] clobbers --pico-color
|
||||||
|
|
||||||
|
Pico's stylesheet includes this rule:
|
||||||
|
|
||||||
|
```css
|
||||||
|
[role=button], [type=button], button, ... {
|
||||||
|
--pico-color: var(--pico-primary-inverse);
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In light mode, `--pico-primary-inverse` is `#fff`. This means **any element with `role="button"` will have white text by default**, regardless of its background. Children inherit this, making text invisible on white/light card backgrounds.
|
||||||
|
|
||||||
|
**Fix:** On the element itself, reset the variable and set an explicit color:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.your-element {
|
||||||
|
--pico-color: initial;
|
||||||
|
color: var(--pico-contrast); /* #181c25 light / #dfe3eb dark — always legible */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative:** Remove `role="button"` and use `tabindex="0"` + keyboard handlers only, avoiding the Pico button rule entirely. Only do this if button semantics aren't needed for accessibility.
|
||||||
|
|
||||||
|
## Palette overrides must be scoped to light mode
|
||||||
|
|
||||||
|
Pico defines its palette twice: once for light (`:root:not([data-theme=dark])`) and once for dark (`@media (prefers-color-scheme: dark)` + `[data-theme=dark]`). If you override Pico vars at bare `:root`, you'll clobber the dark mode values.
|
||||||
|
|
||||||
|
**Wrong:**
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--pico-color: #2C2C2C; /* kills dark mode */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
```css
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root:not([data-theme]) { --pico-color: #2C2C2C; }
|
||||||
|
}
|
||||||
|
[data-theme=light] { --pico-color: #2C2C2C; }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Theme toggling pattern
|
||||||
|
|
||||||
|
Set `data-theme="light"` or `data-theme="dark"` on `<html>`. Pico reads this attribute for its palette selectors. Persist to `localStorage`. Read OS preference (`prefers-color-scheme`) on first visit when no stored value exists.
|
||||||
|
|
||||||
|
## --pico-card-background-color in light mode
|
||||||
|
|
||||||
|
In light mode, Pico sets:
|
||||||
|
```
|
||||||
|
--pico-card-background-color: var(--pico-background-color) → #fff
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want custom card backgrounds, override `--pico-card-background-color` directly (scoped to light mode only — see above).
|
||||||
|
|
||||||
|
## CSS variables that Pico does NOT define
|
||||||
|
|
||||||
|
- `--color-label` (app-defined) — must be set for both light and dark explicitly
|
||||||
|
- Any `--shadow-*` vars — define at `:root` (theme-neutral)
|
||||||
101
src/app.css
101
src/app.css
@@ -54,6 +54,107 @@
|
|||||||
}
|
}
|
||||||
[data-theme=dark] { --color-label: #a0a09a; }
|
[data-theme=dark] { --color-label: #a0a09a; }
|
||||||
|
|
||||||
|
/* ── DAN MODE ──────────────────────────────────────────────────────────────── */
|
||||||
|
/* Screaming bright colors. Offensive to the eyes by design. */
|
||||||
|
[data-theme=dan] {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
/* Base */
|
||||||
|
--pico-background-color: #ffffff;
|
||||||
|
--pico-color: #0000ff; /* pure electric blue body text */
|
||||||
|
--pico-muted-color: #ff6600; /* screaming orange for muted text */
|
||||||
|
--pico-muted-border-color: #ff00ff;
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
--pico-card-background-color: #ffffff;
|
||||||
|
--pico-card-border-color: #ff00ff; /* magenta borders */
|
||||||
|
--pico-card-box-shadow: 0 0 12px 2px #ffff00, 0 0 4px 1px #ff00ff;
|
||||||
|
|
||||||
|
/* Primary — nuclear yellow */
|
||||||
|
--pico-primary: #cccc00;
|
||||||
|
--pico-primary-background: #ffff00;
|
||||||
|
--pico-primary-border: #cccc00;
|
||||||
|
--pico-primary-hover: #aaaa00;
|
||||||
|
--pico-primary-hover-background: #eeee00;
|
||||||
|
--pico-primary-hover-border: #aaaa00;
|
||||||
|
--pico-primary-focus: rgba(255, 255, 0, 0.6);
|
||||||
|
--pico-primary-inverse: #000000; /* black text on yellow buttons */
|
||||||
|
|
||||||
|
/* Secondary — hot magenta */
|
||||||
|
--pico-secondary: #ff00ff;
|
||||||
|
--pico-secondary-background: #dd00dd;
|
||||||
|
--pico-secondary-border: #ff00ff;
|
||||||
|
--pico-secondary-hover: #ff44ff;
|
||||||
|
--pico-secondary-hover-background: #ff00ff;
|
||||||
|
--pico-secondary-inverse: #ffffff;
|
||||||
|
|
||||||
|
/* Contrast */
|
||||||
|
--pico-contrast: #000000;
|
||||||
|
--pico-contrast-background: #000000;
|
||||||
|
--pico-contrast-inverse: #ffff00;
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
--pico-form-element-background-color: #ffffcc;
|
||||||
|
--pico-form-element-border-color: #ff00ff;
|
||||||
|
--pico-form-element-color: #0000ff;
|
||||||
|
--pico-form-element-active-border-color: #ff0000;
|
||||||
|
--pico-form-element-focus-color: rgba(255, 0, 255, 0.5);
|
||||||
|
|
||||||
|
/* App-specific */
|
||||||
|
--color-label: #ff0000; /* red labels */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dan Mode: result tile text — override the [role=button] white-text reset */
|
||||||
|
[data-theme=dan] .result-tile {
|
||||||
|
--pico-color: initial;
|
||||||
|
color: #0000ff;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-color: #ff00ff;
|
||||||
|
box-shadow: 0 0 8px 1px #ffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dan] .result-tile:hover,
|
||||||
|
[data-theme=dan] .result-tile.highlighted {
|
||||||
|
background-color: #ffff00;
|
||||||
|
border-color: #ff0000;
|
||||||
|
color: #000000;
|
||||||
|
box-shadow: 0 0 16px 4px #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dan] .result-label {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dan Mode: highlight bar */
|
||||||
|
[data-theme=dan] .highlighted-result {
|
||||||
|
background-color: #ffff00;
|
||||||
|
border-color: #ff0000 !important;
|
||||||
|
box-shadow: 0 0 20px 4px #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dan] .highlighted-eq-from,
|
||||||
|
[data-theme=dan] .highlighted-eq-to {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dan] .highlighted-eq-sep {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dan Mode: nav / header */
|
||||||
|
[data-theme=dan] header {
|
||||||
|
background-color: #ffff00;
|
||||||
|
border-bottom: 3px solid #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dan] header strong {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme=dan] header nav a {
|
||||||
|
color: #0000ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Results grid ──────────────────────────────────────────────────────────── */
|
/* ── Results grid ──────────────────────────────────────────────────────────── */
|
||||||
.results-grid {
|
.results-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -5,25 +5,28 @@
|
|||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
// Resolve initial theme: stored pref > OS preference
|
type Theme = 'light' | 'dark' | 'dan';
|
||||||
function resolveInitialTheme(): 'light' | 'dark' {
|
const CYCLE: Theme[] = ['light', 'dark', 'dan'];
|
||||||
|
const ICONS: Record<Theme, string> = { light: '🌙', dark: '😎', dan: '☀️' };
|
||||||
|
|
||||||
|
function resolveInitialTheme(): Theme {
|
||||||
if (!browser) return 'light';
|
if (!browser) return 'light';
|
||||||
const stored = localStorage.getItem('theme');
|
const stored = localStorage.getItem('theme');
|
||||||
if (stored === 'light' || stored === 'dark') return stored;
|
if (stored === 'light' || stored === 'dark' || stored === 'dan') return stored;
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
}
|
}
|
||||||
|
|
||||||
let theme = $state<'light' | 'dark'>(resolveInitialTheme());
|
let theme = $state<Theme>(resolveInitialTheme());
|
||||||
|
|
||||||
// Apply theme to <html> and persist
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleTheme() {
|
function cycleTheme() {
|
||||||
theme = theme === 'light' ? 'dark' : 'light';
|
const next = CYCLE[(CYCLE.indexOf(theme) + 1) % CYCLE.length];
|
||||||
|
theme = next;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -37,8 +40,8 @@
|
|||||||
<li><a href="/about">About</a></li>
|
<li><a href="/about">About</a></li>
|
||||||
<li><a href="/contact">Contact</a></li>
|
<li><a href="/contact">Contact</a></li>
|
||||||
<li>
|
<li>
|
||||||
<button class="theme-toggle outline secondary" onclick={toggleTheme} aria-label="Toggle light/dark mode">
|
<button class="theme-toggle outline secondary" onclick={cycleTheme} aria-label="Toggle theme">
|
||||||
{theme === 'light' ? '🌙' : '☀️'}
|
{ICONS[theme]}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user