Commit Graph

595 Commits

Author SHA1 Message Date
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
funman300 ae84dc1504 fix(engine): clear top-bar overlap by aligning action buttons to TYPE_BODY
The post-Option-D screenshot showed the left-anchored HUD column
("Score: 0  Moves: 0  0:00") and the right-anchored action button
row colliding mid-screen at portrait/narrow window widths. Both
were absolute-positioned siblings without a shared flex parent,
so Bevy 0.15's UI couldn't auto-arrange them when their natural
widths exceeded the available horizontal space.

The action button text was a hardcoded `font_size: 16.0` literal
— a miss from the typography migration audit, since every other
text element in `hud_plugin.rs` already routes through the
`TYPE_*` tokens. Switching to `TYPE_BODY` (14.0) brings the
button row in line with the design system *and* trims roughly
12% off label widths.

Pairs with a horizontal-padding cut from VAL_SPACE_3 to
VAL_SPACE_2: 8px less on each side, six buttons, ~96px total
reclaimed across the row. Vertical padding stays at VAL_SPACE_2
so button height tracks the rest of the chrome band.

Combined effect: the action button row narrows by ~150-200px,
which is enough margin to clear typical portrait window widths
without requiring a structural refactor (a shared SpaceBetween
flex parent for HUD+actions would be more robust but touches
many query sites and was out of scope for the visual-polish
pass).

cargo clippy + cargo test --workspace clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:52:55 -07:00
funman300 8719f77ec2 fix(engine): regenerate table backgrounds to flat Terminal palette
The post-Option-D screenshot showed Terminal cards correctly but
a green felt play surface — the chrome migration only retuned
in-engine constants, leaving the on-disk PNGs at
assets/backgrounds/bg_*.png as the legacy felt textures.

Adds solitaire_engine/examples/background_generator.rs following
the same regeneratable pattern as card_face_generator. Five solid
near-black variants from the base16-eighties palette:

- bg_0: #151515 (Terminal canonical, BG_PRIMARY)
- bg_1: #0a0a0a (BG_DEEPEST)
- bg_2: #1a1a1a (BG_ELEVATED — same as card face)
- bg_3: #121820 (slight cool tint)
- bg_4: #201812 (slight warm tint)

Per design-system.md the Terminal play surface is *flat* — no
felt, no gradient — so all 5 slots are pure solid colours. Each
PNG is 120 × 168 (matches the legacy tile size; spawn_background
stretches to window_size * 2.0 at runtime so source resolution
is immaterial). On-disk weight drops from ~16KB average to ~100
bytes per tile.

Run with: cargo run --example background_generator --release

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:52:33 -07:00
funman300 a14200ac2f fix(engine): regenerate default theme SVGs to Terminal aesthetic
Step 4's PNG regeneration left the cards looking unchanged at
runtime because the PNGs at assets/cards/ are only the *fallback*
art — production renders the bundled-default theme's SVGs, which
get include_bytes!()-embedded into the binary by
solitaire_engine::assets::sources and applied to CardImageSet at
startup by theme::plugin::apply_theme_to_card_image_set. Those
SVGs were still the legacy vector-playing-cards art.

Extends card_face_generator to write SVGs into both runtime
paths in lockstep:

1. assets/cards/{faces,backs}/*.png — fallback art (unchanged
   from step 4).
2. solitaire_engine/assets/themes/default/*.svg — what production
   actually renders. 52 face SVGs + 1 back SVG, generated from
   the same face_svg / back_svg builders as the PNGs so the two
   paths can never visually diverge.

Adds two helper functions to card_face_svg:

- theme_suit_token (clubs/diamonds/hearts/spades — lowercase
  full word, matching CardKey::manifest_name)
- theme_rank_token (ace/2..10/jack/queen/king — same)

The theme back uses BACK_ACCENTS[0] (canonical Terminal cyan).
The other four accents only live as PNG fallbacks because the
theme system carries one back per theme.

Net SVG diff: -14884 / +940 lines — the legacy vector-playing-
cards SVGs were ~300 lines each of Inkscape-authored paths;
the Terminal SVGs are ~10 lines of programmatic output.

Workspace clippy + cargo test --workspace clean. Pin test
unaffected (the SVG builders themselves did not change).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:40:24 -07:00
funman300 e8bf9d79da feat(engine): migrate cards to Terminal aesthetic — artwork + constants
Step 4+5 lockstep commit closing Option D from SESSION_HANDOFF.

The 52 face PNGs + 5 back PNGs in assets/cards/ are regenerated
to the Terminal-aesthetic artwork emitted by the
card_face_generator example (#1a1a1a face, #fb9fb1 / #d0d0d0
suit glyphs, scanline-pattern backs with palette-rotated badge
accents). Resolution drops from 512×768 to 256×384 — sufficient
for ~250 px-wide desktop sprites and ~⅓ the on-disk weight.

Constant fallback path migrated in lockstep so the
constant-fallback tests (under MinimalPlugins) and the PNG path
(production) agree at every commit boundary:

- CARD_FACE_COLOUR    → #1a1a1a (was off-white #fafaf2)
- RED_SUIT_COLOUR     → #fb9fb1 (was #c71f26)
- BLACK_SUIT_COLOUR   → #d0d0d0 (was #141414)
- CARD_FACE_COLOUR_RED_CBM → renamed to RED_SUIT_COLOUR_CBM,
  value #6fc2ef (was #d9ebff). Semantic shift: pre-Terminal
  this was a face-background tint, now it's a suit-glyph
  colour swap. The Terminal face is uniformly CARD_FACE_COLOUR
  regardless of CBM; CBM only swaps red suits to cyan in the
  glyph itself.
- card_back_colour() → returns the 5 base16-eighties accents
  matching card_face_svg::BACK_ACCENTS in lockstep, so the
  test-fallback back is the same hue family as the on-disk
  PNG art for that index.

Function signatures shift to follow the semantic move:

- text_colour gains a color_blind: bool parameter (returns
  RED_SUIT_COLOUR_CBM for red+CBM).
- face_colour deleted entirely. The face is uniform
  CARD_FACE_COLOUR; card_sprite inlines the constant. CBM
  parameter dropped from card_sprite as a knock-on.

Test updates land in this commit per the migration plan:

- text_colour_is_red_for_hearts_and_diamonds + sibling: pass
  `, false` to text_colour calls now that the signature has
  the CBM bool.
- 4 face_colour CBM tests replaced with 2 text_colour CBM
  tests asserting (a) red-suit cards swap to cyan in CBM and
  (b) black-suit cards do not change.

Engine test count: 747 → 745 (net -2 from the test
consolidation — 4 face_colour tests collapsed into 2
text_colour CBM tests).

Sign-off criteria: a human still needs to `cargo run -p
solitaire_app` and confirm Terminal cards render. clippy +
cargo test --workspace clean as of this commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:33:44 -07:00
funman300 48b28d29f8 test(engine): pin card-face SVG output against rasteriser drift
Step 3 of the migration plan in docs/ui-mockups/card-face-migration.md.

Extracts face_svg / back_svg + palette constants from the
card_face_generator example into a new
solitaire_engine::assets::card_face_svg module so an integration
test can call them. The example becomes a thin wrapper.

The new tests/card_face_svg_pin.rs hashes the raw RGBA8 pixel
bytes from rasterising every face × suit + every back accent and
compares each FNV-1a fingerprint against an embedded constant.
Catches silent rendering drift if usvg / resvg / tiny_skia / the
bundled FiraMono ever change in a way that perturbs pixels.

Hashing is FNV-1a inline (~5 lines) rather than adding sha2 or
blake3 — cryptographic strength isn't load-bearing here, just
stable byte fingerprints.

When the SVG builders intentionally change, empty EXPECTED to
`&[]` and re-run the test once; it panics with the new hashes
formatted as Rust source ready to paste back in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:21:00 -07:00
funman300 babe5cc9c8 feat(engine): add full card-face SVG generator example
Generates 52 face PNGs (4 suits × 13 ranks) + 5 back PNGs
into assets/cards/. Implements step 2 of the migration plan
in docs/ui-mockups/card-face-migration.md — the bytes this
emits are what step 4 commits alongside the card_plugin
constant migration.

Filled vs outlined glyphs (♥♠ filled; ♦♣ outlined) implement
the always-on color-blind glyph differentiation from the
design system. The 5 back themes share the canonical
Terminal scanline pattern but rotate the badge accent
through the base16-eighties palette so all 5 slots stay
distinguishable without leaving the palette.

Run with: cargo run --example card_face_generator --release

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:12:54 -07:00
funman300 3a4bb63a6f feat(engine): add card-face SVG generator PoC example
Rasterises one Ace of Spades to /tmp/ace_spades_terminal.png via
the existing usvg + resvg + tiny_skia stack already used by
svg_loader. Proves the per-card grain works before looping over
all 52 faces + 5 backs in step 2 of the migration plan.

Run with: cargo run --example card_face_poc --release

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:08:13 -07:00
funman300 56233687b0 docs(ui): add card-face artwork migration plan
Lays out the lockstep migration from legacy white-card PNGs +
constants to the Terminal aesthetic. Steps 4 + 5 (artwork +
constant + test updates) must land in one commit so the PNG
path and the constant-fallback path don't visually diverge.

Tracks Option D from the SESSION_HANDOFF Resume prompt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:08:04 -07:00
funman300 73ac67d76b docs(handoff): record splash pulse + scanline; mark Option B closed
Bookkeeping pass after 29136d8 (cursor pulse) and a27cf5a (scanline
overlay) shipped — both halves of the splash polish arc deferred in
cacb19c are now done.

Changes:
- "Since the v0.20.0 cut": added per-commit narratives for 29136d8
  and a27cf5a with the implementation notes worth preserving past
  the commit log:
  - The "multiply, don't override" pattern that resolved the
    cursor-pulse / fade-timeline conflict (and generalises to any
    two ECS systems writing the same per-frame component).
  - The texture-α × tint-α GPU-composite trick that integrates the
    scanline with the fade without a new "multiplicative fadable"
    abstraction.
  - Two Bevy 0.18 API surprises (RenderAssetUsages module move;
    pixel_size() returning Result) — pinned for next time we touch
    runtime-generated images.
  - The defensive period <= 0.0 guard on cursor_pulse_factor — a
    cheap NaN-prevention pattern worth mirroring on every trig
    helper.
- "Open punch list" → "Visual-identity follow-ups": collapsed the
  two splash-polish bullets into closed pointers.
- Resume prompt → Option B: marked closed with "no further splash
  work pending unless a new mockup detail surfaces" so a future
  session knows it's a finished arc, not an in-flight one.

Three options now closed (A, B, C); D / E / F remain — all three
have a real blocker (D = multi-session, E = artwork PNGs missing,
F = Android hardware/AVD) so the next session starts with a
genuine commitment-vs-blocker decision rather than picking the
smallest piece.
2026-05-07 22:45:46 -07:00
funman300 a27cf5a020 feat(engine): add tiled scanline overlay to splash
Closes the second half of the splash polish arc deferred in cacb19c.
A fullscreen ImageNode tiles a runtime-generated 2×2 RGBA8 texture
over the splash content — top row transparent, bottom row #1a1a1a
at ~30 % alpha — producing the 1 px-pitch horizontal scanline
pattern called for in docs/ui-mockups/splash-mobile.html.

Implementation:

- New build_scanline_image() pure helper returns the 2×2 source
  texture. Pixels hard-coded as RGBA bytes (0,0,0,0 / 26,26,26,76)
  so the visible appearance is locked into source rather than
  reconstructed from constants.
- spawn_splash gains an `Option<ResMut<Assets<Image>>>` parameter;
  when present (always in production), the image is added and an
  ImageNode child of the splash root tiles it via
  NodeImageMode::Tiled { tile_x: true, tile_y: true, stretch_value: 1.0 }.
  When absent (legacy bare-MinimalPlugins tests), the overlay is
  silently skipped — the rest of the splash still spawns.
- New SplashFadableImage marker + extension to advance_splash that
  writes (1, 1, 1, global_alpha) into the ImageNode tint each tick.
  Multiplying (rather than overwriting like SplashFadableBg does)
  preserves the per-pixel 30 % alpha in the texture so the GPU
  composite is `0.3 × global_alpha` — fades cleanly with the
  splash without drifting to 100 % alpha during the hold.
- New SplashScanlineOverlay marker for tests. Distinct from
  SplashFadableImage so the test query intent stays explicit
  (there's only one fadable image today, but adding more later
  shouldn't break the scanline-locator).

Bevy 0.18 API quirks worth pinning for next time: RenderAssetUsages
is re-exported under `bevy::asset::` (not `bevy::render::render_asset`),
and TextureFormat::pixel_size() returns Result<usize, _> rather
than usize. Both fixed in the imports / debug_assert.

Headless test fixture now also init_resource::<Assets<Image>>()
since MinimalPlugins doesn't pull AssetPlugin — same pattern
settings_plugin's tests already use.

Two new tests (1183 → 1185): build_scanline_image_has_expected_2x2_rgba_bytes
locks the texture pixels literally, scanline_overlay_spawns_and_fades_with_splash
asserts spawn placement under SplashRoot and the new fade-images
branch's correctness end-to-end.

This closes Option B from the SESSION_HANDOFF Resume prompt — both
splash polish pieces (cursor pulse + scanline overlay) shipped.
2026-05-07 22:42:54 -07:00
funman300 29136d815d feat(engine): add pulsing trailing cursor to splash "▌ ready_" line
Closes the cursor-pulse half of the splash polish arc deferred in
cacb19c. The "▌ ready_" boot-log line now ends with a 6×12 px cyan
Node that pulses on a 1 s sine cadence — matching the mockup at
docs/ui-mockups/splash-mobile.html. The pulse alpha is multiplied
with the global splash fade timeline rather than fighting it: the
cursor can't reach full alpha while the rest of the splash is still
fading in, and it fades out cleanly with everything else.

Implementation:

- New SplashCursorPulse marker on the trailing Node. Carries
  SplashFadableBg too so it picks up the global fade for free; the
  pulse system overwrites the per-tick BackgroundColor afterward
  (last writer wins, both values are commensurate so the override
  is correct, not a fight).
- New pulse_splash_cursor system, scheduled .chain()'d AFTER
  advance_splash so the pulse multiplication is the final write.
  No-op when no SplashRoot exists (post-despawn or under a test
  fixture without one).
- New pure helper cursor_pulse_factor(age, period, min) returns a
  sine-driven multiplier in [min..1.0]. Defensive zero/negative
  period guard returns 1.0 so a misconfiguration produces a
  steady cursor instead of a divide-by-zero NaN.
- Two splash-local consts: MOTION_PULSE_PERIOD_SECS = 1.0 (terminal-
  blink cadence) and PULSE_ALPHA_MIN = 0.4 (the cursor never fully
  extinguishes — matches a real terminal's blink that dips but
  stays visible).

Used Node-with-explicit-dimensions rather than a `█` text glyph so
the 6×12 px size doesn't drift with line font; the leading `▌`
glyph stays a character (textual) while the trailing pulse is a
Node (geometric) — different primitives for different intents.

One new test (1182 → 1183): cursor_pulse_factor_corners pins the
peak (factor = 1 at age = period/4), trough (factor = min at age =
period * 3/4), and the defensive zero/negative-period guard.

Scanline overlay (the other half of cacb19c's skipped polish)
remains open — separate commit.
2026-05-07 22:31:55 -07:00
funman300 ef54cdeb65 docs(handoff): record GAME caption + MOVE chip; mark Option C closed
Bookkeeping pass after 54005d5 (GAME #YYYY-DDD caption) and e080b49
(MOVE N/M chip restyle) shipped — both pieces of the Option C
banner-local enrichments arc are now done.

Changes:
- "Since the v0.20.0 cut": added per-commit narratives for 54005d5
  and e080b49 with the implementation notes worth preserving past
  the commit log (the BANNER_HEIGHT 48→60 bump rationale, the Bevy
  0.18 BorderColor::all() correction, the "marker on the leaf, not
  the wrapper" ECS-design choice).
- "Open punch list" → "Replay-overlay enrichments beyond the scrub
  bar": pivoted from "tractable banner additions still open" to
  "all banner-local pieces shipped; remaining are cross-plugin or
  multi-session". Reflects current state without erasing the
  forward-looking work.
- Resume prompt → Option C: marked closed with a forward pointer to
  the cross-plugin/multi-session items that should get their own
  decision tree next time.
- Resume prompt → test count: dropped the hardcoded "1180 tests
  pass" (already stale at 1182) for "~1180+; check with
  `cargo test --workspace`" — same dynamic-reference pattern as
  44f5972's commit-count fix, applied to the next aggregate that
  was vulnerable to it.
2026-05-07 22:25:58 -07:00
funman300 e080b49914 feat(engine): restyle replay progress text as Terminal MOVE chip
Closes the centre-text half of the replay-overlay enrichments arc.
The plain "Move N of M" text becomes a 1px ACCENT_PRIMARY-bordered
chip containing "MOVE N/M" — uppercase + slash separator reads as a
Terminal output line and matches the floating-chip motif in
docs/ui-mockups/replay-overlay-mobile.html. The chip lives in-banner
rather than floating above the focused card; the screen-takeover
treatment that requires plumbing cursor → card identity remains
deferred per SESSION_HANDOFF.

Implementation: the centre Text spawn is now wrapped in a Node with
1px border + axes(VAL_SPACE_2, VAL_SPACE_1) padding and no background
fill (Terminal aesthetic gets depth from borders + tonal layering,
not shadows). The ReplayOverlayProgressText marker stays on the
inner Text so update_progress_text continues to repaint contents
unchanged. format_progress now returns "MOVE N/M" for Playing and
"REPLAY COMPLETE" for Completed (uppercase to match the chip's
typographic treatment); Inactive still returns "" since the overlay
shouldn't be spawned in that state.

Used BorderColor::all(ACCENT_PRIMARY) — Bevy's BorderColor is per-side
in 0.18, no longer the tuple struct it was earlier.

Module-level docstring + ReplayOverlayScrubFill doc comment both
updated to quote the new "MOVE N/M" string. Test
overlay_progress_text_reflects_cursor swapped its assertion to match.
1182 tests still pass; clippy clean.

This closes Option C from the SESSION_HANDOFF Resume prompt's banner-
local enrichments. The full screen-takeover redesign (mini-tableau,
playback controls, move-log scroll, WIN MOVE marker requiring a
win_move_index field on Replay) remains the multi-session item.
2026-05-07 22:22:36 -07:00
funman300 54005d5494 feat(engine): add GAME #YYYY-DDD caption beneath the replay headline
Adds the right-anchored game-identifier piece of the replay-overlay
mockup (docs/ui-mockups/replay-overlay-mobile.html), adapted to live
under the existing "▌ replay" headline rather than as a separate
top-bar surface — the screen-takeover redesign is intentionally
deferred per the SESSION_HANDOFF punch list.

The caption reads `GAME #{year}-{ordinal:03}` (e.g. `GAME #2026-122`
for a replay recorded 2026-05-02), matching the mockup's
`GAME #2024-127` motif. Year + chrono ordinal gives a compact,
monotonically-increasing identifier that's grep-friendly across
replay files. TYPE_CAPTION (11 px) / TEXT_SECONDARY paint so the
caption reads as subordinate metadata, not a callout.

Implementation: new ReplayOverlayGameCaption marker, new pure
helper `format_game_caption(state) -> Option<String>` (None for
Inactive / Completed since the replay is consumed in those branches),
left-side label spawn restructured into a column container holding
the headline + caption with a 2 px row gap. BANNER_HEIGHT bumped
48 → 60 px so the column fits without overflow (16 px vertical
padding + 1 px scrub + ~39 px content; +12 px banner mass is the
deliberate cost of the new content).

Two new tests (1180 → 1182): format_game_caption_covers_state_corners
pins the three branches (Inactive / Completed / Playing) plus the
zero-pad-to-3-digits invariant for early-January ordinals; and
overlay_game_caption_shows_replay_date drives ReplayPlaybackState
end-to-end and asserts the caption text on spawn and that the
overlay stays spawned through Playing → Completed.

MOVE chip restyle from the same mockup is the next commit.
2026-05-07 22:19:49 -07:00
funman300 44f5972edd docs(handoff): swap hardcoded ahead-count for live git references
The "4 commits ahead" / explicit-HEAD-SHA lines in SESSION_HANDOFF.md
were stale the moment 13ae160 (the prior touch-up commit) landed —
docs that count themselves are a recursion trap. Replaced four sites
with pointers to `git log --oneline origin/master..HEAD` and
`git rev-parse HEAD` so future docs-only edits don't immediately
stale the handoff.

Sites updated:
- "Last updated:" preamble.
- "Status at pause" → HEAD locally + ahead-count bullets.
- "Canonical remote" → push reminder.
- Resume prompt → branch state.

The narrative entries under "Since the v0.20.0 cut" still name SHAs
explicitly because those *are* the per-commit anchors readers grep
against; only the rolling totals were brittle.

Pure docs; no code changes, no test impact.
2026-05-07 22:10:22 -07:00
funman300 13ae16051d docs(handoff): cross-link skipped items + flag the ▌ replay.tsx deviation
Three small clarity touch-ups to SESSION_HANDOFF.md so a future-session
reader doesn't have to reconstruct intent from git log alone:

- The c84d9f4 narrative listed "header text treatment" as still open;
  6204db8 closed it the same session. Added a parenthetical pointer.
- The cacb19c "Skipped" sub-section now cross-links each item to its
  follow-up status in the punch list (scanline + cursor pulse → still
  open; "RUSTY SOLITAIRE" wordmark → closed, the in-engine wordmark
  stays "Solitaire Quest").
- 6204db8 adopted "▌ replay" instead of the mockup's literal
  "▌replay.tsx" — the .tsx was a Stitch/React prototyping leak.
  Documented the deviation alongside the existing RUSTY SOLITAIRE
  precedent so the in-engine string isn't second-guessed later.

Pure docs; no code changes, no test impact.
2026-05-07 22:07:36 -07:00
funman300 a65e5b8c7b docs: refresh handoff for the post-v0.20.0 state
The prior handoff (f2d2119) was written when [Unreleased] was
accumulating v0.20 candidates. v0.20.0 is now cut at 41a009a and
tagged; four post-cut commits sit on top locally — 39b8496
desktop-adaptation spec, cacb19c splash boot-screen port, c84d9f4
replay-overlay scrub bar finish, 6204db8 replay banner ▌ cursor-
block label — none yet pushed. Working tree is clean.

Rewrites the handoff to:

- Distinguish local-master (6204db8) from origin-master (41a009a)
  so the next session doesn't assume git push has happened.
- Document each of the four post-cut commits in its own subsection
  under "Since the v0.20.0 cut" — the cycle is closed; these are
  early entries in whatever cuts next.
- Name docs/ui-mockups/desktop-adaptation.md as the canonical
  geometry reference for future plugin ports — applies to every
  screen including the 8 still-unported missing-plugin surfaces.
- Note the Stitch generate_variants reliability issue
  (timed out on layout-only adaptation prompts) so a future
  session reaches for generate_screen_from_text instead.
- Refresh the SplashFadable scaffolding pattern to the process
  notes (introduced in cacb19c) — the reusable shape for any
  future overlay that fades N >> 3 elements together.
- Refresh the Resume Prompt's A–F options: push / v0.20.1 cut
  decision (A), splash polish (B), replay-overlay enrichments
  beyond the scrub bar (C), card artwork regeneration (D), app
  icon round (E), APK launch verification + JNI bridges (F).

Tests: 1180 passing / 0 failing. Build clippy-clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:02:55 -07:00
funman300 6204db8bb1 feat(engine): port replay banner label to ▌ cursor-block treatment
Aligns the replay overlay's headline with the splash boot-screen idiom
landed in cacb19c — the cursor block (`▌`, U+258C) prefixed to a
lowercased label reads as a Terminal output line rather than a
generic UI title. "Replay" → "▌ replay" and "Replay complete" →
"▌ replay complete" in both the spawn-time path and the per-frame
update_banner_label updater. Doc comments that quote the literal
strings updated in lockstep so the next reader doesn't grep for an
absent literal.

Tests adjusted to match (banner_text assertions in
overlay_spawns_when_playback_starts and overlay_text_changes_on_completed).
The existing 1178 unit tests still pass; clippy clean.

Move-log scroll, MOVE chip, and WIN MOVE callout from the same mockup
remain open — separate commits.
2026-05-07 21:59:10 -07:00
funman300 c84d9f445c feat(engine): scrub fill bar + per-frame updater for replay overlay
Closes the spawn-time half of the replay-overlay redesign open in
SESSION_HANDOFF.md by adding the 1px cyan scrub bar called for in
docs/ui-mockups/replay-overlay-mobile.html. A track in BORDER_SUBTLE
spans the bottom edge of the banner and the cyan ACCENT_PRIMARY fill
mirrors cursor / total via a new ReplayOverlayScrubFill component +
update_scrub_fill system. The pure scrub_pct helper is shared between
the spawn path (initial fill width) and the per-frame updater so the
first paint already reflects state instead of popping 0 → cursor on
the first tick — same shape as the existing format_progress /
update_progress_text split.

Two new tests (1176 → 1178): scrub_pct_covers_state_corners pins the
helper's four corners (Inactive / cursor=0 / midpoint / Completed) and
overlay_scrub_fill_tracks_cursor drives ReplayPlaybackState end-to-end
and asserts Node.width on the unique scrub-fill entity. Same change-
detection guard as the text updaters, so an idle replay leaves the
node untouched.

Header text treatment, move-log scroll, MOVE chip, and WIN MOVE callout
from the same mockup are still open — separate commits.
2026-05-07 21:56:59 -07:00
funman300 cacb19c03f feat(engine): port the splash to the Terminal boot-screen treatment
Implements the full mockup-spec splash from
docs/ui-mockups/splash-mobile.html plus the desktop adaptation rules
from docs/ui-mockups/desktop-adaptation.md. The header (cursor block,
wordmark, divider, "TERMINAL EDITION" subtitle), boot log (three
✓ check rows + "▌ ready_"), progress bar (1px track with full-width
cyan fill + "DONE · 247 ASSETS" caption), and footer
(BASE16-EIGHTIES label, eight palette swatches, version) all land
together. Rules-driven sizing: boot-log column capped at 480 px on
desktop (otherwise 70 % viewport), progress bar capped at 720 px
(otherwise 80 %), per the desktop-adaptation spec.

Refactored the alpha-fade scaffold from per-marker queries
(SplashTitle / SplashSubtitle / SplashCursor) to a single
SplashFadable { base_color: Color } + SplashFadableBg variant.
~15 fadable elements now share one global query each; adding more
elements is one component-attach, not three new query types.

Skipped (each its own potential follow-up):
- Scanline overlay — needs a tiled-pattern asset or a custom
  shader; both are out of scope for a UI-Node port.
- Pulsing cursor on the "ready_" line — would fight the global
  fade timeline; stays static.
- "RUSTY SOLITAIRE" wordmark from the mockup — actual product is
  "Solitaire Quest"; the mockup leaked the repo name.

Tests: 8 carried + 2 new (Terminal boot-screen content present;
fadables start transparent and reach full alpha).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 19:17:05 -07:00
funman300 39b84965b6 docs(ui): add Terminal desktop-adaptation spec
Closes the spec gap flagged after v0.20.0: the 24 mockups in
docs/ui-mockups/ are 23 mobile + 1 desktop, but desktop is still
the primary delivery surface. Stitch's variant generation kept
timing out on layout-only adaptation prompts, so the deterministic
fix is rules-based: a markdown spec that captures (a) the desktop
viewport assumptions, (b) seven universal adaptation rules that
apply to every screen, and (c) per-screen geometry rules for the
priority surfaces (Game Table, Win Summary, Settings, Help, Pause,
Home, Splash, Stats, Profile / Achievements / Theme Picker / Daily
Challenge).

Why rules > visual mockups for this gap:

- Apply uniformly to every screen — including the 9 missing-plugin
  surfaces (splash, challenge, time-attack, weekly-goals, leader-
  board, sync, level-up, replay-overlay, radial-menu) that have
  only mobile mockups today.
- Reference-able from code comments and commit messages without
  loading an image.
- Layout-agnostic by construction: tells the engine "use percent /
  flex / min(720, 50%) widths" instead of pinning a specific
  desktop pixel layout.
- Cheaper than re-running Stitch generation per screen, which is
  flaky for layout-only adaptation work.

Cross-check confirms that v0.20.0's port (modal scaffold, toasts,
table chrome, card chrome, gameplay-feedback, splash cursor) is
already layout-agnostic — the spec gap mattered for *next* ports,
not the work that just shipped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 19:08:58 -07:00
funman300 41a009a693 docs: cut v0.20.0 — Terminal design system + Android persistence
Promotes the [Unreleased] section to [0.20.0] dated 2026-05-07
and opens a fresh empty [Unreleased]. The cycle's two through-
lines:

- **Terminal visual-identity port.** ui_theme token system
  (0d477ac) is load-bearing; downstream chrome migrations cover
  the modal scaffold, gameplay-feedback layer (ceec4fc), toasts
  with a new ToastVariant enum (a137607), table chrome (651f406),
  card chrome (d752870), splash cursor (cdcadda), and final
  hint-source / dest pairing (9891ae4). Card-face / suit / card-
  back palette intentionally NOT migrated — those track PNG
  artwork that hasn't been regenerated yet. The 24 Stitch-rendered
  mockups and design-system.md spec landed in fa7f98a.
- **Android persistence shim.** solitaire_data::data_dir
  routes through a per-platform shim (4b51e50) closing the
  CLAUDE.md §10 dirs::data_dir() = None pitfall on Android.
  Settings, stats, achievements, replays, game-state, time-attack
  sessions, and user themes now persist on a real APK.

Also closes three v0.19.0 punch-list candidates that landed
earlier in the cycle (pull_failure flake at 67c150b, smart-window-
size opt-out at e1b8766, Shareable badge at 9b065e5).

Tests: 1176 passing / 0 failing (six new this cycle: ui_theme
invariant guards, toast-variant-border-mapping, palette-tracking
guards on MARKER_VALID / HINT_PILE_HIGHLIGHT_COLOUR /
RIGHT_CLICK_HIGHLIGHT_COLOUR / toast-border distinctness).

SESSION_HANDOFF.md refreshed: HEAD pointer, test count, the
v0.20.0 changelog summary, the open punch list (Phase Android
runtime gaps, visual-identity follow-ups including the artwork
regeneration item), the updated design-direction box (was
Midnight Purple + Balatro yellow; now base16-eighties Terminal),
and a refreshed Resume Prompt offering A–F next-step options.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.20.0
2026-05-07 18:58:51 -07:00
funman300 fa7f98ac52 docs(ui): land the Terminal design system + 24-mockup library
Adds the spec the recent visual-identity port pass referenced:

- design-system.md — base16-eighties palette, type scale, spacing
  scale, motion budget, component library, accessibility notes
  (color-blind toggle, high-contrast mode, glyph differentiation),
  and the canonical "Terminal" card-back theme.
- 24 Stitch-rendered mockups (HTML + PNG): 12 redesigned existing
  screens, 1 desktop home variant, 2 onboarding steps, and 9
  missing-plugin screens (splash, challenge, time-attack,
  weekly-goals, leaderboard, sync, level-up, replay, radial-menu).

These mockups are the source the engine plugins were ported
against in commits 0d477ac through 9891ae4 (token system,
modal scaffold, gameplay-feedback layer, toasts, table chrome,
card chrome, splash cursor, hint highlight). Future plugin work
should diff against the matching mockup before touching pixels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:47:57 -07:00
funman300 9891ae4ba3 refactor(engine): final hint-highlight + replay-overlay token cleanup
- input_plugin's hint-source card tint moves from raw bright-yellow
  `srgba(1.0, 1.0, 0.4, 1.0)` to the design-system STATE_WARNING
  token, so the source card and the destination pile (which already
  uses STATE_WARNING via HINT_PILE_HIGHLIGHT_COLOUR) wear the same
  attention colour as a coherent pair.
- replay_overlay had two stale doc comments referencing the old
  "loud yellow accent" — Primary is now cyan (ACCENT_PRIMARY).
  Comments updated; no behaviour change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:45:02 -07:00
funman300 cdcaddaabe feat(engine): add Terminal cursor block to splash overlay
Splash now renders the design system's signature `▌` cyan terminal-
cursor glyph (96px) above the wordmark, matching docs/ui-mockups/
splash-mobile.html. The cursor uses ACCENT_PRIMARY and fades on the
same per-frame alpha schedule as the title and subtitle so the
brand beat still dissolves as a single layer.

Did NOT pull in the mockup's full boot-loader treatment (scanline
overlay, ✓ check log lines, progress bar, ROOT@SOLITAIRE prompt) —
those are aesthetic features that warrant their own commit, not
this token-port pass. The splash already consumed every relevant
ui_theme token; the cursor glyph is the single highest-signal
visual element the spec called for.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:42:29 -07:00
funman300 d752870007 refactor(engine): migrate card_plugin chrome to Terminal tokens
- Drag-elevation shadow now sources its colour from CARD_SHADOW_COLOR
  + CARD_SHADOW_ALPHA_DRAG, so the Terminal "no box-shadow" policy
  disables the stack shadow in lockstep with the per-card shadows.
  Re-enabling shadows for a future palette swap is now a one-line
  edit in ui_theme, not a hunt across plugins.
- RIGHT_CLICK_HIGHLIGHT_COLOUR retuned from raw `srgba(0.2, 0.8, 0.2, 0.6)`
  to STATE_SUCCESS's RGB at 60% alpha. Spelled as a literal because
  Alpha::with_alpha isn't const on stable; a new test pins the RGB
  to STATE_SUCCESS so a palette swap can't drift the two apart.
- Drop the duplicated PILE_MARKER_DEFAULT_COLOUR const — import the
  promoted const from table_plugin instead. STOCK_NORMAL_COLOUR is
  now an alias of that const so all idle pile-marker tints track a
  single source of truth.
- Stock recycle "↺" text changed from raw `srgba(1.0, 1.0, 1.0, 0.7)`
  to TEXT_PRIMARY at 0.7 alpha, picking up the off-white foreground
  used elsewhere in the Terminal UI.

Card-face / suit / card-back palette constants are intentionally
NOT migrated: the runtime path renders PNG artwork that's still on
the previous "white card" palette, so swapping the fallback
constants ahead of artwork regeneration would mix two visual
systems for any code path where image loading fails.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:39:02 -07:00
funman300 1d1543e4bc test(engine): align card-shadow drag-vs-idle assertion with Terminal "no shadow" intent
Commit 0d477ac (the Terminal token system) pinned both
CARD_SHADOW_ALPHA_IDLE and CARD_SHADOW_ALPHA_DRAG to 0.0 because the
Terminal design system explicitly disallows box-shadow ("depth via
1px borders and tonal layering"). The existing invariant
\`drag_alpha > idle_alpha\` then fails — \`0.0 > 0.0\` is false.

Loosen the assertion to \`drag_alpha >= idle_alpha\` and document the
intent: under Terminal both are 0; under any future palette that
re-enables shadows, drag still must not be weaker than idle. The
useful regression-guard (catching an accidental swap of the two
constants) is preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:33:34 -07:00
funman300 651f4060e6 refactor(engine): migrate table_plugin chrome to Terminal tokens
- Promote `marker_colour` to module-level const PILE_MARKER_DEFAULT_COLOUR
  and re-export it. cursor_plugin::MARKER_DEFAULT now imports the const
  directly, replacing the prior duplicated literal kept in sync only by
  doc comment. Drift becomes a compile error instead of a stale claim.
- Empty-tableau "K" placeholder text now uses TEXT_PRIMARY at 0.35 alpha
  (was raw `Color::srgba(1.0, 1.0, 1.0, 0.35)`) so it picks up the
  Terminal off-white foreground.
- HINT_PILE_HIGHLIGHT_COLOUR retuned from bright `srgb(1.0, 0.85, 0.1)`
  to the design-system STATE_WARNING token (`#ddb26f`). Spelled as a
  literal because Alpha::with_alpha is not yet const on stable; a new
  test pins the RGB to STATE_WARNING so a palette swap can't drift the
  two apart silently.
- The existing "is gold" character test was hardcoded to the old bright
  palette (red ≥ 0.9). Loosened to "warmer than cool" + ranges that the
  Terminal muted gold satisfies, with exact-RGB tracking handled by the
  new STATE_WARNING test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:32:03 -07:00
funman300 a1376075bd feat(engine): port toasts to the Terminal design-system spec
Toasts now follow `docs/ui-mockups/design-system.md`:
- Bottom-anchored absolute position (was top / mid-screen)
- Opaque BG_ELEVATED fill (was translucent black-at-alpha)
- 1px accent border keyed off a new ToastVariant enum
- TYPE_BODY_LG caption (was 22 / 32 px literals)
- RADIUS_MD corners

ToastVariant exposes Info / Warning / Error / Celebration, each
mapped to its design-system token via border_color(). Variants are
threaded through every spawn_toast call site:

- Achievement / Level-up / XP / Daily / Weekly / Challenge → Celebration
- Goal-announcement / Time-attack / Settings volume / Auto-complete → Info

Queued banner and fire-and-forget toasts use slightly different
bottom anchors (6% vs. 14%) so a celebration toast spawned in the
same frame as a queued info banner layers above it instead of
overlapping. Two new tests pin variant→border mapping to the
design tokens and require all four borders to be visually distinct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:26:55 -07:00
funman300 ceec4fc486 refactor(engine): route gameplay-feedback colours through Terminal tokens
Selection-highlight tints in selection_plugin and the valid-drop
marker tint in cursor_plugin were hand-tuned RGB literals from the
prior Premium-Solitaire palette. Migrate them to the semantic
state tokens introduced in ui_theme:

- keyboard-drag source highlight (picking)  → ACCENT_PRIMARY
- keyboard-drag source highlight (lifted)   → STATE_WARNING
- keyboard-drag destination highlight       → STATE_SUCCESS
- cursor_plugin::MARKER_VALID               → STATE_SUCCESS @ 0.55α

`MARKER_VALID` stays a Color literal (Alpha::with_alpha is not yet
const on stable); a new tracking test pins its RGB to STATE_SUCCESS
so a future palette swap can't drift the two apart silently.

Also fix three stale doc comments in ui_modal that still described
the previous yellow / magenta palette ("Loud yellow CTA",
"Primary swaps to the magenta secondary accent"). Cyan and lavender
now, matching the actual token values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:06:57 -07:00
funman300 0d477ac9fd feat(engine): Terminal design-token system in ui_theme
Replaces the prior Premium-Solitaire palette and ad-hoc constants
with the full Terminal (base16-eighties) token set: near-black
surface ramp, cyan primary CTA, lime/lavender/gold/teal/pink
semantic accents, 5-rung type scale, 7-rung 4-multiple spacing
scale, 3-step radius, 14-rung z-index hierarchy, and a complete
motion budget. Card drop-shadow alphas pinned to 0 — Terminal
depth is 1px borders + tonal layering, not box-shadow.

Tokens stay as `pub const` so static contexts (default Sprite
colours etc.) keep compiling; a future UiTheme resource can layer
runtime switching on top without breaking the constant API. Four
unit tests pin the spacing/type/z-index invariants so a careless
edit can't silently break the scale. Plugin-by-plugin migration
to consume these tokens follows in subsequent commits.

Spec: docs/ui-mockups/design-system.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:56:08 -07:00
funman300 4b51e50203 fix(data): route data_dir() through a per-platform shim so Android persists
dirs::data_dir() returns None on Android, which silently disabled
every persistence path (settings, stats, achievements, replays,
game-state, time-attack sessions, user themes). New
solitaire_data::platform::data_dir() shim falls through to
dirs::data_dir() on desktop and returns the per-app sandbox at
/data/data/com.solitairequest.app/files on Android — no JNI needed,
since the package id is pinned in
[package.metadata.android].

CLAUDE.md §10 already flagged this as a known pitfall; the shim
pays it down at the one chokepoint instead of per feature.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:55:49 -07:00
funman300 f2d2119db5 docs: refresh handoff + populate CHANGELOG [Unreleased] for v0.20
The v0.19.0 handoff had drifted material across seven commits:
HEAD pointer was wrong (still claimed 6037596; actually 59424a3),
"Tags on origin" still claimed v0.19.0 wasn't pushed, the
known-flake list still mentioned `pull_failure_sets_error_status`
(fixed in 67c150b), and three of four v0.19.0 punch-list
"candidates" had silently shipped without the doc tracking it.
The Android build target landing in fb8b2ac wasn't mentioned at
all despite being the largest single change in the cycle.

CHANGELOG [Unreleased] populated with all seven commits grouped
into Added / Fixed:

  Added:
    - Android build target — first working APK (fb8b2ac)
    - Android developer setup + build runbook (59424a3)
    - F3 FPS / frame-time overlay (690e1d2)
    - "Smart window size" Settings toggle (e1b8766)
    - "Shareable" badge on Latest-win caption (9b065e5)
    - Help: M / P / Win-Summary-Enter rows (35516d3)

  Fixed:
    - pull_failure_sets_error_status flake (67c150b)

SESSION_HANDOFF.md fully rewritten:
  - Status section reflects HEAD 59424a3, clean working tree (apart
    from this commit's docs), 1170 passing tests, no known flakes
  - "Where we are" tracks v0.19.0 candidates' close status (3 of 4
    shipped, App icon still open and now blocked on a re-export)
  - New v0.20 candidates table covers all seven commits
  - New "Phase Android" punch-list section captures the unblocked-
    by-fb8b2ac work: APK launch verification, dirs::data_dir port,
    JNI ClipboardManager, Android Keystore, gpgs integration, the
    cosmetic cargo-apk panic workaround
  - Process notes call out the async-test starvation pattern
    (seen twice now), the bin→lib+shim refactor as a reusable
    pattern, and target-gating-by-default for cross-platform deps
  - Resume prompt's A–D menu refreshed to reflect actually-open
    work: APK verification, Phase-Android persistence, app icon,
    and a v0.20 cut

Build: cargo clippy --workspace --all-targets -- -D warnings clean.
Tests: 1170 passing / 0 failing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 13:28:04 -07:00
funman300 59424a370c docs(android): developer setup + build runbook
Captures the toolchain install for Debian 13 (the path Quat ran on
this dev box, including the JDK 21 / unzip / SDK-licence prompts),
the `cargo apk build` invocation, the cosmetic post-pass panic
workaround, and the table of what's wired vs. stubbed for the
android target. Runnable on a fresh box from a clone — no
machine-local context required.

Pairs with the workspace cfg-gating in fb8b2ac. Future Phase-Android
work (dirs::data_dir port, JNI ClipboardManager, Android Keystore,
gpgs) is listed as the not-yet-done section so a contributor can
pick it up without re-deriving the punch list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:36:36 +00:00