Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion sass/_base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,34 @@ body {
min-height: 100vh;
}

// ─── Light mode background ─────────────────────────────────────────────
// In dark mode the animated canvas (#fractal-bg) IS the visual ground.
// In light mode we hide the canvas (its palettes are tuned for dark)
// and render a faint blueprint dot-grid on the body instead — the engineering
// equivalent of paper, calm enough to read in direct sunlight.

@mixin light-background {
#fractal-bg { display: none; }

// Hide the blue-tinted blueprint grid overlay too (defined in _background.scss);
// its blue lines were tuned for dark and read poorly on cream.
body::after { display: none; }

body {
background-color: $bg;
background-image: radial-gradient(circle, var(--grid-dot) 1px, transparent 1.2px);
background-size: 28px 28px;
background-position: 0 0;
background-attachment: fixed;
}
}

:root[data-theme="light"] { @include light-background; }

@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) { @include light-background; }
}

// Focus indicators for keyboard navigation
:focus-visible {
outline: 2px solid $accent;
Expand Down Expand Up @@ -74,7 +102,7 @@ a {
transition: color 0.2s;

&:hover {
color: lighten($accent, 12%);
color: $accent-hover;
}
}

Expand Down
50 changes: 50 additions & 0 deletions sass/_nav.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,53 @@
border-bottom-color: $accent;
}
}

// Theme toggle button — sun in dark mode, moon in light mode.
// The two SVG icons live inside the button; CSS hides whichever is
// not relevant to the current theme so the button silhouette is stable.
.theme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin-left: 0.5rem;
padding: 0;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: $text-dim;
cursor: pointer;
transition: color 0.2s, border-color 0.2s, background 0.2s;

&:hover {
color: $text;
border-color: $border-subtle;
background: $surface;
}

&:focus-visible {
color: $text;
border-color: $accent;
}

svg {
width: 18px;
height: 18px;
}
}

// Show sun in dark mode (click → switch to light), moon in light mode.
.theme-toggle .icon-sun { display: inline; }
.theme-toggle .icon-moon { display: none; }

@mixin light-toggle {
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: inline; }
}

:root[data-theme="light"] { @include light-toggle; }

@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) { @include light-toggle; }
}
129 changes: 104 additions & 25 deletions sass/_variables.scss
Original file line number Diff line number Diff line change
@@ -1,33 +1,112 @@
// Colors — aligned with thrum unified dashboard (style.css)
$bg: #0f1117;
$bg-subtle: #13161f;
$surface: #1a1d27;
$surface-raised: #242836;
$surface-glass: rgba(26, 29, 39, 0.72);
$border: #4a5068;
$border-subtle: #3d4258;
$text: #e1e4ed;
$text-dim: #8b90a0;
$text-faint: #808698;
$accent: #6c8cff;
$accent-glow: rgba(108, 140, 255, 0.12);
$green: #4ade80;
$red: #f87171;
$amber: #fbbf24;
$cyan: #22d3ee;
$purple: #c084fc;

// Typography — Braille Institute's Atkinson Hyperlegible
$font-sans: 'Atkinson Hyperlegible Next', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
$font-mono: 'Atkinson Hyperlegible Mono', 'Fira Code', monospace;
$font-size: 16px;
// Theme tokens are CSS custom properties (so they can swap at runtime),
// with Sass variables aliasing into them so the existing `$bg`-style
// references in the rest of the partials keep compiling unchanged.
//
// Dark is the default; light is opt-in via `:root[data-theme="light"]`
// or auto-applied when the user's OS reports `prefers-color-scheme: light`
// AND no explicit `data-theme` was chosen. The manual override (set via
// the nav button + localStorage) always wins.
//
// Note on Sass color functions: `lighten`, `darken`, `mix` etc. cannot
// operate on `var(--token)` because Sass evaluates them at compile time
// while CSS variables resolve at render time. The one place that used
// `lighten` (link hover) has been replaced with a dedicated `--accent-hover`.

// ─── Dark theme (default) ─────────────────────────────────────────────
:root {
--bg: #0f1117;
--bg-subtle: #13161f;
--surface: #1a1d27;
--surface-raised: #242836;
--surface-glass: rgba(26, 29, 39, 0.72);
--border: #4a5068;
--border-subtle: #3d4258;
--text: #e1e4ed;
--text-dim: #8b90a0;
--text-faint: #808698;
--accent: #6c8cff;
--accent-hover: #8aa3ff;
--accent-glow: rgba(108, 140, 255, 0.12);
--green: #4ade80;
--red: #f87171;
--amber: #fbbf24;
--cyan: #22d3ee;
--purple: #c084fc;

// Light-mode-only background pattern (no-op in dark mode)
--grid-dot: transparent;

color-scheme: dark;
}

// ─── Light theme — warm cream / engineering paper ─────────────────────
// Triggered when the user explicitly chose light, OR when no choice
// was made and the OS prefers light. The manual `data-theme="dark"`
// attribute always shadows this.

@mixin light-tokens {
--bg: #f4efe4; // cream paper
--bg-subtle: #ebe5d6;
--surface: #fbf7ee; // card body
--surface-raised: #fefcf6;
--surface-glass: rgba(251, 247, 238, 0.88); // mostly opaque — translucency reads poorly on light
--border: #b8ac8c; // warm beige
--border-subtle: #d9d1bd;
--text: #1a2235; // deep navy — high AA on cream
--text-dim: #44516a;
--text-faint: #6b7280;
--accent: #3a5fce; // darkened accent for AA on cream
--accent-hover: #2747ad;
--accent-glow: rgba(58, 95, 206, 0.10);
--green: #15803d;
--red: #b91c1c;
--amber: #a16207;
--cyan: #0e7490;
--purple: #7c3aed;

--grid-dot: rgba(68, 81, 106, 0.10);

color-scheme: light;
}

:root[data-theme="light"] { @include light-tokens; }

@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) { @include light-tokens; }
}

// ─── Sass aliases that resolve to the live CSS variables ──────────────
// Existing partials reference these via `color: $text` etc. — the
// substitution emits `color: var(--text)` in the output CSS, so theme
// switching works without touching every partial.
$bg: var(--bg);
$bg-subtle: var(--bg-subtle);
$surface: var(--surface);
$surface-raised: var(--surface-raised);
$surface-glass: var(--surface-glass);
$border: var(--border);
$border-subtle: var(--border-subtle);
$text: var(--text);
$text-dim: var(--text-dim);
$text-faint: var(--text-faint);
$accent: var(--accent);
$accent-hover: var(--accent-hover);
$accent-glow: var(--accent-glow);
$green: var(--green);
$red: var(--red);
$amber: var(--amber);
$cyan: var(--cyan);
$purple: var(--purple);

// ─── Non-theme tokens (sizes, fonts) — stay as plain Sass values ──────
$font-sans: 'Atkinson Hyperlegible Next', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
$font-mono: 'Atkinson Hyperlegible Mono', 'Fira Code', monospace;
$font-size: 16px;
$line-height: 1.65;

// Glass
$glass-blur: blur(12px);
$glass-radius: 12px;

// Layout
$max-width: 1100px;
$content-width: 720px;
$gap: 20px;
35 changes: 35 additions & 0 deletions static/theme-toggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Theme toggle — clicking the button cycles light ↔ dark and persists
// the choice in localStorage. The bootstrapper in <head> already
// applied the saved preference before render, so this script only
// needs to handle the click and the post-load attribute update.
//
// Resolution order on each click:
// 1. If data-theme is currently "dark" → switch to "light"
// 2. Else (data-theme="light", or unset and OS-prefers-light, or unset
// and OS-prefers-dark) → switch to "dark"
// In other words: click always inverts the *currently rendered* theme,
// then pins that choice in localStorage so the OS preference no longer
// auto-applies.

(function () {
'use strict';

var STORAGE_KEY = 'pulseengine-theme';
var btn = document.querySelector('.theme-toggle');
if (!btn) return;

function currentTheme() {
var explicit = document.documentElement.getAttribute('data-theme');
if (explicit === 'light' || explicit === 'dark') return explicit;
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
return 'light';
}
return 'dark';
}

btn.addEventListener('click', function () {
var next = currentTheme() === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem(STORAGE_KEY, next); } catch (e) { /* private mode */ }
});
})();
25 changes: 25 additions & 0 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@
{% endif %}

{% block head_extra %}{% endblock %}

{# Inline theme bootstrapper — runs before <body> renders so we set
the data-theme attribute (if the user has a saved preference)
without a flash of wrong theme. Auto-detect via prefers-color-scheme
happens in CSS via @media — no JS needed for that path. #}
<script>
(function () {
try {
var saved = localStorage.getItem('pulseengine-theme');
if (saved === 'light' || saved === 'dark') {
document.documentElement.setAttribute('data-theme', saved);
}
} catch (e) { /* private mode / disabled storage — fall back to prefers-color-scheme */ }
})();
</script>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to content</a>
Expand All @@ -84,6 +99,15 @@
{# <a href="{{ get_url(path='@/docs/_index.md') }}"{% if current_path is starting_with("/docs") %} class="active"{% endif %}>Docs</a> #}
<a href="{{ get_url(path='@/reports/_index.md') }}"{% if current_path is starting_with("/reports") %} class="active"{% endif %}>Reports</a>
<a href="https://github.com/pulseengine" rel="noopener">GitHub</a>
<button type="button" class="theme-toggle" aria-label="Toggle light / dark theme" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path>
</svg>
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
</nav>
</div>
</header>
Expand All @@ -101,5 +125,6 @@
<script src="{{ get_url(path='background.js') }}"></script>
<script src="{{ get_url(path='journey.js') }}"></script>
<script src="{{ get_url(path='flip.js') }}"></script>
<script src="{{ get_url(path='theme-toggle.js') }}"></script>
</body>
</html>
Loading
Loading