Skip to content
108 changes: 0 additions & 108 deletions app/assets/stylesheets/_global.css
Original file line number Diff line number Diff line change
Expand Up @@ -370,111 +370,3 @@ html[data-theme="dark"] {
0 0.8em 1.2em -1.6em oklch(var(--lch-black) / 0.9),
0 1.2em 1.6em -2em oklch(var(--lch-black) / 1);
}

/* Fallback to system preference when no explicit theme is set */
@media (prefers-color-scheme: dark) {
html:not([data-theme]) {
--lch-canvas: 20% 0.0195 232.58;
--lch-ink-inverted: var(--lch-black);

--lch-ink-darkest: 96.02% 0.0034 260;
--lch-ink-darker: 86% 0.0061 260;
--lch-ink-dark: 73.97% 0.009 260;
--lch-ink-medium: 62% 0.0122 260;
--lch-ink-light: 40% 0.0148 260;
--lch-ink-lighter: 30% 0.0178 260;
--lch-ink-lightest: 25% 0.0204 260;

--lch-uncolor-darkest: 96.09% 0.0076 100;
--lch-uncolor-darker: 86% 0.021 90;
--lch-uncolor-dark: 73.93% 0.041 80;
--lch-uncolor-medium: 62% 0.0552 70;
--lch-uncolor-light: 40% 0.0387 60;
--lch-uncolor-lighter: 30% 0.012 50;
--lch-uncolor-lightest: 25% 0.0017 40;

--lch-red-darkest: 95.85% 0.0218 46;
--lch-red-darker: 86% 0.086 44;
--lch-red-dark: 73.95% 0.139 42;
--lch-red-medium: 62% 0.154 40;
--lch-red-light: 40% 0.088 38;
--lch-red-lighter: 30% 0.032 36;
--lch-red-lightest: 25% 0.011 34;

--lch-yellow-darkest: 96% 0.056 100;
--lch-yellow-darker: 86% 0.103 90;
--lch-yellow-dark: 74.06% 0.136 80;
--lch-yellow-medium: 62.1% 0.146 70;
--lch-yellow-light: 40% 0.0736 60;
--lch-yellow-lighter: 30% 0.026 50;
--lch-yellow-lightest: 25% 0.01 40;

--lch-lime-darkest: 96.04% 0.066 115;
--lch-lime-darker: 86% 0.098 114;
--lch-lime-dark: 73.97% 0.121 113;
--lch-lime-medium: 62% 0.128 112;
--lch-lime-light: 40% 0.0637 111;
--lch-lime-lighter: 30% 0.024 110;
--lch-lime-lightest: 25% 0.012 109;

--lch-green-darkest: 96.12% 0.035 143;
--lch-green-darker: 86% 0.082 144;
--lch-green-dark: 73.99% 0.117 145;
--lch-green-medium: 62% 0.1261 146;
--lch-green-light: 40% 0.065 147;
--lch-green-lighter: 30% 0.03 148;
--lch-green-lightest: 25% 0.018 149;

--lch-aqua-darkest: 96.15% 0.0244 202;
--lch-aqua-darker: 86% 0.06 204;
--lch-aqua-dark: 73.92% 0.095 206;
--lch-aqua-medium: 62% 0.106 208;
--lch-aqua-light: 40% 0.0594 210;
--lch-aqua-lighter: 30% 0.028 212;
--lch-aqua-lightest: 25% 0.017 214;

--lch-blue-darkest: 95.93% 0.0217 252;
--lch-blue-darker: 86% 0.068 254;
--lch-blue-dark: 74% 0.1293 256;
--lch-blue-medium: 62% 0.159 258;
--lch-blue-light: 40% 0.094 260;
--lch-blue-lighter: 30% 0.0452 262;
--lch-blue-lightest: 25% 0.0318 264;

--lch-violet-darkest: 95.97% 0.019 280;
--lch-violet-darker: 86% 0.068 282;
--lch-violet-dark: 74.08% 0.142 284;
--lch-violet-medium: 62% 0.184 286;
--lch-violet-light: 40% 0.108 288;
--lch-violet-lighter: 30% 0.048 290;
--lch-violet-lightest: 25% 0.025 292;

--lch-purple-darkest: 95.99% 0.0217 302;
--lch-purple-darker: 86% 0.068 304;
--lch-purple-dark: 73.98% 0.141 306;
--lch-purple-medium: 62% 0.177 308;
--lch-purple-light: 40% 0.099 310;
--lch-purple-lighter: 30% 0.04 312;
--lch-purple-lightest: 25% 0.017 314;

--lch-pink-darkest: 95.84% 0.0308 336;
--lch-pink-darker: 86% 0.074 338;
--lch-pink-dark: 74.04% 0.1294 340;
--lch-pink-medium: 62% 0.166 342;
--lch-pink-light: 40% 0.085 344;
--lch-pink-lighter: 30% 0.03 346;
--lch-pink-lightest: 25% 0.011 348;

--color-terminal-bg: var(--color-canvas);
--color-terminal-text-light: oklch(var(--lch-green-dark));
--color-golden: oklch(var(--lch-blue-medium));
--color-highlight: oklch(var(--lch-blue-lighter));

--shadow: 0 0 0 1px oklch(var(--lch-black) / 0.42),
0 .2em 1.6em -0.8em oklch(var(--lch-black) / 0.6),
0 .4em 2.4em -1em oklch(var(--lch-black) / 0.7),
0 .4em .8em -1.2em oklch(var(--lch-black) / 0.8),
0 .8em 1.2em -1.6em oklch(var(--lch-black) / 0.9),
0 1.2em 1.6em -2em oklch(var(--lch-black) / 1);
}
}
6 changes: 0 additions & 6 deletions app/assets/stylesheets/bar.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@
border-block: 1px solid var(--color-ink-lighter);
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
border-block: 1px solid var(--color-ink-lighter);
}
}

&:has(.bar__placeholder[hidden]) {
padding-inline: 1ch;
}
Expand Down
6 changes: 0 additions & 6 deletions app/assets/stylesheets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,6 @@
html[data-theme="dark"] & {
background-color: var(--color-selected-dark);
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
background-color: var(--color-selected-dark);
}
}
}

:where(ul, ol):where([role="list"]) {
Expand Down
6 changes: 0 additions & 6 deletions app/assets/stylesheets/buttons.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@
--btn-hover-brightness: 1.25;
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
--btn-hover-brightness: 1.25;
}
}

&[disabled],
&:has([disabled]),
[disabled] &[type=submit],
Expand Down
12 changes: 0 additions & 12 deletions app/assets/stylesheets/card-columns.css
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,6 @@
html[data-theme="dark"] & {
outline-color: oklch(var(--lch-blue-medium));
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
outline-color: oklch(var(--lch-blue-medium));
}
}
}

&:has(.card) {
Expand Down Expand Up @@ -850,12 +844,6 @@
html[data-theme="dark"] & {
--bubble-opacity: 100%;
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
--bubble-opacity: 100%;
}
}
}

/* Card column indicators
Expand Down
6 changes: 0 additions & 6 deletions app/assets/stylesheets/cards.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@
box-shadow: 0 0 0 1px var(--color-ink-lighter);
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
box-shadow: 0 0 0 1px var(--color-ink-lighter);
}
}

.popup {
inline-size: 260px;
}
Expand Down
6 changes: 0 additions & 6 deletions app/assets/stylesheets/circled-text.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@
html[data-theme="dark"] & {
mix-blend-mode: screen;
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
mix-blend-mode: screen;
}
}
}

span::before,
Expand Down
6 changes: 0 additions & 6 deletions app/assets/stylesheets/inputs.css
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,6 @@
--caret-icon: var(--caret-icon-dark);
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
--caret-icon: var(--caret-icon-dark);
}
}

option {
background-color: var(--color-canvas);
color: var(--color-ink);
Expand Down
6 changes: 0 additions & 6 deletions app/assets/stylesheets/markdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,5 @@
filter: invert(1);
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
filter: invert(1);
}
}

}
}
2 changes: 1 addition & 1 deletion app/assets/stylesheets/notifications.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
}

.card {
@media (prefers-color-scheme: dark) {
html[data-theme="dark"] & {
box-shadow: 0 0 0 1px var(--color-ink-lighter);
}
}
Expand Down
6 changes: 0 additions & 6 deletions app/assets/stylesheets/reactions.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,6 @@
--reaction-hover-brightness: 1.25;
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
--reaction-hover-brightness: 1.25;
}
}

}
}
}
Expand Down
32 changes: 12 additions & 20 deletions app/assets/stylesheets/syntax.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
--markup-deleted: lch(39.64 68.17 31.45);

/* Redefine named color values for dark mode */
html[data-theme="dark"] {
html[data-theme="dark"] & {
--keyword: lch(67.63 58.99 30.64);
--entity: lch(75.13 46.73 306.74);
--constant: lch(74.9 39.71 255.53);
Expand All @@ -25,23 +25,7 @@
--markup-heading: lch(47.93 71.67 280.72);
--markup-list: lch(83.84 57.9 85.03);
--markup-inserted: lch(83.65 59.31 141.61);
--markup-deleted: lch(73.8% 65 29.18);
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) {
--keyword: lch(67.63 58.99 30.64);
--entity: lch(75.13 46.73 306.74);
--constant: lch(74.9 39.71 255.53);
--string: lch(74.9 39.71 255.53);
--variable: lch(76.17 61.1 61.97);
--comment: lch(60.83 6.66 254.46);
--entity-tag: lch(83.65 59.31 141.61);
--markup-heading: lch(47.93 71.67 280.72);
--markup-list: lch(83.84 57.9 85.03);
--markup-inserted: lch(83.65 59.31 141.61);
--markup-deleted: lch(73.8% 65 29.18);
}
--markup-deleted: lch(73.8 65 29.18);
}

color: var(--color-ink);
Expand All @@ -60,7 +44,11 @@

.gd {
color: var(--markup-deleted);
background-color: light-dark(lch(39.64 68.17 31.45 / 0.15), lch(39.64 68.17 31.45 / 0.2));
background-color: lch(39.64 68.17 31.45 / 0.15);

html[data-theme="dark"] & {
background-color: lch(39.64 68.17 31.45 / 0.2);
}
}

.nb, .nc, .no, .nn {
Expand All @@ -73,7 +61,11 @@

.gi {
color: var(--markup-inserted);
background-color: light-dark(lch(49.14 52.75 142.85 / 0.15), lch(83.65 59.31 141.61 / 0.15));
background-color: lch(49.14 52.75 142.85 / 0.15);

html[data-theme="dark"] & {
background-color: lch(83.65 59.31 141.61 / 0.15);
}
}

.kc, .l, .ld, .m, .mb, .mf, .mh, .mi, .il, .mo, .mx, .sb, .bp, .ne, .nl, .py, .nv, .vc, .vg, .vi, .vm, .o, .ow {
Expand Down
7 changes: 0 additions & 7 deletions app/assets/stylesheets/trays.css
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,6 @@
html[data-theme="dark"] & {
box-shadow: 0 0 0 1px var(--color-ink-lighter);
}

@media (prefers-color-scheme: dark) {
html:not([data-theme]) & {
box-shadow: 0 0 0 1px var(--color-ink-lighter);
}
}

}

.card__background {
Expand Down
28 changes: 20 additions & 8 deletions app/javascript/controllers/theme_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["lightButton", "darkButton", "autoButton"]

#mediaQuery
#handleSystemThemeChange

connect() {
this.#mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
this.#handleSystemThemeChange = () => this.#applyStoredTheme()
this.#mediaQuery.addEventListener("change", this.#handleSystemThemeChange)
this.#applyStoredTheme()
}

disconnect() {
this.#mediaQuery.removeEventListener("change", this.#handleSystemThemeChange)
}

setLight() {
this.#theme = "light"
}
Expand All @@ -23,22 +33,24 @@ export default class extends Controller {
return localStorage.getItem("theme") || "auto"
}

get #resolvedTheme() {
const stored = this.#storedTheme
if (stored === "light" || stored === "dark") return stored
return this.#mediaQuery.matches ? "dark" : "light"
}

set #theme(theme) {
localStorage.setItem("theme", theme)

const currentTheme = document.documentElement.getAttribute("data-theme") || "auto"
const hasChanged = currentTheme !== theme
const resolved = this.#resolvedTheme
const currentTheme = document.documentElement.dataset.theme
const hasChanged = currentTheme !== resolved

const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches
const animate = hasChanged && !prefersReducedMotion

const applyTheme = () => {
if (theme === "auto") {
document.documentElement.removeAttribute("data-theme")
} else {
document.documentElement.setAttribute("data-theme", theme)
}

document.documentElement.dataset.theme = resolved
this.#updateButtons()
}
Comment on lines 52 to 55
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theme is now resolved to light/dark independent of OS preference, but the app’s tags (in layouts/shared/_head.html.erb) are still driven by prefers-color-scheme media queries. This can leave the browser UI color out of sync when a user forces light on a dark OS (or vice versa). Consider updating theme_controller to also update/replace the theme-color meta tag based on the resolved theme whenever applying the theme.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@nqst nqst Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice find! This issue wasn't introduced by this branch — in the current main, the theme-color meta tags rely only on the OS preferences and won't be updated if you set a different theme in Profile settings. But of course it's nice to fix it.

Addressed this in b09bff5

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified it in 68965d6

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some more testing, I decided to revert the theme-color changes. I noticed a brief window frame flash in the Chrome web app, and it looks buggy. I was able to fix this, but I wasn't happy with the code and the overall complexity. The goal of this PR is to remove the duplicate CSS in the first place, and I'm not even sure if the team would want to remove the native (prefers-color-scheme: dark) styles. I don't think it's a good idea to overcomplicate it.

The theme-color issue is valid, but I think it makes more sense to address it separately in its own PR.


Expand Down
6 changes: 5 additions & 1 deletion app/views/layouts/_theme_preference.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<%= javascript_tag nonce: true do %>
const theme = localStorage.getItem("theme")
if (theme && theme !== "auto") {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches

if (theme === "light" || theme === "dark") {
document.documentElement.dataset.theme = theme
} else {
document.documentElement.dataset.theme = prefersDark ? "dark" : "light"
}
<% end %>
2 changes: 1 addition & 1 deletion app/views/layouts/public.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<%= render "layouts/shared/head" %>

<body class="public <%= @body_class %>" data-controller="local-time timezone-cookie" data-action="turbo:morph@window->local-time#refreshAll">
<body class="public <%= @body_class %>" data-controller="local-time timezone-cookie theme" data-action="turbo:morph@window->local-time#refreshAll">
<div id="global-container">
<header class="header" id="header">
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
Expand Down