Repository navigation
Update Winter logos & use CSS variables for backend colour - #1541
Merged
Merged
Conversation
Replace the Winter wordmark logos with the new light/dark pair and refresh
the supporting icons. The old names described the brand ("winter-logo"); the
new ones describe where they are used, so a re-brand is a file swap rather
than a rename plus a stylesheet edit.
- winter-logo.svg -> logo-dark.svg (used on light backgrounds)
- winter-logo-white.svg -> logo-light.svg (used on dark backgrounds)
- logo.svg dropped: nothing referenced it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fancy form toolbar renders the delete button's title via an :after pseudo-element. The CMS toolbar renders the same button icon-only with no title, where the empty :after still contributed its margin-left as dead space to the right of the icon. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Core had ~717 colour literals spread across 91 LESS files, so dark mode and
theming meant hunting every rule that painted something. This introduces a
single token block and rewrites those literals to read from it.
Every rewrite keeps the original value as the var() fallback:
color: #666666; -> color: var(--wn-text, #666666);
which means compiled output is unchanged if the token block is ever absent,
and the change is exactly reversible.
661 of 717 literals now resolve through 164 tokens in
modules/system/assets/ui/less/tokens.less, imported by storm.less. The
tokens fall into three groups:
- Neutral scale (28) snapped onto the design system's slate/indigo ramps.
These do change colour; the largest shift is body text #666666 -> #445a6b.
- Identity colours (68) named from the LESS variable they already sat on
(flash, callout, chart, file-type icons). Values preserved exactly.
- Inline colours (68) with no variable to borrow a name from, named by source
file and role. Values preserved exactly.
The remainder stay literal: LESS evaluates darken()/mix()/saturate() at
compile time, so a variable feeding one cannot become a var().
Verified by collapsing the tokens back out of the compiled CSS and comparing
to a pristine build: no selector paints a different colour set, and no rule
was dropped, added or malformed. WCAG contrast was measured across every
text/background token pair that co-occurs on an element; nothing that passed
AA now fails.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three problems in the fancy form layout, all visible to keyboard users. Tabs had no usable focus indicator. The visible tab is not the anchor's box: it is span.title plus two pseudo-elements skewed +/-20deg hanging off either end, so the browser's outline drew a rectangle straight through the angled corners. Replaced with an inset bar on the three pieces that form the shape; drawn pre-transform, so it follows the skew. The UA ring is suppressed on :focus rather than :focus-visible, or it reappears on mouse click. Toolbar buttons had no focus styling at all. They are transparent, text-only and have their box-shadow removed, and only opacity changed on :hover, so keyboard focus was indistinguishable from rest. .btn carries a global `outline: none !important`, so the ring is drawn with box-shadow instead. Focus colour is picked by backdrop: white on the dark master strip, and #103141 on the brand strip, where white reaches only 2.8:1 -- below the 3:1 WCAG 1.4.11 asks of a focus indicator. Two related fixes: - Focusing a tab shifted the whole strip up 2px, permanently. A tab is 2-3px taller than the strip and that overhang is what merges the active tab into the panel; the strip is `overflow: hidden`, which still makes it a scroll container, so the browser scrolled the overhang into view and the offset survived blur. A negative scroll-margin shrinks the box the browser tries to reveal. (`overflow: clip` is not available: the strip must stay horizontally scrollable for drag.scroll.js, and clip beside a scrolling axis computes back to hidden.) - The fancy tab strip, inactive tab and active tab sat within ΔE 2.3-4.4 of each other, so the inactive tab was nearly invisible. They now derive from the brand colour via color-mix at ~11 ΔE steps, which also means they track custom branding at runtime rather than baking a darken() at compile time. The derived tokens are declared inside @supports: where color-mix is unsupported the token stays undefined and each use site falls back to its literal, which is the only case a var() fallback covers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This comment was marked as resolved.
This comment was marked as resolved.
LukeTowers
force-pushed
the
wip/colour-tokens-experiment
branch
from
September 14, 2026 19:22
2f391b9 to
dbd532a
Compare
The header told contributors to edit tokens-values.json and re-run apply.php. Neither exists in this repository -- they live in a separate plugin repo -- so the instruction could not be followed from core. Reworded so the file reads as maintained directly, which is how anyone working in core will treat it. Each entry still records the literal it replaced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From CodeRabbit and Copilot review on #1541. Compiled CSS that was never rebuilt. export.less, import.less, relation.less and mediafinder.less are not registered as asset bundles, so `winter:util compile less` never touches them and their committed CSS still carried the pre-token literals -- the LESS changes were inert. Rebuilt through the same CombineAssets path winter:util uses, unminified to match how these four were originally built. This also picks up drift that predates this PR: import.css still had @brand-danger's old #ab2a1c baked in. Rich editor reverted. richeditor.less imports the DRM-gated froala vendor LESS, so its CSS cannot be built here at all, and default_styles.less is the default body of a user-editable setting that Froala renders inside an iframe, where custom properties from the parent document never reach. Tokens there could not take effect, so those four files go back to literals. Print styles detached from the theme. site.print.less is entirely inside @media print and also forces backgrounds transparent, so a dark theme redefining the tokens would print near-white ink onto white paper. All six declarations are literal again, with a note saying why. Table error row. The pink #fbecec error background was routed to --wn-surface-hover (a cool neutral) -- close enough in deltaE to pass the fit guard, wrong semantically. It gets its own --wn-surface-error. Renamed --wn-table-border to --wn-table-error: it holds the error red and is used for a background too, so the old name was misleading. Partially tokenised declarations. The appliers rewrote only the first literal per line, leaving checkerboard gradients and two-sided inset shadows half tokenised, so an override applied to some stops and not others. Filled in 28 across 4 files. Flash close button. `color: white` was hardcoded, so it ignored overrides of --wn-flash-text; it now follows @color-flash-text. Dead border fallback. .tt-dropdown-menu declares border twice, the rgba() unconditionally overriding the tokenised one -- the legacy no-rgba fallback. Tokenising a declaration that can never take effect is worse than leaving it, so it is literal again with a note. Token count 168 -> 164; no undefined references and none unused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The earlier passes matched hex literals only, so every rgba() in core was
invisible to them -- 185 declarations of shadow, overlay and highlight that a
theme still had to override selector by selector.
106 of the 116 rgba() values in core LESS are pure black or pure white at 37
different alphas. Those are not 37 colours, they are two colours used at many
strengths, so the token holds the CHANNEL TRIPLET and the alpha stays at the
call site:
box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
-> box-shadow: 0 1px 2px rgba(var(--wn-scrim-dark, 0, 0, 0), .15);
Redefining --wn-scrim-dark then moves all of them at once while each keeps its
own strength, which is what a dark theme actually wants -- shadows there are
rarely pure black.
Verified in-browser before applying: LESS passes the construct through
untouched, rgba() accepts a custom property expanding to "r, g, b", and the
var() fallback still works when the token is absent (everything after the
first comma is the fallback), so the "absent token degrades to the original
value" guarantee holds. No @supports needed -- rgba() and custom properties
are universally supported.
Genuinely coloured rgba values are left alone: they are identity.
rgba declarations in compiled core CSS: 185 -> 52, and 36 of the remainder are
in richeditor.css, the DRM-gated Froala output that cannot be rebuilt here.
Overall token coverage of the colour surface: 44% -> 52%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scrim pass put the channel triplet in a token and left alpha at the call site, so a theme could change a shadow's hue but not its strength. Hue is the axis nobody needs: shifting rgba(0,0,0,.2) to rgba(13,17,23,.2) on a dark surface is visually almost identical. The real dark-mode problem with a shadow is that it is invisible against a dark background, which you fix by raising alpha, swapping to a border, or dropping it -- none of which the token reaches. The same applies to the white highlights: you would want alpha 0, not a different white. So those 106 declarations looked themeable without being themeable. Reverted to literals, which is at least honest about what they are. If shadows should be themed later, the useful shape is a small set of role tokens holding the COMPLETE rgba (--wn-shadow-sm, --wn-overlay-backdrop, ...), which means deciding that .12/.14/.15/.16 are the same shadow -- a design call worth making deliberately, alongside the dark-mode pass, not inferred here. Also from review: - mailbrandsetting/custom.less is rendered into email, where :root custom properties never arrive and a client that does not understand var() discards the whole declaration rather than using the fallback inside it. It should never have been tokenised; reverted with a note. - tokens.less emitted standalone `//` separators, which stylelint flags as scss/comment-no-empty. The header no longer produces them. Original rgba spelling restored occurrence-by-occurrence so the revert leaves no whitespace churn: the only rgba change against develop is the deliberate fancy inactive-tab label alpha (.35 -> .8) from the contrast fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…experiment # Conflicts: # modules/system/assets/ui/less/list.variables.less # modules/system/assets/ui/storm.css
From CodeRabbit review on #1541. The fancy delete button's :hover still hard-coded #bc4436 / #fff, so a theme overriding --wn-fancylayout-bg or --wn-text-inverse changed the resting state and hovering snapped back to the stock colours. It now uses a new --wn-fancylayout-bg-hover alongside --wn-text-inverse. The framework and flashmessage warning flashes sat on @brand-warning, a variable reference rather than a literal, so the hex sweep never saw them. They get their own --wn-framework-flash-warning-bg and --wn-flashmessage-flash-warning-bg, matching their success/error/info siblings. The suggested reuse of --wn-flash-warning-bg would have recoloured them: that token holds Snowboard's #b87410, while these compile to #de8754. Compiled output changes only in those three rules plus the new token declarations. Token count 166 -> 169; no undefined references and none unused.
LukeTowers
added a commit
to wintercms/wn-tailwindui-plugin
that referenced
this pull request
Sep 26, 2026
The first version of these lived only in a session scratchpad and was lost
when it was cleared, taking the ability to revert or re-run the change with
it. They are in git now for that reason.
- apply.php / unapply.php replay and exactly reverse the token rewrites
- build.sh compiles BOTH asset pipelines (winter:util for
winter.css, mix for storm.css) with plugins
disabled, then restores their state
- verify.php collapses the tokens back out of the compiled CSS
and proves no selector paints a different colour set
- contrast.php WCAG 2.1 before/after, only for token pairs that
actually co-occur on an element
- extract-map.php re-captures token-map.json from the applied tree
- lab.php CIELAB / deltaE / chroma / WCAG helpers
token-map.json was captured from the applied tree rather than by re-running
the original role-inference heuristics: those were tuned interactively and
are not reproducible, whereas reading the tree back cannot drift from what
was built and reviewed.
Changing a colour is: edit tokens-values.json, apply.php, build.sh.
tokens.less in core is generated and must not be hand-edited.
verify.php's baselines are generated (--capture) and go stale as soon as core
moves, so they are gitignored rather than committed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LukeTowers
added a commit
to wintercms/wn-tailwindui-plugin
that referenced
this pull request
Sep 26, 2026
HANDOFF.md still said the light-mode colour cleanup was unstarted ("Nothing
here is applied to core yet — it's the plan + approval gate"). It is now open
as wintercms/winter#1541.
HANDOFF-token-experiment.md is rewritten from a mid-experiment parking doc
into a record of what shipped, where the tooling is, and what is still open:
the 56 literals LESS colour functions keep locked, 11 pre-existing contrast
failures that are now one-line fixes, and the absence of any test guarding
that every var(--wn-*) resolves.
It also keeps the traps that cost real time, notably: there are two asset
pipelines and tokens only ship in one of them; a var() fallback does NOT
rescue a defined-but-invalid value, so a token needing a feature guard must
be declared inside @supports; verify.php validates the plumbing, not the
palette, so it cannot catch a bad colour; and `overflow: hidden` is still a
scroll container, which is what made the tab strip jump on focus.
color-consolidation.html and color-token-map.html are marked superseded --
their groupings predate the fit guard that shipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LukeTowers
added a commit
to wintercms/wn-tailwindui-plugin
that referenced
this pull request
Sep 26, 2026
Adds a sticky bar with a progress meter, per-group approve/override chips with a note field, and a Copy-decisions button that emits both a readable summary and a JSON block. State persists to localStorage; the clipboard write falls back to a hidden textarea + execCommand, which is what works when the report is opened over file://. Kept for reference: the groupings in this report predate the fit guard that shipped in wintercms/winter#1541, so they no longer match the applied token set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LukeTowers
added a commit
to wintercms/wn-tailwindui-plugin
that referenced
this pull request
Sep 26, 2026
wintercms/winter#1541 shipped with review changes the tooling never saw: the rgba scrim tokens and several misrouted identity tokens were reverted, and five tokens were added in the final round (--wn-list-selection-bg/-border, --wn-framework-flash-warning-bg, --wn-flashmessage-flash-warning-bg, --wn-fancylayout-bg-hover). tokens-values.json now matches core exactly: apply.php run against develop regenerates tokens.less byte-for-byte and rewrites nothing. token-map.json was recaptured from the merged tree with extract-map.php (661 rewrites, 83 files, 169 tokens).
LukeTowers
added a commit
to wintercms/wn-tailwindui-plugin
that referenced
this pull request
Sep 26, 2026
The brand asset refresh in wintercms/winter#1541 removed winter-logo-white.svg. The auth layouts and Plugin.php were updated for it, but the side-menu partial still requested the old file, so the backend showed a broken image wherever no custom logo is set. logo-light.svg is the variant with white lettering for dark backgrounds.
LukeTowers
added a commit
to wintercms/wn-tailwindui-plugin
that referenced
this pull request
Sep 26, 2026
Core now paints through --wn-* tokens (wintercms/winter#1541), so dark mode can redefine those instead of overriding each rule that uses them. The .dark block now maps the neutral tokens onto the existing --drk-* palette by role (text, surfaces, borders, inputs, hover, selection), plus the few identity tokens this file already repainted consistently. That makes 86 declarations here redundant; they are removed, and 48 rules left empty with them. Each removal was verified rather than assumed: on 25 backend pages in dark mode, every element's and pseudo-element's colours, borders, outlines and shadows were captured, the token block applied, and a declaration removed only if doing so changed nothing on any page where it matched. Declarations that matched nothing on those pages are kept, since their redundancy could not be shown. The token block also darkens elements this file never covered, which were rendering their light-mode colours in dark mode: dark slate text and icons on dark surfaces (pagination, record navigation, file upload names, component list), light #d1dbe0 borders and placeholders, the recordfinder and datepicker panels, sweet-alert text and scoreboard values. The switch knob is pinned light: core paints it with --wn-surface-hover, which is now dark. Light mode is untouched: every change is scoped under .dark, and the same 25 pages capture identically in light mode before and after.
LukeTowers
added a commit
to wintercms/wn-tailwindui-plugin
that referenced
this pull request
Sep 26, 2026
Dark mode now depends on core's --wn-* tokens (wintercms/winter#1541), which ship in Winter 1.2.15; on an older core the tokens are absent and the surfaces they cover would stay light. The constraint follows the other first-party plugins in also allowing dev-develop.
LukeTowers
added a commit
to wintercms/wn-tailwindui-plugin
that referenced
this pull request
Sep 26, 2026
The plugin now requires the Winter release with colour tokens, which itself requires PHP 8.1, so the plugin's ">=7.2" was no longer true; it also covers the audit tooling's use of PHP 8 functions. Handoff steps B and C still told the next maintainer to approve the old consolidation report and replace the literals, work that has shipped in wintercms/winter#1541 in a form that report no longer matches. They now say so and point at the maintained tooling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Core's backend colours were hard-coded as literals across ~90 LESS files, so dark mode or any theming meant hunting down every rule that painted something. This routes them through a single token block and fixes the accessibility problems that surfaced while reviewing the result.
How the tokenisation works
Every rewrite keeps the original value as the
var()fallback:So compiled output is unchanged if the token block is ever absent, and the change can be reversed exactly.
653 colour sites across 83 LESS files now resolve through 169 tokens in
modules/system/assets/ui/less/tokens.less. That file is imported bystorm.less, so the:rootblock ships instorm.css. The tokens fall into these groups:color-mixtokens.lessis maintained directly, and each entry records the literal it replaced.What stays literal, and why
87 hex literals remain, each one deliberately:
darken()/mix()/saturate()at build time, so a variable feeding one cannot become avar(). These are concentrated inglobal.variables.less,global.mixins.gradient.lessand the other*.variables.less/mixin files. For example,@color-list-selection-textfeeds adarken()on hover.richeditor.lessimports the DRM-gated Froala vendor LESS, so its CSS can't be rebuilt from core. The.fr-viewstyles in_base_styles.lessandeditorsetting/default_styles.lessare rendered inside Froala's iframe, where:rootcustom properties from the parent document never reach.site.print.lessforces backgrounds transparent, so a dark theme redefining the tokens would print near-white text onto white paper..tt-dropdown-menuininstall.lessdeclares its border twice, and thergba()line always overrides the hex one.Also left alone:
var()discards the whole declaration.rgba()shadows and overlays: a token pass over these was tried and reverted. Tokenising the channel triplet lets a theme change a shadow's hue but not its strength, and strength is what dark mode actually needs. Role tokens holding a completergba()are the better shape, and belong with the dark-mode pass.What actually changes colour
#666666 -> #445a6b.Everything else (identity and inline colours) is byte-identical to before.
Accessibility fixes
Reviewing the result surfaced three pre-existing problems in the fancy form layout:
span.titleplus two pseudo-elements skewed ±20° hanging off either end, so the browser's outline drew a rectangle through the angled corners. Replaced with an inset bar on the three pieces that form the shape.box-shadowis removed, and only opacity changed on:hover..btncarries a globaloutline: none !important, so the ring is drawn withbox-shadow.overflow: hidden, which still makes it a scroll container. The browser scrolled the tab's intentional overhang into view, and the offset survived blur. Fixed with a negativescroll-margin.Focus colour is chosen by backdrop: white on the dark master strip, and
#103141on the brand strip, where white reaches only 2.8:1, below the 3:1 that WCAG 1.4.11 asks of a focus indicator.Separately, the fancy tab strip, inactive tab and active tab sat within ΔE 2.3–4.4 of each other, leaving the inactive tab nearly invisible. They now derive from the brand colour via
color-mixat ~11 ΔE steps, which also means they follow custom branding at runtime instead of baking adarken()in at compile time. Those tokens are declared inside@supports, so wherecolor-mixis unsupported the token stays undefined and each use site falls back to its literal. The inactive tab label alpha also goes from .35 to .8.Other changes
--wn-surface-error/--wn-table-errorinstead of borrowing a neutral hover token.export,import,relationandmediafinder. These aren't registered bundles, sowinter:util compile lessnever touches them, and without a rebuild the LESS changes would have been inert. The rebuild also picks up older drift:import.cssstill had@brand-danger's old#ab2a1cbaked in.Verification
.list-selectionrules from Add "select all records matching this query" to list bulk actions #1542.)winter:util compile lessforwinter.css, Mix forstorm.css) compile clean. Everyvar(--wn-*)reference resolves to a definition, and no token is unused.Known follow-ups
color-mixor precomputed pairs before dark mode can be a pure token redefinition.--wn-shadow-sm,--wn-overlay-backdrop, …) as part of the dark-mode pass.tokens.less.Summary by CodeRabbit
New Features
Style