479 Commits

Author SHA1 Message Date
funman300 511550232c docs(handoff): record HC marker + ← / → wiring; recommend v0.21.5 cut
Two more post-v0.21.4 carve-outs land:
- 23902cd: HC-mode coverage for keybind-footer top border
  (HighContrastBorder marker so apply_high_contrast_borders
  bumps the 1 px top border under HC).
- e5c4f51: ← / → keyboard accelerators for paused stepping
  (hooks game's undo system for backwards step; footer
  extended to [SPACE] pause/resume · [ESC] stop · [← →] step).

Update Since-cut log, visual-identity bullet, B option in the
Resume menu, status (1244 → 1250 total tests / 1249 passing /
1 pre-existing flake), and HEAD hint.

Six post-v0.21.4 commits now form a coherent through-line:
replay-overlay scrubbing affordances + accessibility. Resume
menu's B option now recommends cutting v0.21.5 as the natural
next boundary.

Pre-existing flake noted: daily_challenge warning test fails
when wall-clock UTC is within 30 minutes of midnight (the
warning window the test asserts against). Verified not
introduced by recent commits via stash-and-retest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:59:08 -07:00
funman300 e5c4f51a6e feat(replay): wire ← / → keyboard accelerators for paused stepping
→ during a paused replay advances by one move (mirrors the Stop
button's existing forward-step semantics). ← decrements the
cursor and dispatches `UndoRequestEvent`, which the game's
`handle_undo` reads next frame to reverse its most-recent move
— hooking the existing undo system rather than replaying
forward from cursor 0 (every replay-applied move pushes to the
undo stack the same way a player move would, so undo is the
right reversal primitive).

Both accelerators are paused-only — backwards via a new
`step_backwards_replay_playback` in `replay_playback.rs` that
hard-gates with the same destructure pattern as
`step_replay_playback`. Pressing → during running playback or ←
at cursor 0 are silent no-ops; the player learns "pause first,
then arrow."

The mockup labels these `[← →] scrub` (continuous fast scan).
Single-move step is the closest behaviour shippable today —
continuous scrub would need either a key-held event source or
an internal speed-up loop. Footer hint reads
`[← →] step` to match what's wired rather than the aspirational
"scrub."

Footer hint extended in lockstep:
`[SPACE] pause/resume · [ESC] stop · [← →] step` — the
only-wired-keybinds discipline holds.

ReplayOverlayPlugin gains `add_message::<UndoRequestEvent>()`
defensively so the plugin can run under MinimalPlugins without
GamePlugin attached (idempotent registration; harmless when
GamePlugin is also present).

6 new tests (2 hint pins + 4 keyboard scenarios) + 1 helper-pin
update for the new hint string.

Pre-existing flake noted: `daily_challenge_plugin::tests::
check_system_fires_warning_event_only_once_per_day` is failing
because wall-clock UTC is currently within 30 minutes of
midnight, inside the daily-expiry warning window the test
asserts against. Verified pre-existing by stashing all changes
and re-running — failure persists. Same shape as the
`winnable_seed_search` flake the handoff documented earlier
this session: time-dependent, deterministically passes under
different clock conditions. Not introduced by this commit.

Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:50:59 -07:00
funman300 23902cdc44 feat(replay): HC-mode coverage for keybind-footer top border
Tag the footer's border-carrying Node with
`HighContrastBorder::with_default(BORDER_SUBTLE)` so the existing
`apply_high_contrast_borders` system bumps the 1 px top border
from `BORDER_SUBTLE` (#505050) to `BORDER_SUBTLE_HC` (#a0a0a0)
when `Settings::high_contrast_mode` is on.

Without this the footer reads as floating loose under HC because
the border that visually anchors it to the labels row above is
near-invisible at #505050 against the elevated banner background.

The footer's text colours (`TEXT_SECONDARY` on both the
mode-line and the hint) don't need an HC bump — `TEXT_SECONDARY`
is already at `#a0a0a0`, the same luminance as `BORDER_SUBTLE_HC`.
There's no `TEXT_SECONDARY_HC` constant in the palette because
secondary text is already at HC-border level by design.

The notch labels also use `TEXT_SECONDARY` and inherit the same
"already HC-bright" property — no marker needed there either.

The 1 px scrub track, notch ticks, and WIN MOVE marker render
via `BackgroundColor` (not `BorderColor`) so the
`HighContrastBorder` marker doesn't apply. HC coverage for those
decorative pieces would need a custom settings-aware paint
system (precedent: `radial_rim_outline` in `radial_menu`) and is
deferred to a follow-up commit.

1 new test pinning the marker on spawn. 1243 → 1244. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:41:49 -07:00
funman300 3cc8eacafa docs(handoff): record ESC accelerator; B's next step is HC polish
Post-v0.21.4 fourth carve-out: 90e24d9 wires ESC for replay-stop
with a cross-plugin gate in pause_plugin to defer when replay is
playing. Footer extended in lockstep to
[SPACE] pause/resume · [ESC] stop. Update Since-cut log,
visual-identity bullet, B option in the Resume menu, status
(1240 → 1243 tests), and HEAD hint.

B option's next-step menu now has three branches: HC polish
(smallest), ← / → wiring (medium, needs backwards-step path),
and the multi-session move-log/preview arcs that close B-2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:07:33 -07:00
funman300 90e24d9711 feat(replay): wire ESC accelerator for stop, gate pause modal
ESC during an active replay now stops it (mirrors the existing
Stop button click). UI-first contract from CLAUDE.md §3.3 holds
for the keyboard accelerator: every keybind the footer surfaces
points at a wired action.

Cross-plugin coordination: pause_plugin's `toggle_pause` already
listens for ESC and would otherwise open the pause modal on the
same press. Resolved by adding a fourth defer-if check to the
existing modal-stack pattern in `toggle_pause` —
`replay_state.is_some_and(|s| s.is_playing())` slots in right
after `other_modal_scrims` and before `selection`. Symmetric
shape to the existing forfeit / modal-scrim / selection /
game-over / drag gates.

Footer hint extended from `[SPACE] pause/resume` to
`[SPACE] pause/resume · [ESC] stop` in lockstep — the
"only-wired-keybinds" discipline holds.

3 new tests:
- esc_keyboard_stops_active_replay (positive: Esc → Inactive,
  overlay despawns next frame)
- esc_keyboard_is_noop_when_not_playing (negative: doesn't fire
  on Inactive state, lets global Esc listeners own those frames)
- keybind_footer_hint_lists_space_and_esc (footer text contains
  both keybinds)

Plus updated helper-pin test for the new hint string. Existing
pause_plugin tests unaffected (they don't insert a
ReplayPlaybackState resource so the new gate is a no-op for
them).

Tests: 1240 → 1243 (+3). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:06:02 -07:00
funman300 decbe0bbd9 docs(handoff): record keybind footer; B's next step is ESC accelerator
Post-v0.21.4 third carve-out: 1873b3f ships a keybind-hint footer
(vim-style mode line + `[SPACE] pause/resume`) at the bottom of
the banner (76 → 92 px). Update Since-cut log, visual-identity
bullet, B option in the Resume menu, status (1236 → 1240 tests),
and HEAD hint.

Footer lists only wired keybinds. Next finite step on B-2: wire
ESC for stop and extend the footer to `[SPACE] pause/resume ·
[ESC] stop` — small, single-axis, surfaces another keyboard
accelerator alongside the existing Stop button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:59:57 -07:00
funman300 1873b3f9be feat(replay): add keybind-hint footer to overlay banner
Vim-style mode line on the left (`▌ NORMAL │ replay`) plus a
keybind-hint on the right (`[SPACE] pause/resume`) gives the
existing Space accelerator a visible UI counterpart, satisfying
the UI-first contract from CLAUDE.md §3.3 for the keyboard
accelerator that v0.21.4 shipped.

The footer lists only keybinds that are *actually wired today*.
Future commits that wire ESC for stop or ← / → for prev/next
move will extend the right-hand text in lockstep — the footer
never lists aspirational keybinds (would lie to users).

Banner height grew from 76 → 92 px to make room for the 16 px
footer row. Second layout-changing commit in B-2's screen-
takeover arc; same "grow container, add flex-column child"
pattern as the notch-labels commit. 1px top border in
BORDER_SUBTLE separates the footer from the notch-label row.

Two pure helpers (`keybind_footer_mode_text`,
`keybind_footer_hint_text`) keep the static text testable
without per-text marker components on the inner Text entities.
The shared `font_handle_for_labels` clone covers both label and
footer text spawns since the labels closure only `.clone()`s
the handle (never moves it).

4 new tests: pure-helper guards, footer-spawn cardinality
(exactly one), text-set assertion (both helper strings appear as
descendants), lifecycle parity with the overlay tree.

Tests: 1236 → 1240 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:58:28 -07:00
funman300 d11d97e677 docs(handoff): record notch labels; B's next step is keybind footer
Post-v0.21.4 second carve-out: d322abf ships percentage labels
under each scrub-bar notch (banner 60 → 76 px — first real layout
change in B-2's arc). Update Since-cut log, visual-identity
bullet, B option in the Resume menu, status (1232 → 1236 tests),
and HEAD hint.

Banner geometry is now mutable; future B-2 sub-pieces follow the
same "grow container, add flex-column child" pattern. Next
finite step: keybind-hint footer (small) before the bigger
move-log / mini-tableau pieces.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:52:28 -07:00
funman300 d322abf67b feat(replay): add percentage labels under scrub-bar notches
Five `0%` / `25%` / `50%` / `75%` / `100%` labels in a new 16 px
row beneath the 1 px scrub track give the player explicit
quarter-mark readouts to pair with the notch ticks.

Pure helper `scrub_notch_labels()` returns the fixed array,
paired index-for-index with `scrub_notch_positions()`. Spawn loop
zips both helpers and applies an "endpoints flush, middle three
percent-anchored" positioning pattern: leftmost label gets
`left: 0` (no clip on `0%`), rightmost gets `right: 0` (no overflow
on `100%`), middle three anchor at `left: Val::Percent(p)` since
Bevy 0.18 UI lacks a clean CSS-style `translate-x: -50%` centering
primitive. The slight right-of-notch offset on the middle three
is visually subtle at TYPE_CAPTION; explicit polish target if
anyone notices.

Banner height grew from 60 → 76 px to make room for the label row
(76 = top row 59 flex-grow + scrub track 1 + label row 16). First
real layout change in B-2's screen-takeover arc — every prior
B-2 commit was additive at fixed banner geometry.

Label color is TEXT_SECONDARY rather than mockup's `text-outline`
(BORDER_SUBTLE) — the latter would match the notches but is too
low-contrast against BG_ELEVATED_HI to read at 12 px. TEXT_SECONDARY
keeps the subdued caption hierarchy while staying legible.

4 new tests: pure-helper guard pinning the array + helper-positions
pairing invariant, spawn cardinality, set equality between spawned
texts and helper output, lifecycle parity with the overlay tree.

Tests: 1232 → 1236 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:51:09 -07:00
funman300 c9e4c0b4cd docs(handoff): record scrub-bar notches; B's next step is notch labels
Post-v0.21.4 carve-out: fe68861 ships quarter-mark notches on the
scrub bar. Update Since-cut log, visual-identity bullet, B option
in the Resume menu, status (1228 → 1232 tests), and HEAD hint.

Next finite step on B-2: percentage labels under each notch —
forces banner height to grow from 60 px to ~76 px, making it the
first real layout change in the screen-takeover arc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:44:05 -07:00
funman300 fe68861e10 feat(replay): add quarter-mark notches to scrub bar
Five 1px vertical ticks at 0/25/50/75/100% give the player visual
anchor points for "where am I, relative to the quarter-marks of the
replay" without needing to mentally bisect the bar.

Pure helper `scrub_notch_positions()` returns the fixed array; the
spawn loop sits next to the WIN MOVE marker spawn so the two
overlays share their lifecycle with the rest of the overlay tree.
Notches paint in BORDER_SUBTLE (same as the unfilled track) and
extend vertically past the 1px track (5px tall, anchored 2px above
the track top) — same visibility trick the WIN MOVE marker uses.

Spawned after the WIN MOVE marker so a notch and the marker landing
on the same percentage paint the marker on top.

Mirrors the notch ladder in the screen-takeover mockup at
docs/ui-mockups/replay-overlay-mobile.html. First finite step toward
B-2's screen-takeover layout reflow; labels under each notch land in
a follow-up commit when the banner height grows to accommodate them.

4 new tests: pure-helper guard pinning the [0,25,50,75,100] array,
spawn-cardinality matching helper.len(), lifecycle parity with the
overlay tree, independence from win_move_index.

Tests: 1228 → 1232 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:42:37 -07:00
funman300 c33b39cf11 docs(handoff): refresh post-v0.21.4 — anchor to new tag, reset menu state
Anchors handoff to v0.21.4 at `23ff62c`, resets the "Since the cut"
section to placeholder, updates the READ FIRST CHANGELOG pointer,
bumps the Resume-prompt summary to reflect replay-scrubbing
accessibility as the v0.21.4 through-line, and identifies the
screen-takeover layout reflow as the remaining multi-session arc
on B (with move-log scroller + mini-tableau preview as small
sub-pieces inside it).

Resume menu stays at A/B/C — A and C unchanged; B's prerequisite
sub-pieces shipped in v0.21.4 so the entry now points cleanly at
the layout reflow as the single remaining multi-session piece.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:28:50 -07:00
funman300 23ff62c397 docs: cut v0.21.4 — replay-scrubbing accessibility
Patch release for the three post-v0.21.3 commits on the B-2 replay
screen-takeover redesign arc. One through-line: the replay overlay
gains scrubbing affordances. The player can see at a glance where
the winning move sits (WIN MOVE marker on the scrub bar) and stop
on any move to inspect the board (pause / resume / step controls
plus a Space keyboard accelerator).

Also adds the data foundation that makes the marker possible:
`Replay::win_move_index: Option<usize>`, an additive serde-default
field that doesn't bump `REPLAY_SCHEMA_VERSION` because legacy
on-disk replays load with `None` and simply don't get a marker.

Remaining B-2 work — screen-takeover layout, move-log scroller,
mini-tableau preview — shares a layout-reflow prerequisite the
banner-only overlay can't carry, so it's deferred to a future
cycle that can take it as a single multi-session arc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.21.4
2026-05-08 15:26:54 -07:00
funman300 0b2ffca016 docs(handoff): record playback controls; B's next step is takeover layout
Captures `fbe48ac` (pause / resume / step + Space accelerator) under
"Since the v0.21.3 cut", marks playback controls closed in the
Visual-identity follow-ups list, identifies the screen-takeover
layout itself (with move-log scroller + mini-tableau preview as its
sub-pieces) as the next finite step on B, and bumps the test count
to 1228.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:21:48 -07:00
funman300 fbe48acef6 feat(replay): playback controls — pause / resume / step + Space accelerator
Third commit on the B-2 replay screen-takeover redesign. Adds the
ability to pause an in-flight replay, step through it one move at
a time while paused, and resume — both via on-screen buttons
(UI-first contract per CLAUDE.md §3.3) and the optional `Space`
keyboard accelerator.

State shape: a new `paused: bool` field on
`ReplayPlaybackState::Playing`. The `tick_replay_playback` system
skips the `secs_to_next` decrement entirely while `paused` is set
so cursor and timer freeze together — resuming starts the next
move from a full interval. Stepping fires the next move directly
via a new `step_replay_playback` API that bypasses the tick path
and is hard-gated to `Playing { paused: true }` so it can't race
the running tick loop.

Public API additions:
- `toggle_pause_replay_playback(state)` — flips the flag, returns
  the new value (or None when not Playing).
- `step_replay_playback(state, moves_writer, draws_writer)` —
  advances exactly one move when paused; returns true on dispatch,
  false on any guard miss.

UI:
- Pause / Resume button next to Stop. Label repaints reactively
  via `update_pause_button_label`, which walks `Children` from
  the marked button to its inner `Text` so the spawn path doesn't
  need a second marker.
- Step button next to Pause. Click fires the next move; while
  unpaused the click is a no-op (guarded inside
  `step_replay_playback`).
- `Space` keyboard handler reads `Option<Res<ButtonInput>>` and
  no-ops when missing — keeps test-app compatibility under
  `MinimalPlugins`.

Test coverage: pause-button label truth table, label repaint on
state change, click-toggles-paused, step advances cursor exactly
one with paused flag preserved, step-while-running is no-op,
Space toggles paused flag. 8 new tests (1220 → 1228).

Side-effect: 25 existing `Playing { ... }` construction sites
across `replay_overlay`, `achievement_plugin`, and
`replay_playback` tests gained `paused: false` to satisfy the new
field requirement. Mechanical edit; no behavioral change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:20:45 -07:00
funman300 cd79877933 docs(handoff): record WIN MOVE marker ship; B's next finite step
Captures `52befa6` (WIN MOVE marker on the scrub bar) under "Since
the v0.21.3 cut", marks the marker piece of B-2 closed in the
Visual-identity follow-ups list, identifies playback controls
(play/pause/step) as the next bounded commit on B, and bumps the
test count to 1220.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:54:44 -07:00
funman300 52befa6199 feat(replay): WIN MOVE marker on the scrub bar
Second commit on the B-2 replay screen-takeover redesign — the UI
that consumes the data field landed in `ab857bb`. Adds a small
green tick on the scrub bar at `replay.win_move_index / total`,
positioned so the playback cursor reaches the marker exactly when
the move it's about to apply IS the winning move.

Implementation: a new `ReplayOverlayWinMoveMarker` component
spawned alongside `ReplayOverlayScrubFill` as a sibling under the
1px scrub track. Position computed by a pure helper
`win_move_marker_pct` that returns `None` for any of: state not
`Playing`, replay's `win_move_index` is `None` (older replay
loaded from disk pre-dating the field), or empty move list. The
percentage is clamped to `[0, 100]` defensively. Marker is
absolute-positioned with `top: -1px` so the 3px-tall tick is
centered on the 1px track line — 1px above and 1px below.

Lifecycle is "spawn-time only" — the marker position never changes
during a single playback because the underlying replay is
immutable while `Playing`. Despawned with the rest of the overlay
tree when the state returns to `Inactive`.

8 new tests cover: pure helper for Inactive / Completed / no-field /
correct-position / clamp; spawn presence with field; spawn absence
without field; despawn-with-overlay lifecycle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:53:40 -07:00
funman300 e63046700c docs(handoff): record win_move_index data field; B's next finite step
Captures `ab857bb` (Replay::win_move_index data field) under "Since
the v0.21.3 cut". Updates the Visual-identity follow-up entry for
B-2 to flag the data-layer prerequisite as landed and identifies
the WIN MOVE scrub-bar marker UI as the natural next finite commit.
Bumps test count to 1212.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:45:59 -07:00
funman300 ab857bbb6e feat(data): add Replay::win_move_index for the WIN MOVE scrub marker
First finite step toward the B-2 replay screen-takeover redesign:
the data foundation. Adds an additive optional `win_move_index:
Option<usize>` field on `Replay`, defaulting to `None` via
`#[serde(default)]` so older `latest_replay.json` /
`replays.json` files load unchanged — no `REPLAY_SCHEMA_VERSION`
bump needed since the field is purely additive and nullable.

Populated at the live recording site (`game_plugin::handle_game_won`)
via a new builder-style setter `Replay::with_win_move_index`. For
fresh recordings the value is always `Some(moves.len() - 1)`
because recording freezes on win, but storing the index
explicitly lets the playback UI read the WIN MOVE position
directly without re-deriving it on every render — and leaves
room for future recording semantics that capture post-win state.

UI consumption (the WIN MOVE marker on the scrub bar, plus the
broader screen-takeover redesign — move-log scroller, mini-
tableau preview, playback controls) lands in subsequent commits.

Test coverage: default value, builder set / set-None, on-disk
round-trip, and the legacy-JSON-loads-with-None backward-compat
contract (the test that pins the no-schema-bump claim).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:45:02 -07:00
funman300 886e0cf8a1 docs(handoff): refresh post-v0.21.3 — anchor to new tag, reset menu state
Anchors handoff to v0.21.3 at `3d92a91`, resets the "Since the cut"
section to placeholder, updates the READ FIRST CHANGELOG pointer,
and bumps the Resume-prompt summary to reflect the accessibility
arc closure as the v0.21.3 through-line. Resume menu stays at
A/B/C since v0.21.3 closes only post-v0.21.2 carve-outs (the
remaining options were already heavy / multi-session).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:41:02 -07:00
funman300 3d92a91e3b docs: cut v0.21.3 — accessibility arc closure + Toast Warning driver
Patch release for the two post-v0.21.2 commits. One through-line:
the v0.21.2 "dynamic-paint sites stay un-tagged" carve-out turned
out to be over-cautious — re-reading the code showed only the
radial rim was actually a border-paint cycle. v0.21.3 closes the
carve-out: HUD action buttons + modal buttons take the existing
`HighContrastBorder` marker pattern; the radial rim folds HC into
its per-frame respawn via `radial_rim_outline`.

Bonus: `ToastVariant::Warning` gets its first real consumer in
this cycle (daily-challenge expiry < 30 min from UTC reset). Every
`ToastVariant` now has at least one driver — the enum is fully
load-bearing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.21.3
2026-05-08 14:39:46 -07:00
funman300 9113cdb483 docs(handoff): record HC dynamic-paint rollout; menu drops D → 3 options
Marks the HC dynamic-paint rollout (`c153363`) closed under the
High-contrast accessibility entry, captures it in "Since the v0.21.2
cut", bumps the test count to 1207, and trims the Resume prompt
menu from 4 → 3 options (A Android, B replay screen-takeover,
C Phase 8 sync). All three remaining options are multi-session by
nature; the resume prompt now flags that explicitly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:36:00 -07:00
funman300 c153363626 feat(accessibility): finish HC rollout — HUD + modal buttons + radial rim
Closes the v0.21.2 carve-out: dynamic-paint sites that were left
un-tagged because their paint cycles were assumed to race
`update_high_contrast_borders`. Re-reading the code revealed only
one of three sites is actually a border-paint cycle — the other
two paint backgrounds, with static borders that take the marker
pattern cleanly:

* HUD action buttons (`spawn_action_button`): `paint_action_buttons`
  only mutates `BackgroundColor`. Tag the spawn with
  `HighContrastBorder::with_default(BORDER_SUBTLE)`.
* Modal buttons (`spawn_modal_button`): `paint_modal_buttons` also
  only mutates `BackgroundColor`. Same marker pattern.
* Radial menu rim (`radial_redraw_overlay`): full despawn-respawn
  every frame; sprites, not UI nodes; the marker can't apply. Folds
  the HC choice into the spawn site instead — under HC the
  *focused* rim boosts to `BORDER_SUBTLE_HC` rather than
  `BORDER_STRONG`. Naive marker substitution would invert the
  visual hierarchy because `BORDER_SUBTLE_HC` (#a0a0a0) is lighter
  than `BORDER_STRONG` (#505050); folding the choice in keeps the
  focused rim *more* visible under HC, not less.

Decision logic for the rim is extracted to `radial_rim_outline` —
a pure function with a 4-row truth-table test (focused × HC).

After this commit, every UI surface tagged in v0.21.x's
accessibility arc either carries `HighContrastBorder` or has its
HC behaviour folded into its own spawn cycle. No "un-tagged
because race-risk" surfaces remain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:34:05 -07:00
funman300 93b67f1d0b docs(handoff): record Toast Warning wiring; menu drops C → 4 options
Marks the daily-challenge-expiry Warning toast (`279e23d`) closed in
the Visual-identity follow-ups list, captures it in "Since the
v0.21.2 cut", bumps the test count to 1203, and trims the Resume
prompt menu from 5 → 4 options (A Android, B-2 replay takeover,
C Phase 8 sync, D HC dynamic-paint).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:25:10 -07:00
funman300 279e23d0af feat(toast): wire ToastVariant::Warning for daily-challenge expiry
Adds the first in-engine consumer of `ToastVariant::Warning` — a 4s
amber-bordered toast that fires once per daily-challenge date when the
player is within 30 minutes of UTC midnight reset and hasn't yet
completed today's challenge.

Mirrors the v0.21.2 `ToastVariant::Error` wiring: a domain-event
message (`WarningToastEvent(String)`) crosses the plugin boundary;
`animation_plugin::handle_warning_toast` reads it and spawns the
fire-and-forget toast. Suppression is decided by a pure helper
(`compute_expiry_warning_minutes`) that's exhaustively covered by 7
unit tests + 1 in-Bevy idempotence test.

After this lands, every `ToastVariant` (Info, Warning, Error,
Celebration) has at least one real driver — closing the "is this enum
scaffolding or load-bearing?" ambiguity that's been latent since the
variant was introduced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:22:58 -07:00
funman300 12fba2157a docs(handoff): refresh post-v0.21.2 — anchor to new tag, update menu
Mirrors the post-v0.21.0 → v0.21.1 → v0.21.2 cut-then-refresh
pattern. Cut commit (f23df3b) edited only CHANGELOG; this
follow-up resets the handoff so a fresh session picks up cleanly
post-v0.21.2.

Updated:
- Header points to v0.21.2 at f23df3b; opening paragraph
  summarizes the patch's three threads (accessibility
  extensions, replay polish, first real Toast Error consumer).
- Status at pause: tests bumped to 1195 (net +3 from v0.21.1's
  1192); tags list extended through v0.21.2.
- "Since the v0.21.1 cut" → "Since the v0.21.2 cut" with the
  closure narratives dropped (now in CHANGELOG.md § [0.21.2]).
  Section reset to "no threads in flight" placeholder.
- Visual-identity follow-ups: marked floating MOVE chip closed
  by v0.21.2 (`2fb2d63`), Toast Error closed by v0.21.2
  (`68d50b5`); HC + reduce-motion entries updated to reflect
  v0.21.2's HC chrome rollout (8 surfaces) and splash
  reduce-motion gating. Toast Warning still open with a
  candidate driver suggestion (daily-challenge expiry).
- Resume prompt menu retuned: A (Android) and D (Phase 8)
  unchanged; B narrowed to just the screen-takeover redesign
  (the floating chip piece shipped); C narrowed to just
  Warning variant (Error done); new E added for
  HC+reduce-motion on dynamic-paint sites (HUD action buttons,
  etc — explicitly carved out of the v0.21.2 HC rollout
  because of paint-cycle races).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 14:08:17 -07:00
funman300 f23df3b805 docs: cut v0.21.2 — accessibility extensions + replay polish + first real Toast Error consumer
Promotes [Unreleased] to [0.21.2] dated 2026-05-08 and opens a
fresh empty [Unreleased]. Patch release covering 6 substantive
post-v0.21.1 commits (plus the v0.21.1 handoff refresh).

Three through-lines:

- **Accessibility extensions.** Closes the two threads v0.21.1
  left explicitly open. Reduce-motion was previously gated only
  on card slide_secs; v0.21.2 extends it to splash scanline +
  cursor pulse (`ed152e2`). HC borders had `BORDER_SUBTLE_HC`
  defined but no consumers; v0.21.2 builds the
  `HighContrastBorder` marker + `update_high_contrast_borders`
  system (`c9af1ea`) and rolls it out across 8 surfaces
  (`d87761d` + `ec804d5`).

- **Replay polish.** New floating MOVE chip rendered above the
  destination pile of the most-recently-applied move during
  playback (`2fb2d63`). World-space `Text2d` entity that
  reuses the same `LayoutResource` pile coordinates as every
  other piece of pile geometry — stays correctly positioned
  through window resizes without any UI / camera math.

- **First real `ToastVariant::Error` consumer.** Wires
  `MoveRejectedEvent` to a 2-second pink-bordered "Invalid move"
  toast (`68d50b5`). Joins the existing `card_invalid.wav`
  audio + destination-pile shake visual as the
  accessibility-focused readable text channel.

cargo clippy --workspace --all-targets -- -D warnings clean.
1195 passing / 0 failing (net +3 from v0.21.1's 1192).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.21.2
2026-05-08 14:06:14 -07:00
funman300 68d50b5021 feat(toast): wire ToastVariant::Error for invalid-move feedback
Resume-prompt Option C — first in-engine consumer of
`ToastVariant::Error`. The variant has had a slot in the enum
since v0.20.0's toast system landed; this commit wires a real
driver event so the slot is no longer dead code.

### Driver: MoveRejectedEvent

When a player tries an illegal placement (drops dragged cards on
a real pile but the move violates the rules), `MoveRejectedEvent`
fires. The existing rejection-feedback chain plays
`card_invalid.wav` (audio cue) and triggers the destination-pile
shake (visual cue via `feedback_anim_plugin`). This commit adds a
third leg — a 2-second pink-bordered Error toast reading
"Invalid move" — primarily for accessibility:

- **Audio cue alone** doesn't help deaf players.
- **Visual shake alone** is brief and easy to miss for low-vision
  players or anyone with reduce-motion enabled (which gates the
  shake's animation timing).
- **Toast text** is persistent ~2 s, readable, and unambiguous.

The three legs together cover the major perception channels.

### Implementation

New `handle_move_rejected_toast` system in `animation_plugin`
mirrors the shape of `handle_xp_awarded_toast` — read events,
fire `spawn_toast(commands, "Invalid move", 2.0,
ToastVariant::Error)`. Registered in the plugin's Update set
between `handle_xp_awarded_toast` and `tick_toasts` so the toast
spawn pipeline picks it up the same frame the event fires.

`AnimationPlugin::build` gains
`.add_message::<MoveRejectedEvent>()` so the message is
initialized when the plugin runs under MinimalPlugins (tests).
The message is also registered by `feedback_anim_plugin` —
Bevy's `add_message` is idempotent, so both registrations
coexist cleanly.

Also drops the `#[allow(dead_code)]` from `ToastVariant::Error`
(stale now that the variant has a real consumer) and updates the
variant's doc comment to point at `handle_move_rejected_toast`.

### Test

New `move_rejected_event_spawns_error_toast` pins the wiring:
firing a `MoveRejectedEvent` spawns exactly one `ToastOverlay`
on the next tick. Matches the shape of the existing
`info_toast_event_spawns_toast_overlay` test. 1195 passing
(+1 from prior 1194).

Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:59:39 -07:00
funman300 ec804d54c6 feat(accessibility): finish HC chrome rollout — home + settings panel borders
Continues the rollout from `c9af1ea` (modal scaffold) and
`d87761d` (tooltip + 3 panels). Tags the remaining 7 static-
border surfaces in the chrome so the HC chrome thread is
effectively complete:

- **`home_plugin.rs` × 3**: the home-screen Level/XP/Score
  summary row (line 842), the home-screen mode-selector
  buttons (line 945), the home-screen mode-hotkey chips
  (line 1158).
- **`settings_plugin.rs` × 4**: the card-back picker swatches
  (line 1952), the theme picker swatches (line 2093), the
  Sync Now button (line 2214), and the swatch glyph buttons
  (line 2274).

Pre-tagging audit: confirmed none of these sites have a
dynamic-paint system that would race the
`update_high_contrast_borders` system. `paint_action_buttons`
in `hud_plugin.rs` only paints entities tagged with the
`ActionButton` marker (HUD buttons only). The focus-overlay
system in `ui_focus.rs` spawns *separate* overlay entities for
focus indication, never mutating the original `BorderColor`.
Settings panel buttons / swatches use their own
`SettingsButton` enum for click routing; their `BorderColor`
is set at spawn time and not touched again.

After this commit, every `BorderColor::all(BORDER_SUBTLE)` site
in the chrome (excluding the dynamic-paint sites that are
intentionally skipped — HUD action buttons, modal buttons,
radial menu rim) carries a `HighContrastBorder` marker. The
HC thread for chrome borders is closed; the dynamic-paint
sites remain open for a future iteration that needs a
different shape (folding HC into the dynamic-paint logic, or
having HC consult hover/focus state).

1194 passing / 0 failing across the workspace (unchanged — no
new tests; the system-level lifecycle of `HighContrastBorder`
was already covered by the modal-scaffold scaffolding in
`c9af1ea`). Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:47:58 -07:00
funman300 d87761d451 feat(accessibility): roll HighContrastBorder out to tooltip + 3 panel borders
Continues the HC chrome rollout started by `c9af1ea` (which wired
just the modal scaffold). Tags four more static-border surfaces
so they boost to `BORDER_SUBTLE_HC` (#a0a0a0) when high-contrast
mode is on:

- **Tooltip** (`ui_tooltip.rs:191`). The hover-revealed caption
  popup. Border legibility matters because tooltips are usually
  brief — if the player has to squint to find the panel edge,
  the tooltip dismisses before they've parsed it.
- **Onboarding banner key chips** (`onboarding_plugin.rs:388`).
  The first-run UI's "press H or ?" key chips. First-run
  onboarding has the highest stakes for accessibility — a
  low-vision player who can't see the chips can't discover
  the help system.
- **Help panel key chips** (`help_plugin.rs:265`). Same
  treatment as the onboarding chips: keyboard-shortcut chips
  inside the F1 cheat sheet.
- **Stats panel cells** (`stats_plugin.rs:1019`). The S-key
  overlay's individual stat cells. A dense grid of bordered
  numbers is exactly the kind of surface where HC's
  `#505050 → #a0a0a0` boost makes the layout legible.

Each tagging is one line on the spawn tuple plus an import. The
existing `update_high_contrast_borders` system in
`settings_plugin` (added in `c9af1ea`) handles all tagged
entities uniformly — no system changes needed.

### Skipped on this pass

Sites with dynamic hover/focus paint systems (HUD action
buttons, modal buttons, radial menu rim) intentionally not
tagged because their existing paint cycles would race the HC
system. Wiring HC into those needs a different shape — either
fold HC into the dynamic-paint logic, or have HC consult the
hover/focus state. Future scope.

Other HC-tagging candidates (`home_plugin.rs:842/945/1158` home
menu element borders, `settings_plugin.rs:1952/2093/2214/2274`
settings panel rows) are likely fine to tag but I'm capping
this commit at four to keep it reviewable. Pattern is
established; future commits can extend.

1194 passing / 0 failing across the workspace (unchanged — no
new tests; the system-level test in `c9af1ea`'s scaffolding
covers all tagged entities uniformly). Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:43:04 -07:00
funman300 2fb2d638bf feat(replay): floating MOVE chip above the focused card during playback
Resume-prompt Option B (smaller scope variant) — closes the
"floating MOVE chip" piece flagged as future scope in v0.21.1's
replay-overlay punch list. Leaves the multi-session screen-
takeover redesign for a future B-2.

The existing banner-anchored MOVE chip stays put — it provides
the at-a-glance overview. The new floating chip mirrors the same
text but renders above the destination pile of the most-recently-
applied move, keeping progress at the player's focal point so they
don't have to look up at the banner during fast-paced playback.

### Architecture

- New `ReplayFloatingProgressChip` marker component on a
  `Text2d` entity rendered in 2D world space. World-space
  placement (rather than UI-space + camera projection) keeps
  the math trivial — the chip uses the same `LayoutResource`
  pile coordinates that drive every other piece of pile
  geometry, so it stays correctly positioned through window
  resizes without any extra wiring.
- Lifecycle matches the banner overlay: `spawn_overlay` spawns
  the chip alongside the banner when a replay starts;
  `react_to_state_change` despawns it when the replay ends.
  The chip lives outside the UI tree (because it's world-space)
  so the despawn needs its own query — added a second
  `Query<Entity, With<ReplayFloatingProgressChip>>` parameter.
- Z = 100 keeps the chip above every card stack
  (Z_DROP_OVERLAY = 50, Z_STOCK_BADGE = 30, regular tableau
  cards stack to the low double digits at most).

### Position + visibility logic

`update_floating_progress_chip` runs each Update tick:

- Resolves the destination pile of the last-applied move
  (`replay.moves[cursor - 1]`'s `to`).
- Hides the chip when `cursor == 0` (no moves applied yet —
  nowhere meaningful to land) or when the last move was a
  `StockClick` (no destination pile, and stock-click feedback
  already lives at the stock pile — letting the chip jitter
  back to the stock every cycle would be visual noise).
- Otherwise positions the chip at `pile_position + (0,
  card_size.y * 0.6)` — half a card lifts above the pile
  centre, the extra 10 % is breathing room above the card's
  top edge so the chip doesn't visually clip.
- Updates the chip text via `format_progress(&state)` —
  shares the same MOVE N/M format with the banner chip.

### Test

New `floating_chip_spawns_and_despawns_with_overlay` pins the
lifecycle: chip absent on Inactive, exactly one chip on Playing,
absent again on return to Inactive. Position correctness needs
`LayoutResource` (which the headless fixture doesn't set up);
covered via running-game verification rather than a unit test —
the system's gate logic is small enough that pixel positioning
isn't load-bearing on a test.

1194 passing (+1 from prior 1193). Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:29:38 -07:00
funman300 c9af1ead22 feat(accessibility): wire BORDER_SUBTLE_HC into the modal scaffold
Resume-prompt Option E, part 2 of 2 — HC chrome borders. Pairs
with the reduce-motion gating in `ed152e2`.

v0.21.1 introduced `BORDER_SUBTLE_HC` (#a0a0a0) but never wired
it: the constant existed, no consumer used it. Spec at
`design-system.md` §Accessibility (#2) mandates outline boost
from `#505050` (BORDER_STRONG) to `#a0a0a0` under high-contrast
mode so panels and popovers stay legible on low-quality
displays.

### Architecture

- New `HighContrastBorder` component in `ui_theme` carrying a
  `default_color: Color` field that records the off-state colour
  the entity was spawned with. Tag any UI node where border
  legibility is accessibility-critical.
- New `update_high_contrast_borders` system in `settings_plugin`
  walks all tagged entities each Update tick, sets `BorderColor`
  to `BORDER_SUBTLE_HC` when `Settings::high_contrast_mode` is
  on, otherwise to `marker.default_color`. Compares against
  current `BorderColor` and only mutates when different so
  Bevy's change-detection doesn't trigger repaints every frame.

### Tagged in this commit

- The modal scaffold's card border (`ui_modal::spawn_modal`).
  This is the primary accessibility target — modals demand
  attention and a low-vision player needs to perceive the panel
  boundary. Default colour: `BORDER_STRONG` (#505050); HC
  variant: `BORDER_SUBTLE_HC` (#a0a0a0).

### Future scope

Other `BORDER_SUBTLE` / `BORDER_STRONG` consumer sites (help
panel, stats panel, tooltip, action buttons, settings rows,
etc.) can be tagged in follow-ups by adding
`HighContrastBorder::with_default(...)` to their spawn tuple.
The system handles any entity carrying the marker — no further
changes needed once a site is tagged. Started small here to
keep the commit reviewable and prove the architecture before
rolling out broadly.

Workspace clippy + cargo test --workspace clean. 1193 passing
(unchanged from prior — no new tests added; the system is
small enough that the running-game verification is the meaningful
check).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:13:13 -07:00
funman300 ed152e2d8f feat(accessibility): gate splash scanline + cursor pulse on reduce-motion
Resume-prompt Option E, part 1 of 2 (the reduce-motion piece;
HC chrome borders follow in a separate commit).

v0.21.1 wired `Settings::reduce_motion_mode` through
`effective_slide_secs` so cards snap instead of sliding under
reduce-motion. The design-system spec at §Accessibility (#3)
calls out two more sources of non-essential motion that
reduce-motion should suppress: the splash CRT scanline effect
and the splash cursor pulse. This commit gates both.

### Splash cursor pulse (`pulse_splash_cursor`)

Previously sine-pulsed every frame regardless of settings. Now
reads `Settings::reduce_motion_mode` and skips the pulse
multiplier when on — the cursor still fades in / out with the
global splash alpha (essential timing), but doesn't blink
(decorative motion). The fade is preserved on purpose: skipping
it would hard-cut the splash on/off, which is jarring; the spec
specifically calls out *non-essential* motion as the reduce-
motion target, and a decorative blink is more clearly
non-essential than a fade timeline.

### Splash scanline overlay (`spawn_splash`)

Previously generated and spawned unconditionally when
`Assets<Image>` was available. Now skipped entirely when
reduce-motion is on — without the scanline overlay the boot
screen still reads as terminal-themed (foreground content,
borders, palette swatches all unchanged); the scanlines are
purely decorative.

### Test

New `splash_skips_scanline_overlay_under_reduce_motion` pins
the gate behaviour: under `reduce_motion_mode = true`, the
splash root still spawns (essential motion intact) but the
`SplashScanlineOverlay` entity is absent. 1193 passing
(+1 from prior 1192).

Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 13:07:51 -07:00
funman300 279a834f9d docs(handoff): refresh post-v0.21.1 — anchor to new tag, renumber Resume menu
Mirrors the post-v0.20.0 → v0.21.0 → v0.21.1 cut-then-refresh
pattern. Cut commit (daa655a) edited only CHANGELOG; this
follow-up resets the handoff so a fresh session picks up cleanly
post-v0.21.1.

Updated:
- Last-updated header points to v0.21.1 at daa655a; opening
  paragraph summarizes the patch's three threads (icon,
  accessibility, card-visual iteration with two bug fixes).
- Status at pause: tests bumped to 1192 (net +8 from
  v0.21.0's 1184); tags list extended through v0.21.1.
- "Since the v0.21.0 cut" → "Since the v0.21.1 cut" with the
  closure narratives dropped (now in CHANGELOG.md § [0.21.1]).
  Section reset to "no threads in flight" placeholder so
  future post-cut work has a clean starting point.
- Resume prompt menu trimmed: A and F closure entries dropped
  (preserved in CHANGELOG); remaining options renumbered A-E
  with the v0.21.1 closure callouts inline. New option E
  added: "extend HC through chrome borders + reduce-motion to
  splash/warning-chip" — both small finite items that v0.21.1
  flagged as future scope.
- Workflow notes gain the doc-vs-implementation-drift pattern
  observation from the pile-marker fix: when a module's
  top-level doc comment claims "X happens" but no code enforces
  it, the gap is invisible until a player notices the missing
  behaviour. Worth checking such claims and adding tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:59:24 -07:00
funman300 daa655a0af docs: cut v0.21.1 — icon, accessibility, card-visual iteration
Promotes the [Unreleased] section to [0.21.1] dated 2026-05-08
and opens a fresh empty [Unreleased]. Patch release covering the
10 post-v0.21.0 commits.

Two Resume-prompt options closed:

- A — App icon. Runtime Window::icon wired via WinitWindows on
  desktop (target-gated to non-Android since Android draws its
  launcher icon from the APK manifest); 9-size PNG hierarchy at
  assets/icon/ generated by a new icon_generator example from a
  shared icon_svg builder. The follow-up `716a025` wraps
  NonSend<WinitWindows> in Option<...> to satisfy Bevy 0.18's
  stricter system-param validation.
- F — High-contrast and reduce-motion accessibility modes.
  Settings flags wired through the engine + Settings panel UI
  toggles. CBM and HC compose; reduce-motion forces card slide
  duration to 0 regardless of AnimSpeed.

Card-visual iteration cycle moved through three states: v0.21.0
Terminal pink/gray → 4-colour-deck experiment (`62b61cc`) →
traditional 2-colour reversion at player request (`ddb6540`,
saturated red + near-white). Two visible bugs surfaced and
were fixed:

- `dd97021` dropped the suit-coloured card border to remove
  anti-aliasing artifacts at the rounded corners.
- `4d48cad` hides pile markers when occupied — the actual
  visible-artifact fix for "gray L corners". Implements the
  documented but previously-not-enforced "remain visible only
  where a pile is empty" invariant in table_plugin's module
  doc.

cargo clippy --workspace --all-targets -- -D warnings clean.
1192 passing / 0 failing (net +8 from v0.21.0's 1184).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.21.1
2026-05-08 12:56:32 -07:00
funman300 4d48cad4e3 fix(engine): hide pile markers under cards — kill the gray-corner artifact
Player feedback after the border-drop fix did NOT close the
"gray corners" complaint: "I do not see anything change." The
border was a real artifact, but the *visible* gray came from a
different source.

Root cause: pile markers are 8%-alpha-white sprites sized to
the card area, sitting at `Z_PILE_MARKER = -1.0` beneath every
card. Composited against the dark play surface, the marker's
effective colour is ≈`#272727` — visibly gray. When a card
(rounded corners, opaque body) sits on top, the marker's
rectangular fill bleeds through the 4 small triangular regions
where the card's rounded corner curves cut away from the card's
bounding rectangle. That bleed-through is the "gray L" the
player saw at each card corner.

Fix: hide pile-marker sprites for any pile that has a card on
top. New `sync_pile_marker_visibility` system runs each Update
tick, guarded by `game.is_changed()` so the work skips on idle
frames. Iterates `(&PileMarker, &mut Visibility)` and sets
`Hidden` for occupied piles, `Inherited` for empty.

This implements the *documented* invariant declared in the
module-level doc comment ("Pile markers ... remain visible only
where a pile is empty") that was previously not enforced —
markers always rendered. Strictly speaking this is a
documentation-vs-implementation drift fix, not a behaviour
change.

### Why the border-drop fix didn't address this

The border drop changed the SVG stroke and removed *one* source
of corner artifacts (anti-aliased red/near-white stroke fading
through gray). It correctly drifted 52 face hashes. But the
visible gray at corners came from a *different* layer — the
pile-marker sprite *behind* the card, not the card stroke
itself. Right test target, wrong visible-artifact target.
Two layers, two fixes; this commit closes the second.

### Test

New `pile_markers_hide_when_pile_is_occupied` pins the
post-deal state: 8 markers hidden (stock + 7 tableau), 5
markers visible (waste + 4 foundations). 1192 passing
(+1 from prior 1191).

Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:49:13 -07:00
funman300 dd970215cc fix(engine): drop card-face border to remove gray-corner artifact
Player feedback after the 2-colour revert: "I do not like the
grey corners on the cards." The visible artifact was anti-
aliasing physics — the 1 px suit-coloured stroke (red for
hearts/diamonds, near-white for clubs/spades) faded through
gray pixels into the dark play surface at each rounded corner,
producing a visible "gray sliver" at the four arcs of every
card.

Fix: drop the stroke entirely. The card body fill defines the
shape against the play surface; the 5-unit brightness gap
between `#1a1a1a` body and `#151515` surface is enough to read
as a card edge without an explicit stroke. Anti-aliasing on a
fill-only rounded rect blends `#1a1a1a → #151515` over a few
pixels — barely perceptible compared to the
`stroke → transparent` gradient that produced the artifact.

### Changes

- `card_face_svg.rs`: removed `stroke="{colour}" stroke-width="2"`
  from the card body rect. Reverted the 1 px stroke inset back
  to `(x=0, y=0, width=256, height=384)` since there's no
  longer a stroke to keep inside the pixmap. Module-level
  comment updated to document the reasoning.
- `design-system.md` § Game Cards line 225 updated: "Border:
  1px solid in suit color" → "Border: none." with the
  artifact rationale recorded as audit trail.
- `card_face_svg_pin.rs` rebaselined: all 52 face hashes drift
  (every card's perimeter pixels changed); 5 back hashes
  unchanged.

Workspace clippy + cargo test --workspace clean. 1191 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:41:54 -07:00
funman300 ddb65403c2 feat(engine): revert to traditional 2-colour deck with saturated red + near-white
Per player feedback after the brief 4-colour-deck experiment:
"can we make the card suit colors the same as a regular
solitaire game would." Reverts the 4-colour split (`62b61cc`)
and bumps both 2-colour hues to read more like a real
Microsoft-Solitaire-on-dark-mode deck.

### Constants

- `RED_SUIT_COLOUR`: `#fb9fb1` (Terminal pink, then briefly
  hearts-only) → `#e35353` (saturated red). More chromatic, less
  pastel; reads as "the red suit" rather than "a Terminal-
  themed pink." Visually distinct from `ACCENT_PRIMARY`
  `#a54242` (the brick-red CTA accent) so chrome and suit don't
  collapse to the same hue.
- `BLACK_SUIT_COLOUR`: `#d0d0d0` (matched `TEXT_PRIMARY`) →
  `#e8e8e8` (near-white). Bumped slightly brighter so it reads
  as a chromatic-neutral counterpart to the new saturated red,
  not as "the same gray as body text." `TEXT_PRIMARY_HC`
  (`#f5f5f5`) is still brighter for the high-contrast boost
  path.
- `RED_SUIT_COLOUR_HC`: `#ff8aa0` (pinkish boost matching the
  v0.21.0 pink default) → `#ff6868` (brighter saturated red).
  Now reads as "more chromatic" than the new default red, not
  "less saturated."
- `DIAMOND_SUIT_COLOUR` and `CLUB_SUIT_COLOUR` deleted — the
  4-colour split is gone, hearts/diamonds re-pair under
  `RED_SUIT_COLOUR` and clubs/spades under
  `BLACK_SUIT_COLOUR`.

### `card_face_svg.rs`

- Module-level constants collapse from four (`SUIT_HEART` /
  `SUIT_DIAMOND` / `SUIT_CLUB` / `SUIT_SPADE`) back to two
  (`SUIT_RED` / `SUIT_DARK`) at the new saturated-red /
  near-white values.
- `suit_paint()` reverts to the 2-colour pairing: hearts
  filled-red, diamonds outlined-red, spades filled-near-white,
  clubs outlined-near-white. Filled-vs-outlined glyph
  differentiation stays the always-on CBM fallback.

### `card_plugin.rs`

- `text_colour()` reverts to a `card.suit.is_red()`
  bifurcation. Comment block updated to reflect the new
  truth table: red suits → saturated red (or CBM lime / HC
  brighter red); dark suits → near-white (or HC brighter
  near-white).

### Tests

Test block restructured back to the pre-4-colour shape: two
red/black pairing tests instead of one 4-colour distinctness
test. CBM/HC compose tests retuned to the 2-colour world (red
suits compose, dark suits compose; no separate diamonds-immune
or clubs-immune cases). 1191 passing / 0 failing — net 0 from
the prior commit (3 tests removed: the 4-colour distinctness
test + the diamonds/clubs-immune test; 2 tests added back: the
red-pairing + dark-pairing tests; existing tests amended to
new colour assumptions).

### `card_face_svg_pin`

All 52 face hashes drift (every suit's colour shifted); 5 back
hashes unchanged. Surgical rebaseline.

### `design-system.md`

§Suit Colors retitled "Two-color traditional pairing", table
updated with the new hex values, CBM section text simplified
back to red→lime swap on both red suits.

Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:35:36 -07:00
funman300 62b61cc786 feat(engine): switch card fronts to 4-colour deck
Hearts pink (`#fb9fb1`), Diamonds gold (`#ddb26f`), Clubs lime
(`#acc267`), Spades gray (`#d0d0d0`) — each suit picks up its
own base16-eighties accent so a player scanning the table can
distinguish the suit by hue alone (faster recognition than the
2-colour traditional red/black scheme; common in poker decks).
All four colours already exist in the palette as semantic
state-token accents, so this is a pure remapping at the suit-
glyph site, not a palette extension.

The outlined-glyph differentiation (♦ ♣ outlined, ♥ ♠ filled)
is preserved on top of the colour split — it stays the always-
on colour-blind fallback per `design-system.md` §Accessibility,
and matters more than ever now that CBM hearts (lime) and
default clubs (lime) share a hue.

### Changes

- `card_face_svg.rs`: split `SUIT_RED` / `SUIT_DARK` into four
  per-suit constants (`SUIT_HEART` / `SUIT_DIAMOND` / `SUIT_CLUB`
  / `SUIT_SPADE`). `suit_paint()` returns each suit's own
  colour. Card border picks up the suit colour automatically
  via the existing `(colour, paint)` destructure.
- `card_plugin.rs`: new `DIAMOND_SUIT_COLOUR` + `CLUB_SUIT_COLOUR`
  constants; `text_colour()` rewritten as a per-suit match (was
  red/black bifurcation). Both rendering paths (PNG production +
  constant fallback under MinimalPlugins) stay in lockstep.
- CBM behaviour clarified: only hearts swap to lime now;
  diamonds + clubs + spades are already hue-distinct from
  the heart pink and stay unchanged. Under CBM the heart
  (lime) and club (lime) share a hue but stay distinguishable
  via the always-on filled-vs-outlined glyph differentiation.
- HC behaviour: only hearts (→ HC red) and spades (→ HC white)
  have defined boosts. Diamonds (gold) and clubs (lime) are
  already mid-luminance accents and stay at their default.
  New test `text_colour_diamonds_and_clubs_are_immune_to_accessibility_flags`
  pins all four flag combinations as no-ops for the gold +
  lime suits.
- `design-system.md` §Suit Colors retitled "Four-color deck"
  with the 4-colour table; CBM section text updated to
  describe the hearts-only swap and the hearts/clubs hue
  collision under CBM.
- `card_face_svg_pin.rs` rebaselined: 26 hashes drift
  (13 clubs + 13 diamonds — the two suits whose colours
  changed). Hearts, spades, and the 5 backs all keep their
  prior hashes. Surgical scope, exactly what the pin test
  was designed to surface.

### Tests

1191 passing / 0 failing — net 0 from the prior baseline:
two old 2-colour tests removed
(`text_colour_is_red_for_hearts_and_diamonds`,
`text_colour_is_black_for_clubs_and_spades`), one consolidated
4-colour test added
(`text_colour_4_colour_deck_assigns_each_suit_its_own_hue`)
plus a pairwise-distinct invariant guard, and one new test
covering the gold/lime suits' immunity to CBM/HC flags. Six
existing CBM/HC tests rewritten to use only the suits each flag
actually affects under the new scheme (hearts for CBM, hearts +
spades for HC).

Workspace clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:00:55 -07:00
funman300 31139ae455 docs(handoff): record Options A + F closures, refresh Resume prompt menu
Two post-v0.21.0 options closed today; "Since the v0.21.0 cut"
section now narrates both:

- A — App icon (`3eb3a26` + `716a025`). Runtime Window::icon
  wired via WinitWindows on desktop, 9-size PNG hierarchy at
  assets/icon/. The follow-up `716a025` wraps NonSend in
  Option<...> to satisfy Bevy 0.18's stricter system-param
  validation.
- F — Accessibility modes (`c5787c6` + `07e0357`). High-
  contrast and reduce-motion settings flags + Settings UI
  toggles + engine wiring. CBM and HC compose; reduce-motion
  forces card slide_secs to 0.

Open punch list refreshed:

- Visual-identity follow-ups: HC and reduce-motion entries
  marked closed with future-scope notes (HC chrome borders,
  reduce-motion splash gating).
- Carried forward from v0.19.0: App icon entry marked closed
  with future-scope note for .ico/.icns bundle formats (need
  new deps + matter only at packaging time).

Resume prompt menu trimmed: A and F decision options now
marked closed inline (preserved for audit-trail readability).
B, C, D, E remain live.

No runtime / test changes — pure docs hygiene to keep the
handoff orientation accurate as work flows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 11:28:27 -07:00
funman300 07e035771c feat(accessibility): add Settings UI toggles for high-contrast + reduce-motion
Resume-prompt Option F, part 2 of 2 — pairs with the engine
wiring in c5787c6. Adds two toggle rows to the Settings panel
under Cosmetic so players can flip the new accessibility flags
without hand-editing settings.json.

Mirrors the Color-blind Mode row pattern almost exactly:

- Two new marker components (`HighContrastText`,
  `ReduceMotionText`) tagging the Text nodes that show
  ON/OFF.
- Two new `SettingsButton` enum variants
  (`ToggleHighContrast`, `ToggleReduceMotion`) with
  `focus_order` 61/62 — sit right after `ToggleColorBlind` (60)
  so tab-walk visits all three accessibility flags in one
  vertical run before continuing to picker rows.
- Two new click-handler branches in `handle_settings_buttons`
  flipping the bool, persisting, broadcasting
  `SettingsChangedEvent`, and updating the row label.
- Two new live-label updaters
  (`update_high_contrast_text`, `update_reduce_motion_text`)
  so the row reflects external changes (e.g. someone editing
  settings.json mid-session, or a future a11y-import feature).
- Generic `on_off_label(enabled: bool) -> String` helper shared
  by both new toggles. Could fold `color_blind_label` and
  `winnable_deals_only_label` into it too — punted for scope;
  both already work and a name-only refactor would just churn
  the diff.

Query-disambiguator chains updated: every existing settings-text
query in `handle_settings_buttons` gains
`Without<HighContrastText>, Without<ReduceMotionText>` at the
end so the new components don't ambiguate the existing
mutations. The two new queries carry mirrored `Without<...>`
chains for the same reason. Verbose but matches the existing
pattern; future Bevy archetype-set query API would simplify
this, not in 0.18.

Workspace clippy + cargo test --workspace clean. 1191 passing
(unchanged from c5787c6 — UI plumbing has no test coverage in
this commit; the toggle behaviour is exercised through the
engine tests in c5787c6).

Closes Resume-prompt Option F.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 11:26:24 -07:00
funman300 c5787c6953 feat(accessibility): wire high-contrast + reduce-motion modes through engine
Resume-prompt Option F, part 1 of 2. Adds two accessibility flags
to Settings and threads each through the engine surfaces that
react to them. Settings UI toggle rows follow in a separate
commit; players who want to test today can edit `settings.json`
manually.

Spec at `docs/ui-mockups/design-system.md` §Accessibility (#2 and
#3).

### High-contrast mode

`Settings::high_contrast_mode: bool` (defaults to false; serde-
default for back-compat). When on:

- Red-suit text colour boosts from `RED_SUIT_COLOUR` (`#fb9fb1`)
  to a new `RED_SUIT_COLOUR_HC` (`#ff8aa0`).
- Black-suit text colour boosts from `BLACK_SUIT_COLOUR`
  (`#d0d0d0`) to a new `TEXT_PRIMARY_HC` (`#f5f5f5`).
- New `BORDER_SUBTLE_HC` (`#a0a0a0`) constant available for
  future chrome-side wiring (this commit only routes HC through
  card text rendering — chrome border boost is a separable
  follow-up).

The HC and CBM flags compose. CBM red→lime wins over HC on red
suits when both are on (lime is itself a high-luminance accent,
so the HC boost has nothing further to do). HC still applies to
black suits when both flags are on (CBM doesn't touch black).
Four new `text_colour` tests pin the truth table.

### Reduce-motion mode

`Settings::reduce_motion_mode: bool` (defaults to false; serde-
default for back-compat). When on:

- Card-slide animation duration is forced to `0.0` regardless of
  the player's `AnimSpeed` selection — cards snap instantly to
  their target position. Implemented by extracting a new
  `effective_slide_secs(&Settings)` helper that wraps
  `anim_speed_to_secs` with the reduce-motion gate.
- Future scaffolding hooks (splash scanline, warning-chip pulse,
  card-lift z-bump animation) follow the same `if
  settings.reduce_motion_mode { skip }` pattern when wired —
  stays out of scope for this commit since each motion path
  needs its own per-system gate.

Two new tests cover the gate behaviour and the fall-through-to-
AnimSpeed pass-through path.

### Threading

`text_colour` signature extended with a `high_contrast: bool`
parameter; `sync_cards` / `sync_cards_startup` /
`sync_cards_on_change` / `sync_cards` core / `spawn_card_entity`
/ `update_card_entity` all gain a parallel parameter mirroring
the existing `color_blind: bool` plumbing. Verbose but matches
the established pattern; a future refactor could pack both into
an `AccessibilityView` struct, but bigger blast radius.

### Stats

1191 passing / 0 failing across the workspace (net +6 from
v0.21.0's 1185 baseline once the icon-pin test landed):
- 4 new `text_colour` HC tests in `card_plugin`
  (red-suit boost, black-suit boost, CBM-wins-on-red,
  black-suits-with-CBM+HC-still-boost).
- 2 new `effective_slide_secs` tests in `animation_plugin`
  (zero-out under reduce-motion, fall-through to AnimSpeed when
  off).

`cargo clippy --workspace --all-targets -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 11:23:22 -07:00
funman300 716a025352 fix(app): wrap WinitWindows in Option to satisfy Bevy 0.18 param validation
`NonSend<WinitWindows>` failed system-param validation on the
first few frames before `WinitWindows` was populated, panicking
the Update system before any logic could run. Bevy 0.18's
stricter validation panics rather than skips when a non-send
resource is absent, with an error message spelling out the fix:
*"wrap the parameter in `Option<T>` and handle `None` when it
happens."*

Wraps `winit_windows` as `Option<NonSend<WinitWindows>>` and
early-returns on `None`, mirroring the same lifecycle handling
already applied to `winit_windows.get_window(primary_entity)` —
both fail in the same window of frames before winit's `Resumed`
event fires.

Repro from the user's `cargo run` log:
```
thread 'Compute Task Pool (2)' panicked at .../bevy_ecs-0.18.1/src/error/handler.rs:125:1:
Encountered an error in system ...: Parameter ... failed validation:
Non-send resource does not exist
```

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 11:09:27 -07:00
funman300 3eb3a26789 feat(app): wire desktop window icon — Terminal ▌RS mark at runtime
Closes Resume-prompt Option A (the post-v0.21.0 first option).
Half-day desktop work, no cert dependency.

Three deliverables:

1. **SVG-authored icon** (`solitaire_engine/src/assets/icon_svg.rs`)
   — square Terminal mark: `#151515` background, brick-red
   `#a54242` 1 px border, brick-red ▌ cursor block centered, "RS"
   monogram in `#d0d0d0` foreground gray beneath. Same shape that
   already lives on the splash boot screen and card-back monogram,
   reused as the project's signature visual mark. Authored in a
   64-unit logical box so it scales cleanly at every rasterisation
   target.

2. **9-size PNG hierarchy** (16, 24, 32, 48, 64, 128, 256, 512, 1024
   px) regenerated by `solitaire_engine/examples/icon_generator.rs`
   into `assets/icon/icon_<size>.png`. Sizes cover Linux hicolor
   (16, 24, 32, 48, 64, 128, 256, 512), Windows .ico targets (16,
   32, 48, 256), and macOS .icns targets (16, 32, 64, 128, 256,
   512, 1024). The runtime path uses just the 256 px slot; the
   smaller sizes are pre-rendered for downstream packaging.

3. **Runtime `Window::icon` wiring** (`solitaire_app/src/lib.rs`).
   Bevy 0.18 has no `Window::icon` field — the icon is set through
   the underlying `winit::window::Window` via the `WinitWindows`
   resource. `set_window_icon` runs each Update tick, retries
   silently until `WinitWindows` is populated (typically frame 1
   or 2), decodes the embedded 256 px PNG via `tiny_skia`, builds
   a `winit::window::Icon`, and self-disables via `Local<bool>`.
   Same one-shot pattern as `apply_smart_default_window_size`.
   Desktop-only — Android draws its launcher icon from the APK
   manifest, so the system is target-gated to
   `cfg(not(target_os = "android"))`.

Dep changes (CLAUDE.md §8 user-confirmed):

- `winit = "0.30"` promoted from a transitive Bevy dep to a direct
  dep on `solitaire_app` so `winit::window::Icon` is in scope —
  bevy_winit 0.18 doesn't re-export it. Version pinned to whatever
  Bevy uses; if Bevy bumps winit, this line bumps in lockstep.
- `tiny-skia` added as a direct dep on `solitaire_app` for PNG →
  RGBA decode. Already in workspace deps for `solitaire_engine`;
  no version drift risk.
- Both new deps target-gated to non-Android only.

Test infrastructure: `solitaire_engine/tests/icon_svg_pin.rs`
hashes the rasterised RGBA bytes at all 9 sizes via FNV-1a (same
shape as `card_face_svg_pin`). Bootstrap pattern (empty EXPECTED
→ panic with hashes formatted as Rust source → paste back in)
handles future intentional builder edits cleanly.

Workspace clippy + cargo test --workspace clean. 1185 passing
(+1 from v0.21.0's 1184 baseline — the icon pin's
`rasterised_icon_bytes_match_pinned_hashes`).

Out of scope for this commit: `.icns` / `.ico` bundling for
macOS / Windows app packaging. Both are packaging-time concerns
(set via bundle manifests, not runtime calls) and would need new
deps (`ico` and `icns` crates) — separate followup if/when the
project ships as a packaged macOS / Windows app rather than just
`cargo run`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 11:07:31 -07:00
funman300 0c1cc40266 docs(handoff): refresh post-v0.21.0 — drop historical sections, retune Resume prompt
Mirrors the v0.20.0 → post-cut refresh pattern (commit a65e5b8):
the cut commit (04f9bf9) only edits CHANGELOG.md; this follow-up
resets the handoff so it serves a fresh session cleanly rather
than carrying forward the v0.20.0-era narrative as cruft.

Removed (now redundant with CHANGELOG.md § [0.21.0]):
- The full "Since the v0.20.0 cut (un-pushed)" section — ~300
  lines of per-commit narratives for the post-tag work.
- The "What shipped in v0.20.0 (frozen at 41a009a)" section —
  v0.20.0 detail lives in CHANGELOG.md § [0.20.0].

Replaced with:
- Short header pointing to CHANGELOG.md § [0.21.0] for cycle
  detail.
- "Since the v0.21.0 cut" placeholder ("No threads in flight").

Refreshed:
- Status at pause: HEAD on origin matches local; latest tag
  v0.21.0 at 04f9bf9; tests 1184; references to v0.20.0
  baseline preserved as audit trail.
- Visual-identity follow-ups: dropped the closed entries
  (card-face arc, splash polish, replay banner pieces). Added
  what's still open: replay screen-takeover redesign, floating
  MOVE chip above focused card, toast Warning/Error wiring,
  high-contrast accessibility, reduced-motion accessibility.
- Canonical remote: dropped the "unpushed commits" warning
  since origin is caught up.
- Design direction palette: brick-red primary instead of cyan,
  red→lime CBM swap instead of red→cyan, glyph orientation
  upright. v0.21.0 source commits cited.
- Resume prompt: rebased to v0.21.0 anchor. Decision options
  rewritten — closed B/C/D dropped; live A/E/F renumbered into
  fresh A/B/C plus three new candidates (Toast variants, Phase
  8 sync, accessibility modes). Workflow notes gain the
  token-port-pattern lesson from v0.21.0's three "fallback path
  the migration walked past" follow-ups.

Net diff: −513 / +117 lines; file shrinks from 668 to 272.
v0.20.0 historical context preserved in CHANGELOG.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:53:24 -07:00
funman300 04f9bf9be3 docs: cut v0.21.0 — visual-identity completion + palette refresh
Promotes the [Unreleased] section to [0.21.0] dated 2026-05-08
and opens a fresh empty [Unreleased]. The cycle's three through-
lines:

- **Card-face / suit / card-back artwork migration.** Closes
  the v0.20.0 thread that explicitly deferred card-face palette
  migration. 10 commits across 2 days landed both rendering
  paths (assets/cards/*.png fallback + the bundled-default
  theme SVGs that include_bytes!()-embed into the binary) on
  identical Terminal art generated by shared face_svg /
  back_svg builders. The card_face_svg_pin integration test
  guards rasteriser drift via FNV-1a on raw RGBA bytes.

- **Splash + replay-overlay polish.** Closes Resume-prompt
  Options B (splash cursor pulse + scanline overlay) and C
  (replay banner ▌ label + GAME caption + MOVE chip + scrub
  bar). Splash gets the SplashFadable scaffold that lets
  future overlays fade N >> 3 elements via one marker + one
  global lerp query.

- **ACCENT_PRIMARY palette swap.** Late-cycle stakeholder
  decision: cyan #6fc2ef → brick red #a54242. Touches every
  primary-accent surface across the engine. RED_SUIT_COLOUR_CBM
  swapped from cyan to lime #acc267 in lockstep so the colour-
  blind alternative stays hue-distinct from the new red-family
  primary.

Three sign-off follow-ups surfaced once a human booted the
running game; all matched the same shape ("fallback path the
chrome migration walked past"): the embedded default theme
overrode the new PNGs, the table backgrounds were a separate
PNG path the v0.20.0 chrome migration didn't touch, and the
action-button row's font_size: 16.0 literal slipped through the
typography migration audit. All recorded under "Fixed".

Phase 8 (sync) and Phase Android runtime gaps (JNI bridges,
APK launch verification on device) remain open and roll
forward.

cargo clippy --workspace --all-targets -- -D warnings clean.
1184 passing / 0 failing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.21.0
2026-05-08 10:39:15 -07:00
funman300 a292a7ead0 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>
2026-05-08 10:30:35 -07:00
funman300 d109c32b75 docs(handoff): record Option D closure + 9-commit card-face migration arc
Updates SESSION_HANDOFF.md to reflect the post-2026-05-08 state:

- "Last updated" + status header rewritten — origin caught up
  to local through dd101b3, 1184 tests passing (net +4 from
  the 1180 baseline: splash polish +2, card-face pin +1, CBM
  test consolidation -2 then +1).
- New narrative entry under "Since the v0.20.0 cut" walks
  through the 9-commit Option D arc: plan + tooling
  (5623368/3a4bb63/babe5cc/48b28d2), lockstep step 4+5
  (e8bf9d7), the three sign-off follow-ups (a14200a default-
  theme SVG override, 8719f77 backgrounds flattened, ae84dc1
  top-bar overlap), the path-glyph fix (af414b6), and the
  glyph-orientation tweak (dd101b3).
- "Visual-identity follow-ups" punch-list: card-face item
  marked closed with the same commit chain referenced from
  the narrative.
- Resume prompt header rewritten — Options B/C/D all closed,
  the post-tag work is fully on origin. Option D's bullet
  expanded to record the closure rather than describe pending
  work.
- The "fallback path the migration walked past" pattern is
  documented explicitly so a future session can pattern-match
  on it (token migrations need a checklist of every concrete
  artifact downstream of the tokens, not just the tokens
  themselves).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:12:45 -07:00
funman300 dd101b3d54 fix(engine): render bottom-right card glyph upright (no 180° rotation)
The user noticed the bottom-right large suit glyphs were
rendering upside-down — point-up hearts, stem-up spades — because
the SVG transform pipeline applied a `rotate(180)` to match the
traditional playing-card inverted-corner convention.

That convention exists so a card reads correctly when flipped or
read from the opposite side of the table. Single-orientation
digital play doesn't benefit from it; most modern digital decks
have abandoned it. User preference is upright.

Drops the rotate from face_svg's bottom-right `<g transform>`
and adjusts the translate so the visible glyph still lands at
(178, 286)–(242, 350) — same screen footprint, same scale, just
no flip.

design-system.md § Game Cards updated in lockstep — line 220
no longer says "rotated 180°", instead documents the deliberate
deviation from the traditional convention.

Knock-on lockstep changes in this commit:
- EXPECTED in tests/card_face_svg_pin.rs rebaselined: 52 face
  hashes shift, 5 back hashes unchanged.
- assets/cards/faces/*.png regenerated (52 face PNGs).
- solitaire_engine/assets/themes/default/*_*.svg regenerated
  (52 theme face SVGs that production rasterises at startup).

Workspace clippy + cargo test --workspace clean. Pin test
passes against the new hashes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:09:55 -07:00
funman300 af414b6aed fix(engine): render card suit glyphs as SVG paths instead of text
The user's first post-migration screenshot showed near-invisible
suit glyphs on every card — the rank rendered at correct size but
the ♠ ♥ ♦ ♣ marks were tiny dots regardless of the requested
20px / 64px font-size.

Root cause: the bundled FiraMono in svg_loader::shared_fontdb
doesn't carry usable Unicode suit glyphs (U+2660-2666). usvg
silently fell back to a substitute rendering at default size,
producing the "tofu" effect.

Fixes by replacing the `<text>` glyph rendering with inline SVG
paths. `suit_path_d(suit)` returns a single closed-perimeter path
authored in a 32 × 32 logical box, then face_svg wraps it in two
`<g transform>` blocks (top-left small + bottom-right rotated
large). Path-based rendering bypasses the font system entirely
— same bytes on every machine, no fontdb dependency, no
substitution risk.

Same path data renders correctly whether filled (♥ ♠) or
outlined (♦ ♣ — the always-on color-blind glyph differentiation
from the design system).

Knock-on changes that must land in this commit per the migration
plan's lockstep rule:

- `EXPECTED` in tests/card_face_svg_pin.rs rebaselined: 52 face
  hashes change (text → path), 5 back hashes unchanged
  (back_svg untouched). The bootstrap pattern in the test
  handled the rebaseline cleanly — empty EXPECTED, re-run,
  paste, re-run.
- assets/cards/faces/*.png regenerated (the 52 face PNGs).
- solitaire_engine/assets/themes/default/*_*.svg regenerated
  (the 52 theme face SVGs that production rasterises at
  startup). Both rendering paths must agree.

Workspace clippy + cargo test --workspace clean. Pin test
passes against the new hashes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:02:04 -07:00