feat(engine): swap ACCENT_PRIMARY from cyan #6fc2ef to brick red #a54242

Project-wide palette shift at user request. Replaces the cyan
primary accent everywhere it surfaces — splash boot screen,
home menu glyphs, action chevrons, replay overlay banner +
scrub fill + chip border, achievement checkmarks, leaderboard
#1 indicator, radial menu fill, focus ring, card-back canonical
badge, etc. — with `#a54242` from the same base16-eighties
family as the existing pink suit colour.

Knock-on changes that all land in this commit per the
lockstep rule:

- ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER
  (#c25e5e brightened companion), FOCUS_RING (same hue, 0.85
  alpha). Module-level palette comment + STOCK_BADGE_FG +
  CARD_SHADOW_ALPHA_DRAG doc strings updated to match.
- card_plugin.rs: card_back_colour(0) now returns the brick-red
  ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from
  cyan to lime #acc267 — the CBM alternative needs to stay
  hue-distinct from the new red-family primary, lime is the
  next-best non-red base16-eighties accent. text_colour doc
  + CBM tests renamed cyan→lime in lockstep
  (text_colour_color_blind_mode_swaps_red_suits_to_lime).
- card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical
  Terminal back).
- splash_plugin.rs / ui_modal.rs / replay_overlay.rs /
  selection_plugin.rs: descriptive "cyan" comments swapped to
  "accent" / "primary-accent" wording so the doc strings stay
  decoupled from any specific hue. Future palette tweaks won't
  require comment churn.
- design-system.md: YAML token frontmatter updated (primary,
  surface-tint, suit-red-cb, primary-container,
  on-primary-container, inverse-primary). Palette table gains
  a project-specific `base08` slot for the new red. CTA /
  Selection / Card-back badge / Primary button / Bottom-bar
  active-icon / glow / CBM swap text all retuned. Historical
  references preserved (e.g. "Was cyan #6fc2ef before the
  2026-05-08 swap") so the audit trail stays in the spec.
- card_face_svg_pin.rs: rebaselined. Exactly one hash drift
  (back_0 — the canonical Terminal back's badge changed
  colour). Other 56 hashes identical (face SVGs don't
  reference the accent; back_1..4 use unchanged accents). The
  one-hash-drift signal confirms the change scope was
  surgical.

Workspace clippy + cargo test --workspace clean, 1184 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-08 10:30:35 -07:00
parent d109c32b75
commit a292a7ead0
11 changed files with 79 additions and 64 deletions
@@ -13,7 +13,7 @@
fill="url(#scanlines)"/>
<!-- Top-left accent badge (the only theme-varying element). -->
<rect x="12" y="12" width="24" height="32" fill="#6fc2ef"/>
<rect x="12" y="12" width="24" height="32" fill="#a54242"/>
<!-- Bottom-right "▌RS" monogram in JetBrains-Mono-styled FiraMono. -->
<text x="244" y="368" font-family="Fira Mono" font-size="24"

Before

Width:  |  Height:  |  Size: 956 B

After

Width:  |  Height:  |  Size: 956 B

+4 -4
View File
@@ -38,11 +38,11 @@ const BACK_BORDER: &str = "#353535";
const BACK_MONOGRAM: &str = "#505050";
/// Five back-theme accent colours. Slot 0 is the canonical "Terminal"
/// back from the design system; the other four cycle through the
/// remaining base16-eighties accents so all 5 slots stay visually
/// distinct without leaving the palette.
/// back from the design system (matches `ACCENT_PRIMARY`); the other
/// four cycle through the remaining base16-eighties accents so all 5
/// slots stay visually distinct without leaving the palette.
pub const BACK_ACCENTS: [&str; 5] = [
"#6fc2ef", // 0 — cyan (Terminal canonical)
"#a54242", // 0 — brick red (Terminal canonical, ACCENT_PRIMARY)
"#acc267", // 1 — lime
"#e1a3ee", // 2 — lavender
"#fb9fb1", // 3 — pink
+19 -9
View File
@@ -98,12 +98,18 @@ pub struct CardImageSet {
}
/// Suit-colour swap for red-suit cards in colour-blind mode — Terminal
/// `#6fc2ef` (cyan). Replaces `RED_SUIT_COLOUR` (pink) when CBM is on,
/// `#acc267` (lime). Replaces `RED_SUIT_COLOUR` (pink) when CBM is on,
/// providing a hue-distinct alternative that survives the most common
/// red/green deficiencies. Pre-Terminal this was a *face tint*; the new
/// design moves CBM differentiation into the suit glyph colour itself
/// and keeps the face uniformly `CARD_FACE_COLOUR` regardless of CBM.
const RED_SUIT_COLOUR_CBM: Color = Color::srgb(0.435, 0.761, 0.937);
///
/// The CBM swap is lime (not the `ACCENT_PRIMARY` brick-red) because
/// the primary accent is itself in the red family — using it for
/// "the not-red CBM alternative" would defeat the purpose. Lime is
/// the next-best non-red base16-eighties accent; deuteranopia and
/// protanopia readers see it as visibly distinct from pink.
const RED_SUIT_COLOUR_CBM: Color = Color::srgb(0.675, 0.761, 0.404);
/// Returns the fallback card-back colour for the given unlocked card-back
/// index. Production renders backs from PNG artwork; this fallback only
@@ -112,7 +118,7 @@ const RED_SUIT_COLOUR_CBM: Color = Color::srgb(0.435, 0.761, 0.937);
/// in the same hue family as the on-disk PNG art for that index.
fn card_back_colour(selected_card_back: usize) -> Color {
match selected_card_back {
0 => Color::srgb(0.435, 0.761, 0.937), // #6fc2ef cyan (Terminal canonical)
0 => Color::srgb(0.647, 0.259, 0.259), // #a54242 brick red (Terminal canonical, ACCENT_PRIMARY)
1 => Color::srgb(0.675, 0.761, 0.404), // #acc267 lime
2 => Color::srgb(0.882, 0.639, 0.933), // #e1a3ee lavender
3 => Color::srgb(0.984, 0.624, 0.694), // #fb9fb1 pink
@@ -792,11 +798,15 @@ fn label_for(card: &Card) -> String {
/// Suit colour for the rank/suit overlay rendered atop the constant
/// fallback sprite (only fires under `MinimalPlugins` — production
/// renders the suit glyph baked into the PNG). When `color_blind` is
/// enabled, red-suit cards swap to `RED_SUIT_COLOUR_CBM` (cyan) — the
/// "Settings toggle swaps red→cyan" half of the design system's
/// enabled, red-suit cards swap to `RED_SUIT_COLOUR_CBM` (lime) — the
/// "Settings toggle swaps red→lime" half of the design system's
/// colour-blind support. The other half (always-on filled-vs-outlined
/// glyph differentiation for ♥♠ vs ♦♣) is baked into the PNG art and
/// has no constant-fallback equivalent.
///
/// The CBM swap is lime (not the new brick-red `ACCENT_PRIMARY`)
/// because the primary accent is itself red-family — the CBM
/// alternative needs to be hue-distinct from the original red suit.
fn text_colour(card: &Card, color_blind: bool) -> Color {
if card.suit.is_red() {
if color_blind {
@@ -2066,20 +2076,20 @@ mod tests {
// Pre-Terminal these were `face_colour` tests asserting that CBM
// tinted the *face background* of red-suit cards. The Terminal
// design system moves CBM differentiation into the suit *glyph*
// colour (red→cyan), so these tests now exercise `text_colour`.
// colour (red→lime), so these tests now exercise `text_colour`.
// -----------------------------------------------------------------------
#[test]
fn text_colour_color_blind_mode_swaps_red_suits_to_cyan() {
fn text_colour_color_blind_mode_swaps_red_suits_to_lime() {
let red_card = Card { id: 0, suit: Suit::Diamonds, rank: Rank::Queen, face_up: true };
let cbm_colour = text_colour(&red_card, true);
assert_eq!(
cbm_colour, RED_SUIT_COLOUR_CBM,
"color-blind mode must replace the red suit colour with the CBM cyan",
"color-blind mode must replace the red suit colour with the CBM lime",
);
assert_ne!(
cbm_colour, RED_SUIT_COLOUR,
"CBM red must be visibly distinct from the default red suit colour",
"CBM lime must be visibly distinct from the default red suit colour",
);
}
+5 -5
View File
@@ -5,7 +5,7 @@
//! - A "▌ replay" label on the left so the player knows the surface is
//! under playback control rather than live input.
//! - A "MOVE N/M" progress chip in the centre, recomputed every frame
//! the cursor advances and bordered in cyan ACCENT_PRIMARY so it
//! the cursor advances and bordered in `ACCENT_PRIMARY` so it
//! reads as a discrete callout.
//! - A "Stop" button on the right that aborts playback and returns
//! control to the player.
@@ -105,7 +105,7 @@ pub struct ReplayStopButton;
#[derive(Component, Debug)]
pub struct ReplayOverlayGameCaption;
/// Marker on the cyan "fill" of the bottom-edge scrub bar. The
/// Marker on the accent "fill" of the bottom-edge scrub bar. The
/// `Node`'s `width` is rewritten every frame the cursor advances to
/// `cursor / total` of the bar's full width, so the player has a
/// continuous visual cue of how far through the replay they are.
@@ -250,7 +250,7 @@ fn spawn_overlay(
..default()
})
.with_children(|row| {
// Left: column with the cyan "▌ replay" headline
// Left: column with the accent "▌ replay" headline
// above and a small `GAME #YYYY-DDD` caption below.
// The caption mirrors the mockup's right-anchored
// game identifier but stays visually grouped with
@@ -315,7 +315,7 @@ fn spawn_overlay(
// Right: Stop button. Tertiary variant — the
// action is available but not the loudest element
// in the banner; the "Replay" cyan accent owns
// in the banner; the "Replay" primary accent owns
// that slot. `spawn_modal_button` gives us hover /
// press paint and focus rings for free via the
// existing `UiModalPlugin` paint system.
@@ -425,7 +425,7 @@ fn update_progress_text(
}
}
/// Repaints the bottom-edge cyan scrub fill to mirror cursor progress.
/// Repaints the bottom-edge accent scrub fill to mirror cursor progress.
/// Same change-detection guard as the text updaters — the overlay
/// already early-exits when nothing moved, so an idle replay leaves the
/// scrub bar's `Node` untouched.
+3 -3
View File
@@ -6,7 +6,7 @@
//!
//! 1. [`SelectionState`] tracks the *source-pick* mode. `Tab` / `Shift+Tab`
//! cycles a focus through piles that have a face-up draggable top card.
//! The focused card is decorated with a cyan [`SelectionHighlight`].
//! The focused card is decorated with an accent-coloured [`SelectionHighlight`].
//!
//! 2. [`KeyboardDragState`] tracks the *destination-pick* mode. Pressing
//! `Enter` while a pile is focused enters
@@ -634,7 +634,7 @@ fn clear_selection_on_state_change(
/// Maintains the `SelectionHighlight` outline sprite.
///
/// When a pile is selected (source-pick mode), a cyan sprite is placed
/// When a pile is selected (source-pick mode), an accent-coloured sprite is placed
/// at the selected card's position. While
/// [`KeyboardDragState::Lifted`] the source highlight tints gold and a
/// second highlight follows the focused destination's top card — visually
@@ -662,7 +662,7 @@ fn update_selection_highlight(
let card_size = layout.0.card_size;
// Highlight tints follow the Terminal palette's semantic state
// tokens: cyan focus/selection while picking the source, gold
// tokens: ACCENT_PRIMARY focus/selection while picking the source, gold
// attention/commitment once the cards are lifted, lime valid-move
// tint on the destination. Alphas are kept non-zero so the card
// face beneath remains readable through the wash.
+5 -5
View File
@@ -1,7 +1,7 @@
//! Launch splash overlay.
//!
//! On app start the engine spawns a fullscreen, high-Z overlay that
//! reads the Terminal-style "boot screen" — a cyan cursor block, the
//! reads the Terminal-style "boot screen" — an accent-coloured cursor block, the
//! "Solitaire Quest" wordmark, a short fixture boot log, a progress
//! bar, and a footer with the design-system palette swatches and the
//! build version. The overlay fades in over 300 ms, holds for ~1 s,
@@ -470,8 +470,8 @@ fn spawn_check_row(parent: &mut ChildSpawnerCommands, line_font: &TextFont, labe
/// "▌ ready_" line — visual signature of "boot complete, awaiting
/// input". The leading `▌` glyph picks up `TEXT_PRIMARY` rather than
/// `ACCENT_PRIMARY` so it doesn't compete with the big cyan cursor in
/// the header; the *trailing* 6×12 px cyan pulse Node ([`SplashCursorPulse`])
/// `ACCENT_PRIMARY` so it doesn't compete with the big accent cursor in
/// the header; the *trailing* 6×12 px accent pulse Node ([`SplashCursorPulse`])
/// is what carries the "alive, blinking" signal called for by the
/// mockup. The pulse's alpha is multiplied with the global fade
/// timeline by [`pulse_splash_cursor`] so it never fights the
@@ -492,7 +492,7 @@ fn spawn_ready_row(parent: &mut ChildSpawnerCommands, line_font: &TextFont) {
line_font.clone(),
TextColor(transparent(TEXT_PRIMARY)),
));
// Trailing 6×12 cyan pulse cursor. Node-with-explicit-
// Trailing 6×12 accent pulse cursor. Node-with-explicit-
// dimensions rather than a `█` text glyph so the size
// doesn't drift with the line font; matches the mockup's
// 6×12 px spec literally. Pulse animation lives in
@@ -511,7 +511,7 @@ fn spawn_ready_row(parent: &mut ChildSpawnerCommands, line_font: &TextFont) {
}
/// Progress bar — a 1 px tall track in `BORDER_SUBTLE` with a 100 %-
/// width cyan fill, plus a `DONE · 247 ASSETS` caption right-aligned
/// width accent fill, plus a `DONE · 247 ASSETS` caption right-aligned
/// below. The "247" is fixture text; the bar is decorative, not a
/// real progress signal. Capped at 720 px width on desktop.
fn spawn_progress_bar(parent: &mut ChildSpawnerCommands, line_font: &TextFont) {
+5 -5
View File
@@ -333,17 +333,17 @@ pub fn spawn_modal_button<M: Component>(
};
let label_color = match variant {
// Primary buttons sit on the cyan accent — `BG_BASE` text on
// top reads well and passes AAA contrast against `#6fc2ef`.
// Primary buttons sit on the brick-red accent — `BG_BASE` text on
// top reads well and passes AAA contrast against `#a54242`.
ButtonVariant::Primary => BG_BASE,
ButtonVariant::Secondary | ButtonVariant::Tertiary => TEXT_PRIMARY,
};
let caption_color = match variant {
// Muted near-black on the cyan Primary so the hotkey chip reads
// Muted near-black on the red Primary so the hotkey chip reads
// as a secondary detail without disappearing. Deliberately a
// pure-black-at-alpha rather than `BG_BASE.with_alpha(...)`:
// `BG_BASE` is `#151515` (not 0,0,0), so the alpha-on-cyan
// composite would tint slightly cooler than intended here.
// `BG_BASE` is `#151515` (not 0,0,0), so the alpha-on-accent
// composite would tint slightly off from intended here.
ButtonVariant::Primary => Color::srgba(0.0, 0.0, 0.0, 0.55),
ButtonVariant::Secondary | ButtonVariant::Tertiary => TEXT_SECONDARY,
};
+16 -13
View File
@@ -21,8 +21,8 @@ use bevy::prelude::Val;
use solitaire_data::AnimSpeed;
// ---------------------------------------------------------------------------
// Colours — Terminal (base16-eighties): near-black surface ramp with a cyan
// primary accent and lime/lavender/gold/teal/pink semantic accents.
// Colours — Terminal (base16-eighties): near-black surface ramp with a brick-
// red primary accent and lime/lavender/gold/teal/pink semantic accents.
// ---------------------------------------------------------------------------
/// Window backstop and the default text colour on top of `ACCENT_PRIMARY`.
@@ -67,14 +67,17 @@ pub const TEXT_SECONDARY: Color = Color::srgb(0.627, 0.627, 0.627);
/// Disabled text — greyed-out buttons, locked items. `#505050`.
pub const TEXT_DISABLED: Color = Color::srgb(0.314, 0.314, 0.314);
/// Cyan primary accent — the CTA colour of the system. Reserved for
/// primary actions (Play, Resume, Save), focus rings, and selection.
/// `BG_BASE` text on top of this colour passes AAA contrast. `#6fc2ef`.
pub const ACCENT_PRIMARY: Color = Color::srgb(0.435, 0.761, 0.937);
/// Brick-red primary accent — the CTA colour of the system. Reserved
/// for primary actions (Play, Resume, Save), focus rings, and
/// selection. `#a54242` (base16-eighties `base08`). Pre-2026-05-08
/// this slot was cyan `#6fc2ef`; the swap was a project-wide
/// palette decision recorded in `design-system.md` and the
/// SESSION_HANDOFF entry that followed Option D.
pub const ACCENT_PRIMARY: Color = Color::srgb(0.647, 0.259, 0.259);
/// Brightened `ACCENT_PRIMARY` for hover states on primary buttons.
/// Picks up luminance while keeping the same hue. `#a8dcf5`.
pub const ACCENT_PRIMARY_HOVER: Color = Color::srgb(0.659, 0.863, 0.961);
/// Picks up luminance while keeping the same hue. `#c25e5e`.
pub const ACCENT_PRIMARY_HOVER: Color = Color::srgb(0.761, 0.369, 0.369);
/// Lavender secondary accent — celebratory states (level-up,
/// achievement unlocked, streak milestones). Used sparingly so it stays
@@ -134,8 +137,8 @@ pub const STOCK_BADGE_BG: Color = BG_ELEVATED_HI;
/// Foreground (text) colour of the stock-pile remaining-count chip.
///
/// `ACCENT_PRIMARY` keeps the chip readable against the elevated
/// surface background and matches the cyan accent already used for
/// other "look here" callouts.
/// surface background and matches the primary accent already used
/// for other "look here" callouts.
pub const STOCK_BADGE_FG: Color = ACCENT_PRIMARY;
/// Sprite-space `Transform.z` for the stock-pile remaining-count chip.
@@ -172,8 +175,8 @@ pub const CARD_SHADOW_ALPHA_IDLE: f32 = 0.0;
/// Alpha for the lifted/dragged card shadow. Set to 0 for the same
/// reason as [`CARD_SHADOW_ALPHA_IDLE`]. Drag affordance under the
/// Terminal system is the cyan focus glow + z-index lift, not a deeper
/// shadow.
/// Terminal system is the primary-accent focus glow + z-index lift,
/// not a deeper shadow.
pub const CARD_SHADOW_ALPHA_DRAG: f32 = 0.0;
/// World-space pixel offset of the resting-state card shadow relative to
@@ -217,7 +220,7 @@ pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);
/// (matches `ACCENT_PRIMARY`) at 85% alpha so the ring stays legible
/// against both elevated surfaces and the modal scrim backdrop.
/// `rgba(111, 194, 239, 0.85)`.
pub const FOCUS_RING: Color = Color::srgba(0.435, 0.761, 0.937, 0.85);
pub const FOCUS_RING: Color = Color::srgba(0.647, 0.259, 0.259, 0.85);
// ---------------------------------------------------------------------------
// Typography scale (px) — 5 rungs replace the prior
+1 -1
View File
@@ -76,7 +76,7 @@ const EXPECTED: &[(&str, u64)] = &[
("face_JS", 0x52525a2200c07246),
("face_QS", 0xb4f0251a2757cbb1),
("face_KS", 0x1e1975919bb9a029),
("back_0", 0xf698d0e161eae13a),
("back_0", 0xfd1742ebe330481a),
("back_1", 0x446fdc0a3c83a03a),
("back_2", 0xcf188fdec9f5819a),
("back_3", 0xcaffd02af141743a),