Dark mode
Not an inversion: surfaces lighten as they rise, shadows collapse, and the status hues are re-derived.
Why it is not a token flip
Every page on this site is already in both themes — use the control at the bottom of the nav. Three things had to move independently for that to be true.
Shadows stop working
A shadow against a dark ground is nearly invisible, and faking it with a heavier black produces a smudge. Elevation moves into the surface: lighter is higher.
The accent cannot survive
#4740DB on charcoal is 1.9:1. Links and icons take the lifted #8C86F0; the filled button keeps a bolder indigo with white text. That is why --gd-accent and --gd-accent-ink are two tokens rather than one.
Status must be re-derived
A tint that reads as caution on white reads as sickly on charcoal. Each hue keeps its identity but gains lightness and loses saturation, so all four clear 4.5:1 on the dark surface.
The surface ladder
Lighter is higher. The basemap goes to #22242B rather than black, so parcel strokes and overlay fills keep the same relationship to their ground that they have in daylight.
--gd-surface#FFFFFF#17181Ethe default ground for cards, panels and rows
--gd-surface-page#F4F4F2#101116the page behind everything — warm off-white in the light, the lowest charcoal in the dark
--gd-surface-subtle#FBFBFC#1D1F26table headers and glyph-tile grounds: a surface that is not quite the card it sits on
--gd-surface-raised#FFFFFF#1D1F26a surface lifted off its ground — a floating pill, a popover, a menu. In light the shadow carries it and this equals `surface`; in dark the surface itself has to
--gd-surface-sunken#F4F4F7#101116recessed groups — segmented tracks, icon tiles, layer lists. Sunken is the one people get wrong: a GROUP of things is recessed, not raised
--gd-surface-hover#FAFAFB#24262Epointer over a row. Hover is a SURFACE change, never a shadow change — a list that lifts under the pointer shimmers
--gd-surface-pressed#F4F4F7#2B2D36the moment of the click
--gd-surface-selected#EEF0FF#3B3866the chosen row or option
--gd-surface-disabled#F0F1F4#212329an inert control's fill
--gd-surface-inverse#14151A#24262Etoasts, code blocks, the ink CTA band — a surface that is dark in BOTH themes
--gd-surface-solid#14151A#F2F2F4the neutral high-emphasis CTA fill — near-black on a light page, near-white on a dark one. NOT `surface-inverse`, which stays dark in both
--gd-surface-map#F0EEE9#22242Bthe basemap ground. Deliberately not black in the dark, so parcel strokes and overlay fills keep the relationship to their ground that they have in daylight
--gd-surface-scrimrgba(20, 21, 26, 0.42)rgba(0, 0, 0, 0.55)the dimmer behind modal surfaces — dialog, drawer, palette. Deeper in the dark, because 42% charcoal over a charcoal page barely reads as a veil
--gd-surface-on-accentrgba(255, 255, 255, .10)rgba(255, 255, 255, .10)a card or row sitting ON a filled accent band
--gd-surface-on-accent-strongrgba(255, 255, 255, .16)rgba(255, 255, 255, .16)a chip on a filled accent band — one step up from the card it sits in
Re-derived status
--gd-success#1B8A4C#8FBF87success fill — dots, icon tiles, progress bars. Never set type in it AA-nontext on --gd-surface
--gd-success-ink#15703D#8FBF87the only success value that may carry text. Re-derived for dark rather than lightened — a tint that reads as caution on white reads as sickly on charcoal AA on --gd-surface
--gd-success-wash#F1F7F2color-mix(in srgb, #8FBF87 14%, #17181E)the success ground — alerts, callouts, washed chips
--gd-success-tint#CFE3D5color-mix(in srgb, #8FBF87 34%, #17181E)the border of a success wash
--gd-success-on-solid#FFFFFF#14151Athe GLYPH on a filled success tile. Non-text: these tiles carry a 15px icon, never prose — success at 4.4:1 would not clear AA for a sentence AA-nontext on --gd-success
--gd-info#2F6FB0#7FB3E6info fill — dots, icon tiles, progress bars. Never set type in it AA-nontext on --gd-surface
--gd-info-ink#285F96#7FB3E6the only info value that may carry text. Re-derived for dark rather than lightened — a tint that reads as caution on white reads as sickly on charcoal AA on --gd-surface
--gd-info-wash#EDF3FAcolor-mix(in srgb, #7FB3E6 14%, #17181E)the info ground — alerts, callouts, washed chips
--gd-info-tint#C5DAEEcolor-mix(in srgb, #7FB3E6 34%, #17181E)the border of a info wash
--gd-info-on-solid#FFFFFF#14151Athe GLYPH on a filled info tile. Non-text: these tiles carry a 15px icon, never prose — success at 4.4:1 would not clear AA for a sentence AA-nontext on --gd-info
--gd-caution#A8850F#DDB63Fcaution fill — dots, icon tiles, progress bars. Never set type in it AA-nontext on --gd-surface
--gd-caution-ink#8A6D09#D9B34Athe only caution value that may carry text. Re-derived for dark rather than lightened — a tint that reads as caution on white reads as sickly on charcoal AA on --gd-surface
--gd-caution-wash#FCF6E6color-mix(in srgb, #DDB63F 14%, #17181E)the caution ground — alerts, callouts, washed chips
--gd-caution-tint#EBD9A2color-mix(in srgb, #DDB63F 34%, #17181E)the border of a caution wash
--gd-caution-on-solid#FFFFFF#14151Athe GLYPH on a filled caution tile. Non-text: these tiles carry a 15px icon, never prose — success at 4.4:1 would not clear AA for a sentence AA-nontext on --gd-caution
--gd-danger#C43D3D#E97B7Bdanger fill — dots, icon tiles, progress bars. Never set type in it AA-nontext on --gd-surface
--gd-danger-ink#A83232#E97B7Bthe only danger value that may carry text. Re-derived for dark rather than lightened — a tint that reads as caution on white reads as sickly on charcoal AA on --gd-surface
--gd-danger-wash#FCEFEFcolor-mix(in srgb, #E97B7B 14%, #17181E)the danger ground — alerts, callouts, washed chips
--gd-danger-tint#EFCACAcolor-mix(in srgb, #E97B7B 34%, #17181E)the border of a danger wash
--gd-danger-on-solid#FFFFFF#14151Athe GLYPH on a filled danger tile. Non-text: these tiles carry a 15px icon, never prose — success at 4.4:1 would not clear AA for a sentence AA-nontext on --gd-danger
Selecting a theme
Two hooks, deliberately. The media query follows the operating system unless the user has chosen; the attribute is that choice.
// follow the OS — the default, no attribute at all document.documentElement.removeAttribute('data-gd-theme') // pin it document.documentElement.setAttribute('data-gd-theme', 'dark')Neither hook is a control. The switch a person actually presses is Theme toggle, which cycles all three choices — and which deliberately stamps nothing before paint, because that is a head concern the app has to add for itself.
An app that ships light-only should say so with color-scheme: light rather than by shipping half a palette. Half a dark theme is a bug report.