diff --git a/AGENTS.md b/AGENTS.md index 31a11d2f..d3bdd57f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,6 +99,7 @@ The `docs/` directory contains **what** applies to this project: - `docs/development.md` - Coding agreements: function visibility and using `drupal_helpers` - `docs/content-types.md` - Adding custom node bundles and the theme layer they need +- `docs/brand-colours.md` - The brand palette, where it lives and how to change it - `docs/automated-lists.md` - Automated list component and its pagination - `docs/testing.md` - Testing conventions and agreements - `docs/ci.md` - CI provider and configuration diff --git a/config/default/drevops.settings.yml b/config/default/drevops.settings.yml index 79f1f081..e294c6cd 100644 --- a/config/default/drevops.settings.yml +++ b/config/default/drevops.settings.yml @@ -72,53 +72,53 @@ components: use_media_name: 1 colors: use_color_selector: 1 - use_brand_colors: 1 + use_brand_colors: 0 brand: light: - brand1: '#42748a' - brand2: '#f2f5f8' - brand3: '#d16313' + brand1: '#1e7582' + brand2: '#eff6ff' + brand3: '#1e7582' dark: - brand1: '#90cfeb' - brand2: '#2b3f53' - brand3: '#d16314' + brand1: '#96e7f4' + brand2: '#152235' + brand3: '#96e7f4' palette: light: - background: '#f2f5f8' - border: '#606263' - highlight: '#d16313' - heading: '#1a2e37' - body: '#3d4548' - background_light: '#fdfefe' - background_dark: '#c1c4c6' - border_light: '#b5b7ba' - border_dark: '#181818' - interaction_text: '#fcfdfd' - interaction_background: '#42748a' - interaction_hover_text: '#fcfdfd' - interaction_hover_background: '#274552' - interaction_focus: '#ff6b6b' - information: '#007ebd' - warning: '#c95100' - error: '#ce3936' - success: '#008482' + background: '#eff6ff' + border: '#8fa0b8' + highlight: '#1e7582' + heading: '#152235' + body: '#2b394d' + background_light: '#ffffff' + background_dark: '#c8d9f3' + border_light: '#c8d9f3' + border_dark: '#425166' + interaction_text: '#ffffff' + interaction_background: '#1e7582' + interaction_hover_text: '#ffffff' + interaction_hover_background: '#045a65' + interaction_focus: '#cd5b43' + information: '#326971' + warning: '#a37a00' + error: '#b7202e' + success: '#208436' dark: - background: '#2b3f53' - border: '#405264' - highlight: '#d16314' - heading: '#f9fcfe' - body: '#eef7fc' - background_light: '#35485b' - background_dark: '#1e2c3a' - border_light: '#b4bbc2' - border_dark: '#1e2c3a' - interaction_text: '#2b3f53' - interaction_background: '#90cfeb' - interaction_hover_text: '#1e2c3a' - interaction_hover_background: '#bce2f3' - interaction_focus: '#ff6b6b' - information: '#4dc4fd' - warning: '#e38444' - error: '#e85653' - success: '#14b0ae' + background: '#152235' + border: '#425166' + highlight: '#96e7f4' + heading: '#ffffff' + body: '#eff6ff' + background_light: '#2b394d' + background_dark: '#030d1e' + border_light: '#5b6a80' + border_dark: '#2b394d' + interaction_text: '#030d1e' + interaction_background: '#96e7f4' + interaction_hover_text: '#030d1e' + interaction_hover_background: '#e1fbff' + interaction_focus: '#ff9c86' + information: '#9cccd3' + warning: '#ffcf3d' + error: '#ea858f' + success: '#8ce3a0' optouts: { } diff --git a/docs/README.md b/docs/README.md index 05f215a3..3564a01c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ applies to this project. For **how** to perform operations, see - [Development](development.md) - Coding agreements: function visibility and using `drupal_helpers` - [Content types](content-types.md) - Adding custom node bundles and the theme layer they need +- [Brand colours](brand-colours.md) - The brand palette, where it lives and how to change it - [Testing](testing.md) - Testing conventions and agreements - [CI](ci.md) - Continuous integration configuration - [Deployment](deployment.md) - Deployment configuration diff --git a/docs/brand-colours.md b/docs/brand-colours.md new file mode 100644 index 00000000..04d76bfe --- /dev/null +++ b/docs/brand-colours.md @@ -0,0 +1,99 @@ +# Brand colours + +The brand defines six colours. https://brand.drevops.com/ is the source of truth for them, and for the ratio they are meant to appear in: white and Cool White 56%, Navy 28%, Teal 12%, Coral 4%. + +| Name | Hex | Role | +|---|---|---| +| Navy | `#152235` | Primary ground: structure, chrome, headers, footers, dark panels | +| Deep Navy | `#030d1e` | The deepest ground layer | +| Cool White | `#eff6ff` | The light surface | +| Teal | `#1e7582` | Accent on light backgrounds: links, kickers, rules, ticks, the single call to action | +| Bright Cyan | `#96e7f4` | Replaces Teal on Navy backgrounds | +| Coral | `#cd5b43` | Problems, warnings, "before" states, risk labels | + +Steps between those six come from the ramps in `static-prototype/framework.css` (`--color-primary-*` around Navy, `--color-secondary-*` around Teal and Bright Cyan, `--color-tertiary-*` around Coral, plus `pass`, `fail`, `warn` and `info` ramps for status). They are the generated ramps around the brand anchors, so a shade that the brand does not name is taken from there rather than invented. + +## Where the palette lives + +CivicTheme renders from 18 named palette slots per theme. The site carries them in two places, and both have to be edited together. + +| File | What it is | +|---|---| +| `config/default/drevops.settings.yml` | The source of truth. `CivicthemeColorManager` compiles `colors.palette` into `public://css-variables.drevops.css` as `--ct-color-{light,dark}-{slot}` | +| `web/themes/custom/drevops/components/variables.base.scss` | The Storybook fallback. Component SCSS compiles `ct-color-light('x')` to `var(--ct-color-light-x, )`, and Storybook has no generated stylesheet, so the fallback is what it renders | + +The config uses underscores (`background_light`), the SCSS map uses hyphens (`background-light`). + +`colors.brand` never reaches CSS. It seeds the Brand colors fieldset in the theme settings form, where a JS handler recomputes the palette fields from a brand swatch. `use_brand_colors` is `0`, which hides that fieldset, so the recompute cannot overwrite the palette. + +## The palette + +| Slot | Light | | Dark | | +|---|---|---|---|---| +| `heading` | `#152235` | Navy | `#ffffff` | White | +| `body` | `#2b394d` | Navy 8 | `#eff6ff` | Cool White | +| `background_light` | `#ffffff` | White | `#2b394d` | Navy 8 | +| `background` | `#eff6ff` | Cool White | `#152235` | Navy | +| `background_dark` | `#c8d9f3` | Navy 2 | `#030d1e` | Deep Navy | +| `border_light` | `#c8d9f3` | Navy 2 | `#5b6a80` | Navy 6 | +| `border` | `#8fa0b8` | Navy 4 | `#425166` | Navy 7 | +| `border_dark` | `#425166` | Navy 7 | `#2b394d` | Navy 8 | +| `interaction_text` | `#ffffff` | White | `#030d1e` | Deep Navy | +| `interaction_background` | `#1e7582` | Teal | `#96e7f4` | Bright Cyan | +| `interaction_hover_text` | `#ffffff` | White | `#030d1e` | Deep Navy | +| `interaction_hover_background` | `#045a65` | Teal 7 | `#e1fbff` | Cyan 1 | +| `interaction_focus` | `#cd5b43` | Coral | `#ff9c86` | Coral 3 | +| `highlight` | `#1e7582` | Teal | `#96e7f4` | Bright Cyan | +| `information` | `#326971` | Info 8 | `#9cccd3` | Info 4 | +| `warning` | `#a37a00` | Amber 8 | `#ffcf3d` | Amber 5 | +| `error` | `#b7202e` | Red 7 | `#ea858f` | Red 4 | +| `success` | `#208436` | Green 8 | `#8ce3a0` | Green 4 | + +`highlight` is the stripe accent: the bar on callouts, blockquotes, accordions and cards, the active primary-navigation border, and the table-of-contents hover marker. It is also what the eyebrow kicker and rich-text table captions use, through `assets/sass/_eyebrow.scss`. + +`warning` uses the amber ramp rather than Coral. The brand assigns Coral to warnings, but that is about authored content - a risk label, a "before" column - and the four status slots sit next to each other in form and message chrome, where amber, red, green and teal-grey stay distinguishable in a way that two adjacent reds do not. + +## Every slot is set, none is derived + +CivicTheme can derive all 18 slots from three brand anchors, using formulas such as `heading = brand1|shade,60`. Those filters mix with pure black and pure white, which desaturates: deriving from Teal and Cool White produces a grey `#38484a` body and a grey `#bfc5cc` surface rather than steps along the brand's blue ramp. Every slot is therefore written out. + +The consequence is that changing a brand colour changes nothing on its own. A rebrand means editing the 18 values in both files. + +## Contrast + +Every pair the palette puts together clears WCAG AA. The tight ones are worth knowing about: + +| Pair | Ratio | +|---|---| +| Teal on white | 5.36:1 | +| Teal on Cool White | 4.92:1 | +| White on the Teal button | 5.36:1 | +| Bright Cyan on Navy | 11.5:1 | +| Deep Navy on the Bright Cyan button | 13.9:1 | +| `#a37a00` warning on white | 3.93:1 | + +The warning amber is only ever a border, which needs 3:1 rather than 4.5:1. Moving it to a text role means darkening it to `#705400` first. + +`interaction_focus` cannot clear 3:1 against both white and the Teal button, because the two requirements point in opposite directions for any single colour. CivicTheme's shipped default has the same limitation. The chosen values are the ones that work on the page backgrounds, which is where the offset ring actually sits. + +## Applying a change + +The compiled stylesheet is written once and then left alone, so a config change on its own leaves the old palette on disk: + +```bash +ahoy drush cim -y +ahoy drush cr +``` + +`ThemeColorSubscriber` purges the stylesheet when the colour config is saved, and `do_base_deploy_refresh_theme_colors()` purges it on every deploy, so an import is enough. For the SCSS side: + +```bash +ahoy cli npm run build --prefix web/themes/custom/drevops +``` + +Check both themes after a change. Light and dark are separate palettes and a slot can be right in one and wrong in the other. + +## Related + +- [Front-end performance](performance.md) - image styles, self-hosted fonts and layout stability +- [Content types](content-types.md) - adding a bundle and the theme layer it needs diff --git a/web/themes/custom/drevops/assets/sass/_eyebrow.scss b/web/themes/custom/drevops/assets/sass/_eyebrow.scss index 3412ab4b..28d8240c 100644 --- a/web/themes/custom/drevops/assets/sass/_eyebrow.scss +++ b/web/themes/custom/drevops/assets/sass/_eyebrow.scss @@ -2,16 +2,12 @@ // Eyebrow. // // Small, uppercase kicker label intended to sit above a heading. The brand -// palette has no orange, so this colour is a deliberate accent rather than a -// CivicTheme token; adjust `$ct-eyebrow-color` to taste. The chosen value -// clears WCAG AA contrast on light backgrounds. +// assigns kickers the same accent it assigns rules, which is what the +// `highlight` palette slot holds: teal on light bands, bright cyan on dark. // -$ct-eyebrow-color: #c1430a !default; - -// The orange reads too dark against dark bands, so those flip to the -// palette's light-blue interaction accent, which clears WCAG AA there. -$ct-eyebrow-color-dark: #90cfeb !default; +$ct-eyebrow-color: ct-color-light('highlight') !default; +$ct-eyebrow-color-dark: ct-color-dark('highlight') !default; .eyebrow { display: block; diff --git a/web/themes/custom/drevops/components/00-base/spacing/spacing.stories.scss b/web/themes/custom/drevops/components/00-base/spacing/spacing.stories.scss index 45526966..015b48e0 100644 --- a/web/themes/custom/drevops/components/00-base/spacing/spacing.stories.scss +++ b/web/themes/custom/drevops/components/00-base/spacing/spacing.stories.scss @@ -24,7 +24,7 @@ $transparency: if($index % 2 == 0, 25%, 50%); - background-color: color.mix(#61daff, white, $transparency); + background-color: color.mix(#96e7f4, white, $transparency); &::before { content: 'spacing #{$name}'; diff --git a/web/themes/custom/drevops/components/variables.base.scss b/web/themes/custom/drevops/components/variables.base.scss index 4e36cd0d..40cffe3b 100644 --- a/web/themes/custom/drevops/components/variables.base.scss +++ b/web/themes/custom/drevops/components/variables.base.scss @@ -9,27 +9,66 @@ // // stylelint-disable scss/dollar-variable-pattern +// The brand palette. These values are the Storybook fallbacks baked into +// `var(--ct-color-*, )`; in Drupal the generated stylesheet defines +// the same variables from `config/default/drevops.settings.yml`, which is the +// source of truth. Both must be edited together - see `docs/brand-colours.md`. $ct-colors-brands: ( 'light': ( - 'brand1': #00698f, - 'brand2': #e6e9eb, - 'brand3': #121313, + 'brand1': #1e7582, + 'brand2': #eff6ff, + 'brand3': #1e7582, ), 'dark': ( - 'brand1': #61daff, - 'brand2': #003a4f, - 'brand3': #00698f, + 'brand1': #96e7f4, + 'brand2': #152235, + 'brand3': #96e7f4, ) ); + +// Every slot is set explicitly rather than derived from the brand colours +// above: CivicTheme derives by mixing with pure black and white, which pulls +// the intermediate steps off the brand's blue ramp and toward grey. $ct-colors: ( 'light': ( - 'success': green, - 'custom1': brown, + 'heading': #152235, + 'body': #2b394d, + 'background-light': #fff, + 'background': #eff6ff, + 'background-dark': #c8d9f3, + 'border-light': #c8d9f3, + 'border': #8fa0b8, + 'border-dark': #425166, + 'interaction-text': #fff, + 'interaction-background': #1e7582, + 'interaction-hover-text': #fff, + 'interaction-hover-background': #045a65, + 'interaction-focus': #cd5b43, + 'highlight': #1e7582, + 'information': #326971, + 'warning': #a37a00, + 'error': #b7202e, + 'success': #208436, ), 'dark': ( - 'custom1': red, - 'custom2': orange, - 'custom3': blue, + 'heading': #fff, + 'body': #eff6ff, + 'background-light': #2b394d, + 'background': #152235, + 'background-dark': #030d1e, + 'border-light': #5b6a80, + 'border': #425166, + 'border-dark': #2b394d, + 'interaction-text': #030d1e, + 'interaction-background': #96e7f4, + 'interaction-hover-text': #030d1e, + 'interaction-hover-background': #e1fbff, + 'interaction-focus': #ff9c86, + 'highlight': #96e7f4, + 'information': #9cccd3, + 'warning': #ffcf3d, + 'error': #ea858f, + 'success': #8ce3a0, ), ); diff --git a/web/themes/custom/drevops/components/variables.components.scss b/web/themes/custom/drevops/components/variables.components.scss index 5d2b3b77..3488d652 100644 --- a/web/themes/custom/drevops/components/variables.components.scss +++ b/web/themes/custom/drevops/components/variables.components.scss @@ -65,7 +65,6 @@ $ct-icon-sizes: ( 'm': (ct-particle-px(3.5), ct-particle-px(3.5)), ), ); -$ct-back-to-top-light-background-color: ct-color-light('custom1'); // // Banner.