Compare commits

..

45 Commits

Author SHA1 Message Date
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>
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>
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
funman300 fb8b2ac684 feat(app): Android build target — first working APK at 54 MB
Wires the workspace through `cargo apk build`. After this commit
`cargo apk build -p solitaire_app --target x86_64-linux-android`
produces a debug-signed APK at `target/debug/apk/solitaire-quest.apk`
containing all assets and `lib/x86_64/libsolitaire_app.so` — runnable
on the AVD or a physical x86_64 device.

The five gating points discovered by iterating compile cycles:

1. solitaire_app split into bin + lib. cargo-apk needs a `cdylib`
   to bundle as `libmain.so`; pure-bin crates panic with
   "Bin is not compatible with Cdylib". `src/lib.rs` carries the
   ECS bootstrap as `pub fn run`; `src/main.rs` is a 3-line shim
   that delegates for the desktop `cargo run` path.

2. `[package.metadata.android]` pins target SDK 34 / min SDK 26
   so cargo-apk doesn't probe for whatever default it ships
   (which on this machine was an uninstalled API 30). `assets =
   "../assets"` lets the same asset directory feed both desktop
   and APK.

3. Workspace `bevy` features add `android-native-activity` (the
   Bevy-side glue that pairs with cargo-apk's NativeActivity
   wrapper). The feature is target-gated inside bevy_internal so
   desktop builds compile it out.

4. `arboard` (clipboard, used by Stats's "Copy share link") has
   no Android backend — `cargo apk build` fails with E0433 on
   `platform::Clipboard` if unconditional. Target-gated to
   `cfg(not(target_os = "android"))`; the system surfaces an
   informational toast on Android until JNI ClipboardManager is
   wired in the Phase-Android round.

5. `keyring` + `keyring-core` cannot compile for android — the
   transitive `rpassword` uses `libc::__errno_location` which
   bionic doesn't expose. Both crates target-gated; `auth_tokens`
   ships a stub on Android that returns `KeychainUnavailable` for
   every call, matching how callers already handle a Linux box
   without Secret Service.

Cosmetic post-pass panic: cargo-apk panics AFTER the APK is signed
when it tries to also wrap the bin target. The APK on disk is
unaffected. Working around this with `cargo apk build --lib` is
the next small step.

What's verified:
- Desktop `cargo build`, `cargo clippy --workspace --all-targets`,
  and `cargo test --workspace` all clean.
- `cargo apk build -p solitaire_app --target x86_64-linux-android`
  produces 54 MB debug APK with libsolitaire_app.so + assets.

What's NOT yet verified:
- Whether the APK actually launches on the AVD / a phone (next
  step: `adb install` + `adb logcat` against the bevy_test AVD).
- Whether `dirs::data_dir()` on Android returns a usable path
  (sync / persistence will surface this if not).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:34:48 +00:00
funman300 690e1d2ad6 feat(engine): F3-toggleable FPS / frame-time overlay
Performance work for the upcoming Android port needs a numeric
baseline we can quote across desktop and mobile, instead of "feels
slow". DiagnosticsHudPlugin wraps Bevy's FrameTimeDiagnosticsPlugin
and renders a tiny corner readout the developer can toggle with F3.

- Hidden by default — production builds ship the plugin but the
  overlay starts invisible.
- F3 reads ButtonInput<KeyCode> directly (not gated by pause /
  modal state); diagnostics should always be reachable.
- Reads `smoothed()` FPS + frame_time so the cell isn't a jittery
  per-frame scoreboard. Format: "FPS NN \u{2022} M.MM ms".
- Anchored top-right at z = Z_SPLASH + 100 so the readout sits
  above every modal / toast / splash layer.
- Update system bails when hidden so we don't pay the
  diagnostic-store lookup or text mutation when nobody's looking.

Next up on the perf track: get the Android build target wired so we
can put real numbers in this readout from a phone or emulator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:03:18 +00:00
funman300 35516d31f6 docs(help): add M / P / Win-Summary-Enter to the Overlays section
The Help (F1) modal's Overlays section listed S/A/L/O but skipped
two post-v0.18 entries — M (Home / Mode launcher) and P (Profile) —
and never mentioned the recently-shipped Enter accelerator that
dismisses the Win Summary.

Help is the canonical keyboard-discovery surface. Three new rows
cover the gap so a player who opens F1 sees every overlay-toggle
key, plus the contextual Enter shortcut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:40:44 +00:00
funman300 9b065e5ac6 feat(stats): append "Shareable" badge to the Latest-win caption
The Copy share link button on the Stats overlay only produces a URL
when the displayed replay has a `share_url` populated; otherwise it
surfaces a toast explaining the upload prerequisite. Players had no
way to know the button would work without clicking it.

Adds a "\u{2022} Shareable" suffix to the Latest-win caption when
the displayed replay carries a share_url, matching the format the
v0.19.0 handoff sketched ("Replay 3 / 8 \u{2022} Shareable") for
the future Prev/Next selector. The Prev/Next markers exist in
stats_plugin but no spawn site renders them today, so the live
fix is on the existing single-replay caption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 04:04:55 +00:00
funman300 e1b8766e15 feat(settings): "Smart window size" toggle to opt out of monitor-relative
launch sizing

Players who specifically prefer the literal 1280×800 baseline on
every fresh-install launch had no way to opt out of the v0.19.0
smart-default sizer. Adds a Gameplay-section toggle (mirrors the
"Winnable deals only" pattern) so they can flip it off.

- New `Settings::disable_smart_default_size: bool` field with
  `#[serde(default)]` so legacy `settings.json` files load to the
  shipped behaviour (smart sizer enabled).
- Settings panel gains a "Smart window size" row with ON/OFF label
  inverting the negative flag, and a tooltip clarifying that saved
  window geometry always wins over both branches.
- `solitaire_app::main` reads the flag once at startup and skips
  the `apply_smart_default_window_size` registration when it's set.
  Mid-session changes apply on next launch (documented on the
  field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 04:00:43 +00:00
funman300 67c150bd7b test(engine): wall-clock-bounded loop for pull_failure flake
The fixed 5-update budget in `pull_failure_sets_error_status` was
the last test still subject to the AsyncComputeTaskPool starvation
mode that v0.19.0's auto-save fix already cleared. Under heavy
parallel cargo-test load, 5 updates wasn't always enough for the
failing pull task to surface its Err and flip
SyncStatusResource to Error.

Pumps updates in a loop bounded by a 5-second deadline (with
std::thread::yield_now between iterations to give the task pool a
chance to run), exiting as soon as the status flips. Mirrors the
auto-save flake fix shape — a healthy run hits the assertion in a
handful of frames, while a starved run gets the budget it needs
without hanging the suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 03:54:51 +00:00
207 changed files with 12771 additions and 15630 deletions
+499 -1
View File
@@ -6,7 +6,505 @@ project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
_Nothing yet._
No threads in flight. v0.21.0 cut on 2026-05-08; CHANGELOG accumulates
the next cycle here.
## [0.21.0] — 2026-05-08
Closes the visual-identity arc opened in v0.20.0. Three through-lines
landed: the **card-face / suit / card-back artwork migration** that
v0.20.0 deliberately deferred, the **splash boot-screen + replay-
overlay polish** that closes Resume-prompt Options B and C, and a
late-cycle **`ACCENT_PRIMARY` palette swap** from cyan `#6fc2ef` to
brick red `#a54242` after a quick stakeholder review on the
shipped art.
The card-face arc is the largest piece by commit count (10 of the
25 post-tag commits) and shape: it ports both rendering paths
production traverses — the PNG fallback at `assets/cards/*.png`
and the bundled-default theme SVGs at
`solitaire_engine/assets/themes/default/*.svg` that
`include_bytes!()`-embed into the binary and override the PNGs at
runtime — to identical Terminal-aesthetic art generated by the
same `face_svg` / `back_svg` builders. A new
`card_face_svg_pin` integration test pins rasteriser output via
FNV-1a on raw RGBA bytes, so future `usvg`/`resvg` upgrades or
intentional builder edits surface as test failures rather than
silent visual drift. The pin test fired three times during the
arc (text→path glyph fix, glyph orientation tweak, palette swap)
and rebaselined cleanly each time via the empty-then-paste
bootstrap pattern baked into the test.
Three sign-off follow-ups surfaced once a human booted the
running game and they all matched the same shape — "fallback
path the chrome migration walked past": the embedded default
theme overrode the new PNGs at runtime, the table backgrounds
were a separate PNG path that 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
three are recorded under "Fixed" below.
Phase 8 (sync) and the Phase Android runtime gaps (JNI bridges,
APK launch verification on device) remain open and roll forward.
### Added
- **Card-face SVG generator pipeline** (`5623368` plan doc,
`3a4bb63` PoC, `babe5cc` full generator, `48b28d2` pin test).
`solitaire_engine/examples/card_face_generator.rs` writes 52
face PNGs + 5 back PNGs into `assets/cards/` and 53 theme SVGs
into `solitaire_engine/assets/themes/default/`, all from the
shared `face_svg` / `back_svg` builders in the new
`solitaire_engine::assets::card_face_svg` module. Run with
`cargo run --example card_face_generator --release`. The PoC
(`card_face_poc.rs`) stays alongside as historical record of
the per-card grain proof. Pin test `card_face_svg_pin`
guards rasterised output via inline FNV-1a so the arc has
test-time coverage of both intentional builder edits (rebase
via empty-then-paste) and unintentional dependency-upgrade
drift.
- **Background generator example** (in `8719f77`).
`solitaire_engine/examples/background_generator.rs` emits 5
flat Terminal-palette play-surface PNGs at 120 × 168, the
same tile size the legacy felt textures used (the runtime
stretches to `window_size * 2.0` so source resolution is
immaterial). All 5 slots stay in the near-black family —
`#151515` canonical, `#0a0a0a` deeper, `#1a1a1a` elevated,
`#121820` cool tint, `#201812` warm tint.
- **Splash boot-screen port** (`cacb19c`). Full mockup-spec
splash: header, boot log, progress bar, palette swatches,
version footer, plus the `SplashFadable` scaffold that lets
any future overlay fade `N >> 3` elements via one marker +
one global lerp query (replaces the `Without<X>, Without<Y>`
exclusion pattern that the legacy splash hit at three
siblings).
- **Splash trailing cursor pulse** (`29136d8`). Trailing
6×12 px Node, sine-pulsed, multiplied with the global splash
fade — the "multiply, don't override" pattern that resolves
the original `cacb19c` skip-rationale. Closes Option B half 1
from the SESSION_HANDOFF Resume prompt.
- **Splash tiled scanline overlay** (`a27cf5a`). Runtime-
generated 2×2 RGBA8 texture tiled via `NodeImageMode::Tiled`;
per-pixel alpha × tint alpha gives multiplicative fade
integration without new abstractions. Closes Option B
half 2.
- **Replay overlay scrub bar** (`c84d9f4`). 1px accent fill at
the bottom of the banner, mirroring `cursor / total`. Per-
frame updater + scrub-pct unit tests.
- **Replay overlay banner label port** (`6204db8`). The
"▌ replay" headline picks up the cursor-block treatment that
aligns it with the splash boot-screen idiom.
- **Replay overlay GAME caption** (`54005d5`). `GAME #YYYY-DDD`
game-identifier caption beneath the headline. Mirrors the
mockup's right-anchored ID but stays grouped with the headline
so the two pieces of "this is a replay of game X" read as one
unit.
- **Replay overlay MOVE chip** (`e080b49`). `MOVE N/M` progress
readout wrapped in a 1px accent-bordered chip — discrete
callout rather than free-floating text. Closes Option C from
the SESSION_HANDOFF Resume prompt (paired with `54005d5`).
- **Terminal desktop-adaptation spec** (`39b8496`).
`docs/ui-mockups/desktop-adaptation.md` — the rules-based
companion to the 24-mockup library. Closes the spec gap
exposed when 23 of 24 mockups turned out to be mobile-only;
any future plugin port should read this first and apply the
universal rules before consulting the per-screen table.
- **`solitaire_engine::assets::card_face_svg` module**
(`48b28d2`). Public SVG builders (`face_svg`, `back_svg`,
`suit_path_d`) extracted from the example so the pin test
could call them — examples can't be referenced from
`tests/`. The generator and the test now share the same
source-of-truth, so the pin guards both rendering paths
the engine consults.
### Changed
- **`ACCENT_PRIMARY` swapped from cyan `#6fc2ef` to brick red
`#a54242`** (`a292a7e`). Project-wide palette decision after
initial rollout. Affects every cyan-accented surface — 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. `RED_SUIT_COLOUR_CBM`
swapped in lockstep from cyan to lime `#acc267` so the
colour-blind alternative stays hue-distinct from the new
red-family primary. Comment doc strings throughout the
engine retuned from "cyan" to "accent" / "primary-accent" so
future palette changes don't require comment churn. Spec doc
`design-system.md` updated in lockstep with historical
references preserved as audit trail.
- **Card-face / suit / card-back constants migrated to Terminal
palette in lockstep with new artwork** (`e8bf9d7`). Five
constants flipped: `CARD_FACE_COLOUR``#1a1a1a` (was
off-white `#fafaf2`), `RED_SUIT_COLOUR``#fb9fb1` (was deep
red `#c71f26`), `BLACK_SUIT_COLOUR``#d0d0d0` (was near-
black `#141414`), `CARD_FACE_COLOUR_RED_CBM` renamed to
`RED_SUIT_COLOUR_CBM` and repurposed from a face-background
tint to a suit-glyph swap (the Terminal face is uniformly
`CARD_FACE_COLOUR` regardless of CBM; CBM only swaps red
suits to a hue-distinct alternative in the glyph itself).
`card_back_colour()` retuned to the 5 base16-eighties accent
colours matching `BACK_ACCENTS`. `face_colour()` deleted —
the function collapsed to a constant once the Terminal face
became uniform. `text_colour()` gained a `color_blind: bool`
parameter to surface the CBM swap on the constant-fallback
path (the production path bakes glyphs into the PNG, but
tests under `MinimalPlugins` still need the CBM-aware
fallback). Four `face_colour` CBM tests collapsed into two
`text_colour` CBM tests in the same commit.
- **Default-theme SVG art regenerated to Terminal aesthetic**
(`a14200a`). `solitaire_engine/assets/themes/default/*.svg`
— the bundled-default theme that
`include_bytes!()`-embeds into the binary — was still the
legacy vector-playing-cards art post-`e8bf9d7`. The PNG
migration alone didn't change what production rendered
because `apply_theme_to_card_image_set` overrides
`CardImageSet.faces[..]` at startup with the theme's
rasterised SVG handles. Both rendering paths now agree:
same `face_svg` / `back_svg` builders feed both paths, and
the pin test guards both.
- **Card glyphs render upright in both corners** (`dd101b3`).
The traditional 180° inverted-corner-indicator rotation on
the bottom-right glyph was dropped at user preference —
single-orientation digital play doesn't benefit from the
flipped-readback convention. Both glyphs now render in the
same upright orientation. `design-system.md` § Game Cards
line 220 updated in lockstep — the deviation from
traditional playing-card layout is documented in the spec,
not just the code.
- **Action-button row typography aligned to `TYPE_BODY`**
(`ae84dc1`). Was a hardcoded `font_size: 16.0` literal that
the v0.20.0 typography-migration audit walked past. Brings
it in line with the `TYPE_*` token system every other text
element in `hud_plugin` already routes through, and trims
~12% off label widths so the action-button row no longer
collides with the left-anchored HUD column at portrait /
narrow window widths. Pairs with a horizontal-padding step-
down from `VAL_SPACE_3` to `VAL_SPACE_2`: ~96 px reclaimed
across the 6-button row.
- **Table backgrounds flattened to solid Terminal colours**
(`8719f77`). Replaces the legacy felt-texture PNGs at
`assets/backgrounds/bg_*.png` with 5 flat near-black
variants per design-system.md (Terminal play surface is
flat; no felt, no gradient). On-disk tile weight drops
from ~16 KB average to ~100 bytes per tile; runtime
appearance flips from green felt to flat `#151515`.
### Fixed
- **Card suit glyphs rendered as near-invisible "tofu" marks**
(`af414b6`). The bundled `FiraMono` in
`svg_loader::shared_fontdb` doesn't carry usable U+2660-2666
glyphs at the requested size — usvg silently substituted a
default-size fallback regardless of `font-size="20"` /
`font-size="64"`. Switched suit-glyph rendering from `<text>`
elements to inline SVG `<path>` elements via a new
`suit_path_d` helper authoring each suit as a single closed
perimeter in a 32×32 logical box. 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).
- **Default-theme SVGs were overriding new PNG artwork at
runtime** (`a14200a`). The PNG migration in `e8bf9d7` looked
correct under `cargo test` (the constant-fallback path
matched) but a real `cargo run` showed legacy white cards
because `theme::plugin::apply_theme_to_card_image_set`
overlays the bundled-default theme's rasterised SVGs onto
`CardImageSet.faces[..]` at startup, and those SVGs were
still legacy. Fixed by regenerating both rendering paths
from the same `face_svg` / `back_svg` builders. The
migration plan flagged "Theme system — out of scope here";
that was a planning miss documented in the SESSION_HANDOFF.
- **Top-bar HUD column collided with action-button row at
portrait window widths** (`ae84dc1`). Both nodes were
absolute-positioned siblings at `top: VAL_SPACE_2` without a
shared flex parent, so they could overlap horizontally when
the window narrowed past their combined natural widths.
Fixed via the typography + padding tightening described
under "Changed" — minimal-blast-radius fix; the structural
fix (shared `JustifyContent::SpaceBetween` parent) stays
open as a follow-up if narrower windows surface.
- **Table-surface fill was still legacy green felt despite
v0.20.0's chrome-migration claim** (`8719f77`). Commit
`651f406` retuned in-engine constants but the runtime path
loads from `assets/backgrounds/bg_0.png`, an on-disk PNG that
the migration didn't touch. Same shape as the default-theme
override above — token migration walked past a fallback
rendering path. Fixed by regenerating the 5 background PNGs.
### Stats
- **1184 passing tests / 0 failing** across the workspace
(net +8 from v0.20.0's 1176 baseline). New tests this cycle:
the scrub-bar pair (`scrub_pct_covers_state_corners`,
`overlay_scrub_fill_tracks_cursor`); the splash boot-screen
pair (`splash_renders_terminal_boot_screen_content`,
`fadables_start_transparent_and_reach_full_alpha`); the
splash-polish pair (`build_scanline_image_has_expected_2x2_rgba_bytes`,
`scanline_overlay_spawns_and_fades_with_splash`); the
card-face pin (one integration test in
`card_face_svg_pin.rs` that exercises 57 rasteriser outputs
through 57 hash comparisons in a single
`#[test]`-marked function); and the CBM consolidation that
rewrote four `face_colour` tests as two `text_colour` CBM
tests in the same commit (net 0 to count, clean rewrite).
- Zero clippy warnings under `cargo clippy --workspace
--all-targets -- -D warnings`.
- `cargo test --workspace` clean.
### Documentation
- `docs/ui-mockups/card-face-migration.md` (`5623368`) — the
multi-session lockstep migration plan that the card-face arc
followed step-by-step. Now reads as historical record of
closed work; lessons documented under "Process notes" in
SESSION_HANDOFF.md.
- `docs/ui-mockups/desktop-adaptation.md` (`39b8496`) — rules-
based companion to the 24-mockup library. Required reading
before any future plugin port.
- `docs/ui-mockups/design-system.md` updates: § Game Cards
line 220 (glyph orientation), CTA / suit-red-cb / Card-back
badge / Primary button / Bottom-bar active-icon palette
retunes for the cyan→red swap. Historical references
preserved as audit trail.
- Multiple `SESSION_HANDOFF.md` refreshes (`a65e5b8`,
`13ae160`, `44f5972`, `73ac67d`, `ef54cde`, `d109c32`)
recording Options B / C / D closures and process notes.
## [0.20.0] — 2026-05-07
Two through-lines closed: a full **Android port** (build target,
first 54 MB APK, JNI-free per-app persistence shim) and the
**Terminal visual-identity port** that replaces the prior
Premium-Solitaire palette across every UI surface. The Android
arc opened in `fb8b2ac` (compile + APK), continued in `4b51e50`
(`solitaire_data::data_dir` shim closing the CLAUDE.md §10
`dirs::data_dir() = None` pitfall), and is functional end-to-end
on a real device — though the runtime artwork is still the legacy
white-card palette, and JNI ClipboardManager / keyring bridges
remain stubbed (matching v0.19.0's documented fallback behaviour).
The Terminal port lands as a top-down stack: the `ui_theme` token
API in `0d477ac` is load-bearing, and the rest of the cycle is
downstream applications (modal scaffold, gameplay-feedback,
toasts, table / card chrome, splash cursor, hint-highlight
pairing). The card faces and suit-pip palette are deliberately
NOT migrated — those track PNG artwork that hasn't been
regenerated yet, and swapping the fallback constants ahead of the
artwork would mix two visual systems on any code path where
image loading fails.
The 24 Stitch-rendered mockups in `docs/ui-mockups/` are now
in-tree (`fa7f98a`); future plugin work should diff against the
matching mockup before touching pixels.
Two threads from v0.19.0's punch list also closed in this cycle:
the pull-failure test flake (`67c150b`), the Settings opt-out for
the smart-default window sizer (`e1b8766`), and the share-link
discoverability surfacing (`9b065e5`). The remaining v0.19.0
candidate — the app-icon round — stays open.
### Added
- **`ui_theme` Terminal design-token system** (`0d477ac`). Single
source of truth for the engine's visual identity:
base16-eighties palette (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, full
motion budget, and four invariant-pinning unit tests. Every
downstream port commit in this cycle reads from this module —
swapping the palette is now a one-file edit, not a hunt across
~50 plugin files. Card-shadow alphas pinned to 0 (Terminal
achieves depth via 1px borders + tonal layering, no
`box-shadow`); the rendering path is left intact so a future
palette can re-enable shadows without touching consumers.
- **`ToastVariant` enum + Terminal toast styling** (`a137607`).
Toasts now follow `docs/ui-mockups/design-system.md`: opaque
`BG_ELEVATED` fill, 1px accent border keyed off
`Info` / `Warning` / `Error` / `Celebration` variants, 18px
monospaced caption (`TYPE_BODY_LG`), bottom-anchored. All ten
call sites pass their semantic variant: achievement / level-up
/ XP / daily / weekly / challenge → Celebration (lavender);
goal-announcement / time-attack / settings volume / auto-complete
→ Info (teal). Two regression tests pin variant→border mapping
to the design tokens and require all four borders to be visually
distinct. Queued and immediate toasts use slightly different
bottom anchors (6 % vs. 14 %) so a celebration toast spawned
alongside a queued info banner layers above it.
- **Terminal cursor block on the splash overlay** (`cdcadda`).
The launch splash now renders the design system's signature
`` cyan (`ACCENT_PRIMARY`) glyph (96 px, hand-tuned literal)
above the wordmark, matching `docs/ui-mockups/splash-mobile.html`.
Cursor 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, progress bar, ROOT@SOLITAIRE
prompt) — those are aesthetic features warranting their own
commit.
- **Terminal design-system spec + 24-mockup library** (`fa7f98a`).
`docs/ui-mockups/design-system.md` (palette, type scale, spacing
scale, motion budget, component library, accessibility notes —
color-blind toggle, high-contrast mode, glyph differentiation,
canonical `"Terminal"` card-back theme) and 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). The spec the rest of this
cycle ports against; future plugin work diffs here before
touching pixels.
- **Android build target — first working APK** (`fb8b2ac`).
`cargo apk build -p solitaire_app --target x86_64-linux-android`
now produces a 54 MB debug-signed APK at
`target/debug/apk/solitaire-quest.apk`. Five gating points
resolved end-to-end:
- **`solitaire_app` split into bin + lib.** cargo-apk needs a
`cdylib` to bundle as `libmain.so`; pure-bin crates panic
with "Bin is not compatible with Cdylib". `src/lib.rs`
carries the ECS bootstrap as `pub fn run`; `src/main.rs` is
a 3-line shim that delegates for the desktop path.
- **`[package.metadata.android]`** pins target SDK 34 / min
SDK 26 and points `assets = "../assets"` at the workspace
asset directory so desktop and APK share one set.
- **Workspace `bevy` features** add `android-native-activity`
(target-gated inside bevy_internal — desktop builds compile
it out). Pairs with cargo-apk's NativeActivity wrapper.
- **`arboard` target-gated** to `cfg(not(target_os =
"android"))`. The crate has no Android backend; cargo apk
fails with E0433 on `platform::Clipboard` if left
unconditional. Stats's "Copy share link" surfaces an
informational toast on Android until JNI ClipboardManager
lands in the Phase-Android round.
- **`keyring` + `keyring-core` target-gated.** Bionic doesn't
expose `libc::__errno_location` so the transitive
`rpassword` won't compile. `auth_tokens` ships an Android
stub returning `KeychainUnavailable` for every call —
matches the existing fallback for a Linux box without
Secret Service.
- Cosmetic: cargo-apk panics post-sign when it tries to also
wrap the bin target. The APK on disk is unaffected;
`cargo apk build --lib` is the small workaround.
- **Android developer setup + build runbook** (`59424a3`).
Captures Debian 13 toolchain install (JDK 21, unzip, SDK
licence prompts), the `cargo apk build` invocation, the
cosmetic post-sign panic workaround, and a what-is-wired-vs-
stubbed table for the android target. Runnable on a fresh
clone — no machine-local context required.
- **F3-toggleable FPS / frame-time overlay** (`690e1d2`).
`DiagnosticsHudPlugin` wraps Bevy's `FrameTimeDiagnosticsPlugin`
and renders a corner readout the developer toggles with F3.
Hidden by default; F3 is not gated by pause / modal state.
Reads `smoothed()` so the cell isn't a per-frame jittery
scoreboard. Format: `FPS NN \u{2022} M.MM ms`. Anchored
top-right at `z = Z_SPLASH + 100` above every modal / toast /
splash. Update system bails when hidden so the
diagnostic-store lookup is free when nobody's looking.
- **"Smart window size" Settings toggle** (`e1b8766`). Gameplay
section gains an opt-out toggle for v0.19.0's
`apply_smart_default_window_size` system. New
`Settings::disable_smart_default_size: bool` with
`#[serde(default)]` so legacy `settings.json` files load to
the shipped behaviour (smart sizer enabled). `solitaire_app::main`
reads the flag once at startup and skips the system's
registration when set. Saved window geometry still wins over
both branches; tooltip on the row makes that explicit.
- **"Shareable" badge on the Latest-win caption** (`9b065e5`).
The Stats overlay's Latest-win caption now appends
`\u{2022} Shareable` when the displayed replay carries a
populated `share_url`. Players can see at a glance whether the
Copy share link button will produce a URL or surface the
upload-prerequisite toast.
- **Help overlay covers M / P / Win-Summary-Enter** (`35516d3`).
Three new rows in the Overlays section: M (Home / Mode
launcher), P (Profile), and the Enter accelerator that
dismisses the Win Summary modal. Three post-v0.18 entries
that had drifted out of the cheat sheet are now listed.
### Changed
- **Gameplay-feedback colours route through Terminal state
tokens** (`ceec4fc`). Selection-highlight tints in
`selection_plugin` and the valid-drop marker tint in
`cursor_plugin` were hand-tuned RGB literals. Migrated to
semantic state tokens: keyboard-drag picking source →
`ACCENT_PRIMARY` (cyan focus); keyboard-drag lifted source →
`STATE_WARNING` (gold attention); destination → `STATE_SUCCESS`
(lime valid-move); `cursor_plugin::MARKER_VALID` →
`STATE_SUCCESS` at 0.55 α with a tracking test pinning its RGB
to the token. Three stale doc comments in `ui_modal` corrected
("loud yellow CTA" / "magenta secondary accent" → cyan /
lavender to match the actual token values).
- **`table_plugin` chrome migration to Terminal tokens** (`651f406`).
`marker_colour` promoted to module-level `pub const
PILE_MARKER_DEFAULT_COLOUR` so `cursor_plugin::MARKER_DEFAULT`
imports the const directly — replaces the prior
duplicated literal kept in sync only by doc comment with a
compile-enforced invariant. The empty-tableau "K" placeholder
text now uses `TEXT_PRIMARY` at 0.35 α; `HINT_PILE_HIGHLIGHT_COLOUR`
retuned from bright `srgb(1.0, 0.85, 0.1)` to the `STATE_WARNING`
token (`#ddb26f`) with a tracking test, and the existing "is
gold" character test loosened to fit the muted Terminal gold
while still rejecting non-warm colours.
- **`card_plugin` chrome migration to Terminal tokens** (`d752870`).
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. `RIGHT_CLICK_HIGHLIGHT_COLOUR`
retuned from raw green to `STATE_SUCCESS` at 0.6 α with a
tracking test. The duplicated `PILE_MARKER_DEFAULT_COLOUR`
const dropped — this plugin now imports the promoted const
from `table_plugin`. Stock recycle "↺" text moved from raw
white-at-0.7-α to `TEXT_PRIMARY.with_alpha(0.7)`. Card-face /
suit / card-back palette constants were intentionally NOT
migrated (the runtime path renders PNG artwork that's still on
the previous "white card" palette).
- **Hint-source card tint matches the destination pile**
(`9891ae4`). `input_plugin`'s hint-source card tint moved from
raw bright-yellow `srgba(1.0, 1.0, 0.4, 1.0)` to `STATE_WARNING`,
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.
### Fixed
- **`solitaire_data::data_dir` shim closes the Android persistence
gap** (`4b51e50`). `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]`. Six call sites across
`solitaire_data` plus `solitaire_engine/assets/user_dir.rs`
migrated. CLAUDE.md §10 already flagged this as a known
pitfall; the shim pays it down at the one chokepoint instead
of per feature.
- **`card_shadow_params` test aligned with Terminal "no shadow"
intent** (`1d1543e`). The Terminal token system pinned both
`CARD_SHADOW_ALPHA_IDLE` and `CARD_SHADOW_ALPHA_DRAG` to 0.0,
which made the prior `drag_alpha > idle_alpha` assertion fail
(`0 > 0` is false). Loosened to `drag_alpha >= idle_alpha`
with a comment naming the new invariant: 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.
- **`pull_failure_sets_error_status` test flake** (`67c150b`).
The fixed 5-update budget was the last test still subject to
the AsyncComputeTaskPool starvation mode that v0.19.0's
auto-save fix already cleared. Replaced with a wall-clock-
bounded loop (5-second deadline, `std::thread::yield_now`
between iterations) that exits as soon as the status flips.
Mirrors the auto-save flake fix shape.
### Stats
- **1176 passing tests / 0 failing** across the workspace
(six new tests this cycle: four `ui_theme` invariant guards
for the type / spacing / z-index scales + `scaled_duration`,
one toast-variant-border-mapping pair, and four palette-
tracking guards on `MARKER_VALID` / `HINT_PILE_HIGHLIGHT_COLOUR`
/ `RIGHT_CLICK_HIGHLIGHT_COLOUR` / toast-border distinctness).
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
## [0.19.0] — 2026-05-06
+8 -1
View File
@@ -54,7 +54,7 @@ bevy = { version = "0.18", default-features = false, features = [
"bevy_window",
"custom_cursor",
"reflect_auto_register",
# default_platform (desktop subset; no android/webgl/gilrs/sysinfo)
# default_platform (desktop subset)
"std",
"bevy_winit",
"default_font",
@@ -65,6 +65,13 @@ bevy = { version = "0.18", default-features = false, features = [
# the game in an X11 frame inside the Wayland compositor.
"wayland",
"x11",
# Android: NativeActivity glue. The feature is target-gated inside
# bevy_internal — desktop builds compile it out, so leaving it on
# the always-on list is harmless on Linux/macOS/Windows. Pairs with
# cargo-apk's NativeActivity wrapper (cargo-apk 0.10+ uses this by
# default). Switch to `android-game-activity` later if we want
# AndroidX GameActivity for Google Play Games integration.
"android-native-activity",
# common_api
"bevy_color",
"bevy_image",
+621 -133
View File
@@ -1,171 +1,659 @@
# Solitaire Quest — Session Handoff
**Last updated:** 2026-05-06 (post-v0.19.0) — Tagged + pushed at
`6037596`. v0.19.0 closes the v0.18.0 punch list (async H-key hint,
persistent replay share URLs), expands desktop platform fit (Wayland
session support + monitor-aware default window size), polishes the
win-celebration and double-click animation paths, and clears two
test-flake contributors. A short-lived "Rusty Pixel" pixel-art card
theme was prototyped and reverted in the same window.
**Last updated:** 2026-05-08 — v0.20.0 cut and tagged at `41a009a`,
all post-cut commits pushed to origin (HEAD = `dd101b3`), working
tree clean.
The cut itself shipped two through-lines: a full **Terminal visual-
identity port** (token system, modal scaffold, gameplay-feedback,
toasts, table / card chrome, splash cursor) and the **Android
persistence shim** that closes the `dirs::data_dir() = None` pitfall
flagged in CLAUDE.md §10. Since the cut, the post-tag work split
into two arcs: (1) splash boot-screen port + replay-overlay
banner enrichments + desktop-adaptation spec — closing Resume-prompt
Options B and C (see "Since the v0.20.0 cut" entries below); and
(2) **the card-face artwork regeneration arc — Option D, closed
2026-05-08** — full Terminal cards rendering on every face, plus
three follow-up fixes that surfaced during sign-off (default-theme
SVG override, table backgrounds, top-bar overlap), plus a
glyph-orientation tweak (no 180° inverted-corner rotation).
## Status at pause
- **HEAD on origin:** `6037596` (post-tag commit; the tag itself
points at this commit).
- **Working tree:** modified — `CHANGELOG.md` and
`SESSION_HANDOFF.md` carry the v0.19.0 promotion + this refresh,
ready to commit.
- **HEAD locally:** see `git rev-parse HEAD`. Most recent narrative
entry below names the latest substantive commit; this status line
intentionally avoids hard-coding the SHA so a docs-only edit
doesn't immediately stale the handoff.
- **HEAD on origin:** matches local. All post-cut commits pushed
through `dd101b3`. Decide whether to roll the post-tag work
into v0.20.1 / v0.21.0-candidates the next time a release is cut.
- **Working tree:** clean. No WIP outstanding.
- **`artwork/` directory:** still untracked. Intentional.
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
clean (verified this session).
- **Tests:** **1170 passing / 0 failing** across the workspace
(verified this session). One known flake remains:
`solitaire_engine::sync_plugin::tests::pull_failure_sets_error_status`
occasionally fails when cargo-test parallelism starves the
`AsyncComputeTaskPool` within the test's 5-update budget. Same
shape as the auto-save flake before v0.19.0's hardening; could be
fixed similarly with a wall-clock-bounded loop.
- **Tags on origin:** `v0.9.0` through `v0.18.0` (v0.19.0 ready to
push once committed).
clean.
- **Tests:** **1184 passing / 0 failing** across the workspace.
Net delta from the 1180 baseline: splash polish added two
(`build_scanline_image_has_expected_2x2_rgba_bytes`,
`scanline_overlay_spawns_and_fades_with_splash`); the
card-face migration added one (`card_face_svg_pin` integration
test) and consolidated two (`face_colour` CBM tests folded
into `text_colour` CBM tests, net 2 then +1 from pin);
call it +4 net.
- **Tags on origin:** `v0.9.0` through `v0.20.0`. v0.20.0 is on
`41a009a`.
## Where we are
## Since the v0.20.0 cut (un-pushed)
v0.18.0's resume-prompt menu (AD) is closed:
### `39b8496` `docs(ui): add Terminal desktop-adaptation spec`
- ~~**A — Tag v0.18.0:**~~ shipped at `bfcd05f`.
- ~~**B — Solver-on-`AsyncComputeTaskPool` for the H-key hint:**~~
shipped at `3e11e9e`.
- **C — Desktop packaging:** still gated on artwork + signing
certs. Icon export PNGs (11 sizes, 161024 px) sit in
`artwork/` from the v0.18-era export; not yet wired into the
Bevy window or assembled into `.icns` / `.ico`. App icon is
the first natural step.
- ~~**D — Persistent share link:**~~ shipped at `42d90b1`.
`docs/ui-mockups/desktop-adaptation.md` — 283 lines covering
viewport assumptions, seven universal adaptation rules, and per-
screen geometry rules for the priority surfaces (Game Table, Win
Summary, Settings, Help, Pause, Home, Splash, Stats, and the
modal-pattern screens Profile / Achievements / Theme Picker /
Daily Challenge). Closes the spec gap — 23 of 24 mockups were
mobile-only, but the v0.20.0 token-port pass was already layout-
agnostic so nothing shipped broken. The spec matters for *next*
ports.
The Rusty Pixel theme arc is documented as a sub-history but
not part of v0.19.0's content:
**Why rules > visual mockups for this gap:** Stitch's
`generate_variants` API timed out on the layout-only adaptation
prompt (server-side flake, not a prompt-shape issue — confirmed
by polling `list_screens` with no new variant landing). A markdown
rules file applies to every screen including the 9 missing-plugin
surfaces (splash, challenge, time-attack, weekly-goals,
leaderboard, sync, level-up, replay-overlay, radial-menu) that
aren't in the Stitch project at all. It's also referenceable from
code comments and commit messages without loading an image.
| Commit | Status |
|---|---|
| `de47511` PNG-format thumbnail support | reverted |
| `17e3112` `pixel_art: bool` field + nearest-sampling opt-in | reverted |
| `21ec03b` bundle Rusty Pixel as `embedded://` theme | reverted |
| `aad8bb9` / `e41def8` / `0b3140a` reverts | landed |
### `cacb19c` `feat(engine): port the splash to the Terminal boot-screen treatment`
The arc remains in commit history for archaeology but the
codebase reaches v0.19.0's HEAD identical to where it would be if
the arc had never landed.
Implements the full mockup-spec splash from
`docs/ui-mockups/splash-mobile.html` plus the desktop adaptation
rules:
### Design direction (unchanged)
- **Header**: cursor block (96 px `▌`), wordmark ("Solitaire
Quest"), 192 px divider, "TERMINAL EDITION" subtitle.
- **Boot log**: three ✓ check rows (`assets loaded`,
`theme: terminal`, `progress restored`) + a `▌ ready_` line.
Capped at 480 px width on desktop (else 70 % viewport).
- **Progress bar**: 1 px track (`BORDER_SUBTLE`) with a 100 %-
width cyan (`ACCENT_PRIMARY`) fill + `DONE · 247 ASSETS`
caption. Capped at 720 px on desktop (else 80 %).
- **Footer**: `BASE16-EIGHTIES` label, eight palette swatches
(12 × 12 px each — one per named token in the design system),
version line.
- **Tone:** Balatro — chunky readable type, theatrical hierarchy,
satisfying micro-interactions.
- **Palette:** Midnight Purple base + Balatro yellow primary + warm
magenta secondary.
**Refactored the alpha-fade scaffold** from per-marker queries
(`SplashTitle` / `SplashSubtitle` / `SplashCursor`) to a single
`SplashFadable { base_color: Color }` + `SplashFadableBg`
variant. ~15 fadable elements share one global query each;
adding more is one component-attach, not three new query types.
**Skipped, with rationale captured in the commit:**
- Scanline overlay (needs a tiled-pattern asset or custom shader).
*Open in "Visual-identity follow-ups" below.*
- Pulsing cursor on the "ready_" line (would fight the global
fade timeline). *Open in "Visual-identity follow-ups" below.*
- "RUSTY SOLITAIRE" wordmark from the mockup (the actual product
is "Solitaire Quest"; the mockup leaked the repo name). *Closed
— the in-engine wordmark stays "Solitaire Quest".*
### `c84d9f4` `feat(engine): scrub fill bar + per-frame updater for replay overlay`
Closes the WIP described in the prior handoff. Adds the 1 px 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 cover the four corners
of `scrub_pct` and an end-to-end drive of `ReplayPlaybackState`
asserting `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 (closed by `6204db8` immediately below),
move-log scroll, MOVE chip, and WIN MOVE callout from the same
mockup are still open — separate commits.
### `6204db8` `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`: `Replay``▌ replay` and
`Replay complete``▌ replay complete`. The cursor block (`▌`,
U+258C) prefixed to a lowercased label reads as a Terminal output
line rather than a generic UI title, tightening the family
resemblance between the two top-level overlay surfaces. Pure
text-content change; no behavioural shift, no new components, no
new systems.
**Mockup deviation (intentional):** the source mockup string in
`docs/ui-mockups/replay-overlay-mobile.html` is `▌replay.tsx`. The
`.tsx` is a prototyping leak — Stitch renders in React, so the
mockup author reached for a familiar filename — and was dropped
for the in-engine version since the codebase is Rust. The `▌` +
lowercase pattern is what reads as a Terminal-output-line; the
extension is incidental. (Same shape as the "RUSTY SOLITAIRE"
wordmark deviation noted under `cacb19c` — the mockup leaked the
repo name; the actual product is "Solitaire Quest".)
### `54005d5` `feat(engine): add GAME #YYYY-DDD caption beneath the replay headline`
Adds the right-anchored game-identifier piece of the replay-overlay
mockup, adapted to live *under* the existing "▌ replay" headline as
a `TYPE_CAPTION` (11 px) / `TEXT_SECONDARY` subtitle. Format is
`GAME #{year}-{ordinal:03}` (e.g. `GAME #2026-122` for a replay
recorded 2026-05-02) — year + chrono ordinal gives a compact,
monotonically-increasing identifier matching the mockup's
`GAME #2024-127` motif. New `ReplayOverlayGameCaption` marker, new
pure helper `format_game_caption(state) -> Option<String>` (None
for Inactive / Completed since the replay is consumed in those
branches; spawn-time fall-through to empty string).
**Layout impact:** `BANNER_HEIGHT` bumped 48 → 60 px so the new
left column (headline + 2 px gap + caption ≈ 39 px content) fits
under the scrub bar with room to spare. +12 px banner mass is the
deliberate cost of the new content; no other plugin observes
`BANNER_HEIGHT` so the change is local.
Two new tests (1180 → 1182): `format_game_caption_covers_state_corners`
pins the three branches plus the zero-pad-to-3-digits invariant
for early-January ordinals; `overlay_game_caption_shows_replay_date`
drives `ReplayPlaybackState` end-to-end.
### `e080b49` `feat(engine): restyle replay progress text as Terminal MOVE chip`
Closes the centre-text half of the replay-overlay enrichments. 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).
**Implementation note:** `BorderColor` in Bevy 0.18 is a per-side
struct, not a tuple — `BorderColor::all(ACCENT_PRIMARY)` is the
correct constructor. Worth pinning for next time we touch a
border-painted UI surface. The `ReplayOverlayProgressText` marker
stays on the inner Text rather than the new chip Node so
`update_progress_text` keeps repainting unchanged — a deliberate
"markers belong on the entity that updates change" choice.
Test count unchanged (1182); `overlay_progress_text_reflects_cursor`
swapped its assertion from "Move 5 of 10" to "MOVE 5/10".
This pair (`54005d5` + `e080b49`) closes Option C from the
SESSION_HANDOFF Resume prompt's banner-local enrichments. Floating-
chip-above-focused-card and the full screen-takeover redesign
remain — both data-layer or cross-plugin and intentionally still
open.
### `29136d8` `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_" line now ends with a 6×12 px cyan Node
that pulses on a 1 s sine cadence, multiplied with the global
splash fade timeline so the cursor never reaches full alpha while
the rest of the splash is still fading in.
**The "multiply, don't override" pattern.** Two systems write the
same `BackgroundColor` per frame: `advance_splash` writes the
global-fade alpha, `pulse_splash_cursor` overwrites with
`global_alpha × pulse_factor`. Both derive from `SplashAge` on the
root, so the writes are commensurate — the second one isn't
"fighting" the first, just refining it. This is the cleanest fix
for the "fight the global fade timeline" warning the original
`cacb19c` skip note flagged.
**Defensive division guard.** `cursor_pulse_factor(age, period, min)`
short-circuits to `1.0` when `period <= 0.0` so a future
misconfiguration produces a steady cursor rather than NaN
propagation (NaN in alpha = invisible UI, hard to debug). Worth
mirroring on every trig/division helper, not just this one.
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 zero/negative-period guard.
### `a27cf5a` `feat(engine): add tiled scanline overlay to splash`
Closes the scanline half of the splash polish arc. 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`.
**Texture-α × tint-α composite for fade integration.** The 30 %
alpha is baked into the texture pixels, not the `ImageNode.color`
tint. `advance_splash`'s new third query writes
`(1, 1, 1, global_alpha)` into the tint each tick; the GPU
multiplies texture-α by tint-α, so the visible composite is
`0.3 × global_alpha`. Cleaner than building a "multiplicative
fadable" abstraction in the ECS — the GPU already does this
multiplication for free.
**Bevy 0.18 API surprises (worth pinning):**
- `RenderAssetUsages` re-exports under `bevy::asset::`, not
`bevy::render::render_asset::`. Type name unchanged; module
path moved.
- `TextureFormat::pixel_size()` returns `Result<usize, _>` rather
than the bare `usize` you'd expect for a static format query.
Annoying enough that the `debug_assert_eq!` against the buffer
length just hard-codes the `2 × 2 × 4 = 16` literal.
Headless test fixture now also `init_resource::<Assets<Image>>()`
since `MinimalPlugins` doesn't pull `AssetPlugin` — same pattern
`settings_plugin::tests` already used. Without it, the
`Option<ResMut<Assets<Image>>>` parameter on `spawn_splash` would
fall through and the scanline overlay would silently skip,
defeating the new tests.
Two new tests (1183 → 1185):
`build_scanline_image_has_expected_2x2_rgba_bytes` locks the
texture pixels literally so a future tweak can't drift the
appearance silently; `scanline_overlay_spawns_and_fades_with_splash`
asserts spawn placement under `SplashRoot` and the new
fade-images branch's correctness end-to-end.
This pair (`29136d8` + `a27cf5a`) closes Option B from the
SESSION_HANDOFF Resume prompt — both splash polish pieces now
shipped.
### `5623368`…`dd101b3` — Option D card-face migration arc
Closed 2026-05-08 across nine commits. The full Terminal card
artwork now renders end-to-end. Detail breakdown lives in the
"Visual-identity follow-ups" punch-list entry below; the short
version:
- Migration plan + pipeline tooling: `5623368` (plan doc),
`3a4bb63` (single-card PoC proving the `usvg`/`resvg` pipeline
at per-card grain), `babe5cc` (full
`solitaire_engine/examples/card_face_generator.rs` example
emitting 52 faces + 5 backs into `assets/cards/`), `48b28d2`
(the `card_face_svg_pin` integration test pinning rasteriser
output via inline FNV-1a hashing of raw RGBA8 bytes — the
pin's bootstrap pattern, "empty `EXPECTED` → run → paste",
is the maintenance interface for future intentional changes).
- Lockstep step 4+5: `e8bf9d7`. New PNG bytes + the 5
`card_plugin` constants (`CARD_FACE_COLOUR`,
`RED_SUIT_COLOUR`, `BLACK_SUIT_COLOUR`,
`CARD_FACE_COLOUR_RED_CBM``RED_SUIT_COLOUR_CBM`,
`card_back_colour`) + signature shifts in one commit.
`face_colour` deleted — Terminal face is uniformly
`CARD_FACE_COLOUR` regardless of CBM, so the function
collapsed to a constant. `text_colour` gained a
`color_blind: bool` parameter (red→cyan suit-glyph swap when
CBM is on). Four `face_colour` CBM tests folded into two
`text_colour` CBM tests in lockstep.
- Three follow-ups that surfaced during sign-off, all from the
same "fallback path the migration walked past" pattern:
`a14200a` regenerated the embedded **default-theme SVGs** at
`solitaire_engine/assets/themes/default/*.svg`; those bytes
`include_bytes!()`-embed into the binary and override
`assets/cards/*.png` at startup, so the PNG migration alone
didn't change what production rendered. `8719f77`
regenerated `assets/backgrounds/bg_*.png` to flat Terminal
near-black (5 solid-colour PNGs via a new
`solitaire_engine/examples/background_generator.rs` example).
`ae84dc1` cleared the **top-bar overlap** at portrait/narrow
window widths by swapping the action-button row's hardcoded
`font_size: 16.0` to `TYPE_BODY` (a typography-migration
miss) and stepping horizontal padding from `VAL_SPACE_3`
to `VAL_SPACE_2`.
- Glyph-rendering fix: `af414b6`. The bundled `FiraMono`
doesn't carry usable U+2660-2666 glyphs at the requested
size — `usvg` was silently substituting tiny "tofu" marks.
Switched suit glyphs from `<text>` elements to inline SVG
`<path>` elements via a new `suit_path_d` helper. 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).
- Glyph-orientation tweak: `dd101b3`. Removed the 180° rotation
from the bottom-right large suit glyph at user request. Both
glyphs now render upright. `design-system.md` § Game Cards
line 220 updated in lockstep — the deliberate deviation from
the traditional inverted-corner-indicator convention is
documented in the spec, not just the code.
The pin test fired exactly twice during this arc (once for the
text→path switch, once for the unrotation) and rebaselined
cleanly each time via the empty-then-paste pattern. The 5
`back_*` hashes stayed identical across both rebaselines —
secondary signal that the FNV-1a fingerprinting is purely
deterministic on rasteriser output.
This arc closes Option D from the SESSION_HANDOFF Resume prompt
and effectively completes the Terminal visual-identity port —
only the toast warning/error variant slots remain wired-but-
unused.
## What shipped in v0.20.0 (frozen at `41a009a`)
### Terminal visual-identity port
Top-down stack — every commit downstream of the token system
reads from it, so swapping the palette is now a one-file edit:
- **`ui_theme` token system** (`0d477ac`). base16-eighties
palette, 5-rung type scale, 7-rung 4-multiple spacing scale,
3-step radius, 14-rung z-index hierarchy, full motion budget,
4 invariant-pinning unit tests. Card-shadow alphas pinned to 0
(Terminal achieves depth via 1px borders + tonal layering).
- **Modal scaffold already on tokens** — `ui_modal` was ported
in the same commit's wake; three stale "loud yellow" /
"magenta secondary" doc comments fixed.
- **Gameplay feedback → semantic state tokens** (`ceec4fc`).
Selection / valid-drop tints route through `ACCENT_PRIMARY` /
`STATE_WARNING` / `STATE_SUCCESS`.
- **Toasts** (`a137607`). New `ToastVariant` enum
(Info / Warning / Error / Celebration); opaque `BG_ELEVATED`
+ 1px accent border + bottom-anchor. All ten call sites pass
their semantic variant.
- **`table_plugin` chrome** (`651f406`).
`PILE_MARKER_DEFAULT_COLOUR` promoted; `cursor_plugin` imports
it, replacing a "kept in sync" doc comment with a compile-
enforced invariant. `HINT_PILE_HIGHLIGHT_COLOUR`
`STATE_WARNING`.
- **`card_plugin` chrome** (`d752870`). Drag-elevation shadow
routes through `CARD_SHADOW_*` tokens. `RIGHT_CLICK_HIGHLIGHT_COLOUR`
`STATE_SUCCESS`. Stock recycle "↺" text → `TEXT_PRIMARY @ 0.7α`.
Card-face / suit / card-back palette intentionally NOT migrated
(artwork dependency — see open-list item below).
- **Splash cursor** (`cdcadda`). The signature `▌` cyan glyph
(96 px) added above the wordmark, matching the spec.
*Subsequently expanded post-cut by `cacb19c` into the full
boot-screen treatment.*
- **Hint-source / dest pairing** (`9891ae4`). `input_plugin`'s
source-card tint now matches the destination pile's
`STATE_WARNING`.
- **Design system + 24-mockup library** (`fa7f98a`).
`docs/ui-mockups/design-system.md` + 24 Stitch mockups (HTML +
PNG) covering every screen plus 9 missing-plugin surfaces.
- **`card_shadow_params` test aligned** (`1d1543e`). Drag-vs-
idle shadow assertion loosened to `>=` to accept the Terminal
"no shadow" intent without losing the regression-guard.
### Android persistence
- **`solitaire_data::data_dir` shim** (`4b51e50`). New
`solitaire_data::platform::data_dir()` 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 (package id pinned in `[package.metadata.android]`).
Six `solitaire_data` callsites + `solitaire_engine/assets/user_dir.rs`
migrated. Settings, stats, achievements, replays, game-state,
time-attack sessions, and user themes now persist on Android.
### Inherited from earlier in the cycle (pre-session)
- Android build target + APK (`fb8b2ac`), runbook (`59424a3`),
F3 FPS overlay (`690e1d2`), Smart Window Size opt-out
(`e1b8766`), Shareable badge (`9b065e5`), Help cheat-sheet
M/P/Enter rows (`35516d3`), `pull_failure_sets_error_status`
flake fix (`67c150b`).
## Open punch list
### Phase Android (build + persistence shipped; runtime gaps remain)
- **APK launch verification on AVD / device.** `adb install` then
`adb logcat` against the `bevy_test` AVD or an x86_64 device.
The build works and persistence is wired, but no end-to-end
device run has been logged. Shakes out runtime bugs the build +
unit tests can't catch.
- **JNI ClipboardManager bridge.** Replaces the Android stub for
the Stats "Copy share link" toast. `arboard` doesn't ship an
Android backend; small custom JNI call.
- **Android Keystore for credentials.** `keyring` is target-gated
to a stub returning `KeychainUnavailable`; replace with Android
Keystore via JNI when sync auth ships on mobile.
- **Google Play Games (gpgs) integration.** Listed as a
Phase-Android target since Phase 1; now unblocked by the build
target.
- **Cosmetic `cargo apk build --lib` workaround.** Post-sign
panic doesn't affect the APK on disk but produces noisy stderr.
Either upstream a cargo-apk fix or document `--lib` as
canonical in the runbook.
### Visual-identity follow-ups (opened by v0.20.0's port)
- *Card-face / suit / card-back artwork regeneration — closed
2026-05-08 by the commit chain `5623368``dd101b3`.* The
Terminal spec called for dark `#1a1a1a` cards with light suit
pips (pink for hearts/diamonds, foreground gray for spades/
clubs). Closed across nine commits over two arcs:
- **Plan + tooling (`5623368``48b28d2`):** migration plan
doc, single-card PoC, full `card_face_generator` example
(52 faces + 5 backs into `assets/cards/`), and the
`card_face_svg_pin` integration test pinning rasteriser
output via FNV-1a so future `usvg`/`resvg` upgrades surface
as test failures rather than silent visual drift.
- **Lockstep step 4+5 (`e8bf9d7`):** PNGs + the 5 `card_plugin`
constants + signature shifts in one commit.
`CARD_FACE_COLOUR_RED_CBM` renamed to `RED_SUIT_COLOUR_CBM`
and repurposed from a face-tint to a suit-glyph swap (the
Terminal face is uniform `CARD_FACE_COLOUR` regardless of
CBM; CBM only swaps red suits to cyan in the glyph itself).
`face_colour` deleted, `text_colour` gained a `color_blind`
parameter.
- **Three follow-ups that surfaced during sign-off:**
`a14200a` regenerated the **default-theme SVGs** at
`solitaire_engine/assets/themes/default/*.svg` — those
`include_bytes!()`-embed into the binary and override
`assets/cards/*.png` at runtime, so the PNG migration alone
didn't change what production rendered. `8719f77`
regenerated `assets/backgrounds/bg_*.png` to flat Terminal
near-black (5 solid-colour PNGs via a new
`background_generator` example). `ae84dc1` cleared the
**top-bar overlap** at portrait/narrow window widths by
swapping the action-button row's hardcoded `font_size: 16.0`
to `TYPE_BODY` and stepping horizontal padding from
`VAL_SPACE_3` to `VAL_SPACE_2`.
- **Glyph-rendering fix (`af414b6`):** suit glyphs render as
inline SVG paths (not `<text>`) because the bundled
`FiraMono` doesn't carry usable U+2660-2666 at the
requested size — `usvg` was silently substituting tiny
"tofu" marks. Path-based rendering bypasses the font system
entirely; same bytes on every machine. The pin test
rebaselined cleanly via the empty-then-paste pattern.
- **Glyph-orientation tweak (`dd101b3`):** removed the 180°
rotation from the bottom-right large suit glyph at user
request — both glyphs now render in the same upright
orientation. `design-system.md` § Game Cards line 220
updated in lockstep to document the deliberate deviation
from the traditional inverted-corner-indicator convention.
- *Splash boot-loader scanline overlay — closed by `a27cf5a`.*
Runtime-generated 2 × 2 RGBA8 texture tiled via
`NodeImageMode::Tiled`; per-pixel alpha × tint alpha gives
multiplicative fade integration without new abstractions.
- *Splash cursor pulse — closed by `29136d8`.* Trailing 6 × 12 px
cyan Node, sine-pulsed, multiplied with the global splash fade
(the "multiply, don't override" pattern that resolves the
original `cacb19c` skip-rationale).
- **Replay-overlay enrichments beyond the scrub bar.** Banner-local
pieces of the mockup (`docs/ui-mockups/replay-overlay-mobile.html`)
all shipped: scrub bar (`c84d9f4`), `▌ replay` cursor-block label
(`6204db8`), `GAME #YYYY-DDD` caption (`54005d5`), `MOVE N/M`
chip restyle (`e080b49`). What's still open are the cross-plugin
/ data-layer pieces: a `MOVE N/M` chip *floating above the
focused card* during playback (would need to thread the cursor
through to the card layer — `update_progress_text` writes the
banner chip but the card-position lookup belongs in `card_plugin`).
The full mockup's screen-takeover treatment — mini-tableau
preview, playback controls, move-log scroll, WIN MOVE marker on
the scrub bar — is a multi-session redesign with
data-layer impact (move-log scroller; the WIN MOVE marker
needs a `win_move_index` field on `Replay` that doesn't yet
exist). Banner-overlay behaviour is intentionally preserved
for now.
- **Toast Warning / Error variants.** The `ToastVariant` enum
has slots for `Warning` (gold) and `Error` (pink) but no
in-engine event uses them yet. Wire when a warning- or error-
flavoured toast event materialises.
### Carried forward from v0.19.0
- **App icon round.** `Window::icon` not yet wired; no
`.icns` / `.ico` / Linux hicolor PNG hierarchy. The 11-size
icon export the v0.19 handoff referenced is *not* currently
in `artwork/` (current `artwork/` holds the reverted Rusty
Pixel card PNGs and is intentionally untracked); icon-export
needs to be re-run before this item can be picked up.
Half-day task once the PNGs are back in place. No cert
dependency.
### Other small candidates
- **Prev/Next selector chips spawn site.** v0.19.0's `9b065e5`
noted Prev/Next markers exist in `stats_plugin` but no spawn
site renders them today — the Shareable badge therefore lands
on the single-replay caption. If/when Prev/Next is plumbed,
the badge will need to follow.
- **Toast queue / immediate unification.** The two toast paths
(`spawn_queued_toast` for `InfoToastEvent` queue; `spawn_toast`
for fire-and-forget) now share visual treatment but remain
separate functions because they serve different temporal
needs (sequential vs. parallel). If overlap becomes a UX
issue, merge into one queue with priority lanes.
### Process notes
- **The desktop-adaptation spec is the canonical reference for
geometry decisions** when porting any future plugin. Read
`docs/ui-mockups/desktop-adaptation.md` first; apply the
universal rules to every surface; consult the per-screen
table for the priority surfaces. The 9 missing-plugin screens
(splash now ported; eight remaining) inherit the universal
rules without dedicated guidance.
- **Stitch `generate_variants` is unreliable for layout-only
adaptation prompts** as of 2026-05-07. The first call timed
out and no variant ever landed in `list_screens`. If a future
session wants visual desktop mockups, prefer
`generate_screen_from_text` with a fresh narrow prompt per
screen rather than `generate_variants` against existing
mobile screens.
- **Token-port pattern.** v0.20.0's chrome-migration commits
set a reusable shape for "centralised design system applied
across N plugins":
1. Constants module (`ui_theme.rs`) is the source of truth.
2. Const sites that can't call `Alpha::with_alpha` (not yet
`const` on stable) use a literal RGB matching the token,
with a unit test pinning the RGB to the token (e.g.
`MARKER_VALID`, `HINT_PILE_HIGHLIGHT_COLOUR`,
`RIGHT_CLICK_HIGHLIGHT_COLOUR`).
3. Cross-plugin duplication (e.g. `MARKER_DEFAULT`
`PILE_MARKER_DEFAULT_COLOUR`) collapses to a single
promoted const re-exported from one plugin and imported
by the other — replaces "kept in sync" doc comments with a
compile-time invariant.
4. Domain colours (suit pips, card faces, lerp helpers) stay
as literals with a comment naming the rationale; only UI
chrome routes through tokens.
- **`SplashFadable` scaffolding pattern** (introduced in
`cacb19c`). Any future overlay that needs to fade `N >> 3`
elements together should follow the same shape: one tiny
marker carrying the full-alpha base colour, one global query
that lerps every marker's alpha each frame, no per-element
query plumbing. Cleanly outscales the `Without<X>, Without<Y>`
query exclusion pattern that the old splash was hitting at
three siblings.
### Canonical remote
`github.com/funman300/Rusty_Solitaire` is the canonical repo.
Always push there.
Always push there. **Local master has unpushed post-cut commits**
— run `git log --oneline origin/master..HEAD` for the live list;
`git push` is the next durability step (or roll the post-cut
commits into v0.20.1).
## v0.19.0 (2026-05-06)
### Design direction (Terminal — base16-eighties)
| Area | Commits | What landed |
|---|---|---|
| Async H-key hint | `3e11e9e` | New `pending_hint.rs` module: `PendingHintTask` resource, `poll_pending_hint_task` + `drop_pending_hint_on_state_change` systems, cancel-on-replace, stale-state guard via `move_count_at_spawn`. Removes the last synchronous solver hot path. |
| Persistent share URLs | `42d90b1` | `Replay.share_url: Option<String>` with `#[serde(default)]`. `poll_replay_upload_result` writes into `replays[0].share_url` + persists. Stats Copy button reads from selected replay. `LastSharedReplayUrl` deleted. |
| Auto-save flake fix | `91b7605` | `test_app` clears `PendingRestoredGame(None)` after plugin build; test re-arms the timer in a bounded loop. No production-code change. |
| Wayland support | `b57db01` | Adds `wayland` to Bevy features. winit prefers Wayland when `WAYLAND_DISPLAY` is set, falls back to X11. Native Wayland surface instead of XWayland frame. |
| Smart default window size | `b57db01` | New `apply_smart_default_window_size` Update system queries `PrimaryMonitor` and resizes the window to ~70 % of monitor's logical size on the first frame. Skipped when saved geometry was applied. |
| Win-celebration cleanup | `55c235b` | Drops the duplicate "You Win" toast that rendered behind the WinSummary modal. Cards-fly-off cascade kept; toast removed. |
| Double-click reject animation | `d7ffb16` | Single-card double-clicks with no destination now play the same shake + sound as multi-card stack misses. Both priorities' failure paths converge on one `MoveRejectedEvent` write. |
| Double-click animation dedup | `6037596` | Drops the redundant `StateChangedEvent` write in `end_drag`'s uncommitted-drag branch; previously raced an in-flight CardAnim and restarted the slide visibly. |
## Open punch list
### Carried forward
- **Desktop packaging** per `ARCHITECTURE.md §17`. Eleven icon
PNG sizes (16, 24, 32, 48, 64, 96, 128, 192, 256, 512, 1024)
exported via `artwork/Icon Export.html` sit in `artwork/`
pending wiring. Pending: actual Bevy window-icon hookup,
macOS `.icns` assembly via `iconutil`, Windows `.ico` via
`magick convert`, Linux hicolor PNG hierarchy install,
AppImage recipe, macOS notarisation cert, Windows
Authenticode cert.
### Possible next-round candidates
- **App icon round** — wire the icon into the Bevy window via
`Window::icon`, generate `.icns` and `.ico` from the existing
PNGs. Half-day task; doesn't depend on signing certs.
- **`pull_failure_sets_error_status` flake fix** — same pattern
as the auto-save flake. Wall-clock-bounded loop instead of
fixed 5-update budget. ~10 lines.
- **Settings UI for "open at this size on launch"** — once the
smart-default-size system is shipping, expose a checkbox to
*disable* it (player who specifically wants 1280×800 every
time). Trivial.
- **Persistent share link URL on selector caption** — surface
whether the currently-selected replay has a `share_url`
populated (e.g. "Replay 3 / 8 \u{2022} Shareable") so players
know which entries the Copy button can copy.
### Process notes (from this round)
- **Async port template (worked again):** the H-key port
followed `d489e7a`'s `PendingNewGameSeed` shape one-to-one
and the second async port required no new infrastructure.
Future async ports (e.g. moving `try_solve_with_first_move`'s
full-search variant, if it ever surfaces in the picker UI)
should follow the same shape.
- **Rusty Pixel reverted cleanly:** `git revert` of three
contiguous feature commits produced a clean three-revert
sequence with no manual conflict resolution. Bisect remains
fast over the full v0.19.0 history because the reverts are
individual commits, not a squash.
- **Defensive event writes pattern:** the
`auto_save_writes_after_30_seconds` flake AND the
`end_drag` double-animation bug shared a root cause:
defensive `MessageWriter` writes that originally covered an
edge case which no longer holds, but became load-bearing
once another system started paying attention to the event.
Worth a periodic pass: any event write that doesn't
correspond to a real state change is a candidate for
removal.
- **Tone:** retro-terminal / synthwave — flat depth (no box-shadows),
monospaced-forward typography (JetBrains Mono / FiraMono), tight
16 px edge margins, 8 px card radius.
- **Palette:** near-black surface ramp (`#151515` / `#202020` /
`#2a2a2a` / `#353535`), cyan primary CTA (`#6fc2ef`), lime
success (`#acc267`), gold warning (`#ddb26f`), pink error /
suit-red (`#fb9fb1`), lavender celebration (`#e1a3ee`), teal
info (`#12cfc0`).
- **Two-color suits.** Red = `#fb9fb1`, black = `#d0d0d0`.
Outlined glyphs for diamonds & clubs are *always on*; the
Settings "color-blind mode" toggle only swaps red → cyan.
## Resume prompt
```
You are a senior Rust + Bevy developer working on Solitaire Quest.
Working directory: <Rusty_Solitaire clone path on this machine>.
Branch: master. v0.19.0 just shipped. The next natural item is
desktop-packaging follow-through, starting with the app icon.
Branch: master. v0.20.0 is tagged at 41a009a; the post-cut work
through dd101b3 is pushed to origin (Options B, C, D all closed).
Run `git log --oneline 41a009a..HEAD` to see what landed since the
tag — substantives: desktop-adaptation spec, splash boot-screen
port, replay-overlay banner enrichments, and the full card-face
artwork arc (52 faces + 5 backs as Terminal SVG-rasterised PNGs,
default-theme SVGs in lockstep, table backgrounds flattened,
top-bar layout fix, glyph orientation upright).
State: HEAD at 6037596 + the v0.19.0 docs commit on top (this
session). Tag v0.19.0 points at the docs commit.
State: HEAD locally — see `git rev-parse HEAD`. Working tree is
clean. All workspace tests pass (~1180+; check with
`cargo test --workspace`), clippy clean.
READ FIRST (in order, before doing anything):
1. SESSION_HANDOFF.md — this file
2. CHANGELOG.md — [Unreleased] is empty; [0.19.0] just landed
2. CHANGELOG.md — [0.20.0] section is the most recent cut
3. CLAUDE.md — unified-3.0 rule set
4. CLAUDE_SPEC.md — formal architecture spec
5. ARCHITECTURE.md — crate responsibilities + data flow
6. ~/.claude/projects/<this-project>/memory/MEMORY.md
6. docs/ui-mockups/ — design system + 24-mockup library +
desktop-adaptation.md (the rules-based
companion to the mockups; read this
before any plugin port)
7. docs/android/* — Android setup + build runbook
8. ~/.claude/projects/<this-project>/memory/MEMORY.md
— saved feedback / project context
(machine-local; may be missing on a
fresh machine)
DECISION TO ASK THE PLAYER FIRST:
A. App icon — wire artwork/icon-{size}.png into Bevy's
Window::icon, generate .icns + .ico, drop into Linux
hicolor hierarchy. Half-day task. No cert dependency.
B. Desktop packaging continued — AppImage recipe, .desktop
file, install scripts. Larger task; unlocks distro
packaging. No cert dependency.
C. macOS / Windows signing cert acquisition — needs user
action; agent can't drive.
D. `pull_failure_sets_error_status` flake fix — small, well-
scoped. Same pattern as the v0.19.0 auto-save flake fix.
A. Push the post-cut commits to origin. Either as-is on master
or rolled into a v0.20.1 cut (CHANGELOG entry + tag).
Mechanical, but local master diverges from origin until done.
B. *Closed by `29136d8` + `a27cf5a`.* Both splash polish
pieces shipped (cursor pulse + scanline overlay). No further
splash work pending unless a new mockup detail surfaces.
C. *Closed by `54005d5` + `e080b49`.* Banner-local replay-overlay
pieces all shipped (scrub bar, ▌ label, GAME caption, MOVE
chip). Remaining are cross-plugin (floating MOVE chip above
the focused card — needs cursor → card-position plumbing) or
multi-session (full screen-takeover redesign — move-log
scroll, mini tableau, WIN MOVE marker, data-layer impact).
Either belongs in its own decision tree the next time replay
work surfaces.
D. *Closed 2026-05-08 by `5623368`…`dd101b3`.* The full
card-face / suit / card-back / default-theme / table-
background / top-bar / glyph-orientation arc landed across
nine commits. Terminal cards rendering on every face (dark
`#1a1a1a` background, pink/gray suit glyphs as inline SVG
paths, scanline-pattern cyan-accent backs); both rendering
paths (`assets/cards/*.png` and the bundled-default theme
SVGs at `solitaire_engine/assets/themes/default/*.svg`) in
lockstep; pin test (`card_face_svg_pin`) guards against
future rasteriser drift. Visual-identity arc effectively
complete — only the toast warning/error variant slots
remain wired-but-unused.
E. App icon round — re-run artwork/Icon Export.html (the
export PNGs are not currently in `artwork/`), then wire
Window::icon + generate .icns / .ico. Half-day task. No
cert dependency.
F. APK launch verification on AVD / device + the JNI bridges
it would shake out (ClipboardManager, Keystore).
WORKFLOW NOTES:
- Use the system git config (already correct).
@@ -173,8 +661,8 @@ WORKFLOW NOTES:
"Quat" not "Rhys" (saved feedback memory).
- Sub-agents stage + verify only; orchestrator commits.
- Every commit must pass build / clippy / test before pushing.
- Push to GitHub (origin) — gh auth setup-git is already
wired on this machine.
- Push to GitHub (origin) — gh auth setup-git wired on
primary dev box; verify on laptop before first push.
OPEN AT THE START: ask which of AD. Don't pick unilaterally.
OPEN AT THE START: ask which of AF. Don't pick unilaterally.
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+228
View File
@@ -0,0 +1,228 @@
# Android build — developer setup
This doc captures the toolchain install + build invocation for the
Android target. Steps are runnable on a fresh Debian 13 (trixie) box;
later sections document what's known to compile, what's stubbed, and
the next milestones.
> **Status (2026-05-07):** First working APK at `fb8b2ac`. 54 MB
> debug-signed `solitaire-quest.apk` for `x86_64-linux-android`. Has
> NOT yet been verified to launch on a device or emulator — that's
> the next milestone.
---
## 1. Toolchain install (Debian 13 / trixie)
Run as one block. Will pull ~15-20 GB of disk between APT, the SDK,
the NDK, the system image, and Rust target sysroots. Requires sudo.
```bash
# 1. JDK 21 (Android tooling needs JDK 17+; Debian 13 default is 21).
sudo apt update && sudo apt install -y openjdk-21-jdk-headless unzip wget
# 2. SDK directory + Google's cmdline-tools bootstrap.
export ANDROID_HOME="$HOME/Android/Sdk"
mkdir -p "$ANDROID_HOME/cmdline-tools"
wget -O /tmp/cmdline-tools.zip \
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
unzip -q /tmp/cmdline-tools.zip -d "$ANDROID_HOME/cmdline-tools"
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
rm /tmp/cmdline-tools.zip
# 3. Persist env vars.
{
echo ''
echo '# Android dev'
echo 'export ANDROID_HOME="$HOME/Android/Sdk"'
echo 'export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/26.3.11579264"'
echo 'export JAVA_HOME="$(dirname $(dirname $(readlink -f $(which java))))"'
echo 'export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator"'
} >> ~/.bashrc
source ~/.bashrc
# 4. Accept SDK licences (interactive prompts answered by `yes |`).
yes | sdkmanager --licenses
# 5. Platform packages — ~5 GB.
sdkmanager \
"platform-tools" \
"platforms;android-34" \
"build-tools;34.0.0" \
"ndk;26.3.11579264" \
"emulator" \
"system-images;android-34;google_apis;x86_64"
# 6. AVD for testing (one-time).
echo no | avdmanager create avd \
-n bevy_test \
-k "system-images;android-34;google_apis;x86_64" \
-d pixel_7
# 7. Rust cross-compile targets.
rustup target add \
aarch64-linux-android \
armv7-linux-androideabi \
x86_64-linux-android \
i686-linux-android
# 8. cargo-apk.
cargo install cargo-apk
```
Sanity:
```bash
java --version | head -1 # openjdk 21.0.x
adb --version | head -1 # 35.x or higher
sdkmanager --list_installed | head # build-tools, emulator, ndk, platforms, system-images
avdmanager list avd | head # bevy_test
rustup target list --installed | grep android # 4 targets
cargo apk --help | head -5
```
If `sdkmanager --version` errors with `JAVA_HOME is not set`, the env
section in step 3 didn't apply to your shell — `source ~/.bashrc`
again or open a new terminal.
### Optional: emulator runtime libs
The Android emulator is dynamically linked against X11/GL/audio. If
`emulator -list-avds` works but `emulator -avd bevy_test` complains
about `libX11.so.6`, install:
```bash
sudo apt install -y \
libx11-6 libxcursor1 libxrandr2 libxi6 libxinerama1 libxxf86vm1 \
libgl1 libnss3 libpulse0 libxcomposite1
```
Headless emulator launch:
```bash
emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
adb wait-for-device && adb devices
# Stop later:
# adb -s emulator-5554 emu kill
```
Headless + software rendering is fine for "does it boot" smoke tests
but useless for perf measurement — use a physical Pixel-class device
over USB for real numbers.
---
## 2. Build the APK
```bash
cargo apk build -p solitaire_app --target x86_64-linux-android
```
Output:
```
target/debug/apk/solitaire-quest.apk
```
Targets shipped via `[package.metadata.android].build_targets` in
`solitaire_app/Cargo.toml`:
| Target | Use |
|--------|-----|
| `aarch64-linux-android` | Real phones (modern 64-bit ARM) |
| `armv7-linux-androideabi` | Older 32-bit ARM phones |
| `x86_64-linux-android` | The `bevy_test` AVD on this dev box |
Build any of them with `--target <triple>`.
### Known cosmetic warning
After the APK is signed cargo-apk panics with:
```
thread 'main' panicked: Bin is not compatible with Cdylib
```
This happens AFTER the APK is on disk and signed. cargo-apk is
trying to also wrap the desktop `[[bin]]` target. The APK is still
valid. Work around with `--lib`:
```bash
cargo apk build -p solitaire_app --target x86_64-linux-android --lib
```
(Permanent fix to come — likely a `[[bin]] required-features = ["desktop"]`
gate so cargo-apk skips the bin target on Android.)
---
## 3. Install + run
Physical device:
```bash
adb devices # confirm connection
adb install target/debug/apk/solitaire-quest.apk
adb shell am start -n com.solitairequest.app/android.app.NativeActivity
adb logcat | grep -iE "RustStdoutStderr|solitaire|panic"
```
Emulator:
```bash
emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
adb wait-for-device
adb install target/debug/apk/solitaire-quest.apk
# ... same start + logcat steps as above.
```
If `adb install` errors with `INSTALL_FAILED_NO_MATCHING_ABIS`, the
emulator is x86_64 but the APK was built for arm — rebuild with the
`x86_64-linux-android` target, or add an x86_64 system image to the
AVD.
---
## 4. What's wired vs. what's stubbed
The first build pass (commit `fb8b2ac`) gates four desktop-only
crates / call sites so the workspace cross-compiles. Each gate is
documented at its call site.
| Surface | Desktop | Android |
|---------|---------|---------|
| Bevy windowing | x11 + wayland | `android-native-activity` (NativeActivity glue) |
| Clipboard ("Copy share link") | `arboard` writes URL | Toast surfaces the URL inline |
| OS keychain (JWT tokens) | `keyring` v4 → Secret Service / Keychain / Credential Store | Stub returning `KeychainUnavailable`; sync requires fresh login each launch |
| App entry point | `bin` target → `solitaire_app::run()` | `cdylib` target loaded by NativeActivity |
What's NOT yet ported / not yet measured:
- `dirs::data_dir()` returns `None` on Android. Callers in
`solitaire_data/src/storage.rs`, `progress.rs`, `replay.rs`,
`achievements.rs`, `settings.rs` all need an Android-aware
helper (likely `/data/data/com.solitairequest.app/files`).
- Touch UX pass — hit-target sizes, modal scaling on small screens,
app lifecycle (suspend / resume), font scaling.
- Android Keystore via JNI for `auth_tokens`.
- JNI ClipboardManager for share links.
- Google Play Games sign-in (the `solitaire_gpgs` crate referenced
in older docs doesn't yet exist).
---
## 5. Iteration loop
```bash
# Edit code…
cargo build -p solitaire_app # desktop sanity
cargo clippy --workspace --all-targets -- -D warnings # gate
cargo test --workspace # gate
cargo apk build -p solitaire_app --target x86_64-linux-android --lib
adb install -r target/debug/apk/solitaire-quest.apk # `-r` reinstalls
adb logcat -c && adb shell am start -n com.solitairequest.app/android.app.NativeActivity
adb logcat | grep -iE "RustStdoutStderr|solitaire"
```
`adb logcat` is the canonical way to see Bevy / Rust panic output —
they end up in the `RustStdoutStderr` tag.
+293
View File
@@ -0,0 +1,293 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Rusty Solitaire - Achievements</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&amp;family=Inter:wght@400;500;700&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed": "#fbd7ff",
"on-tertiary-container": "#683476",
"surface-container-lowest": "#0b0f11",
"error": "#fb9fb1",
"secondary-fixed-dim": "#bad073",
"on-primary-fixed-variant": "#004c69",
"background": "#101417",
"error-container": "#93000a",
"tertiary-container": "#e1a3ee",
"inverse-primary": "#00668a",
"highlight-valid": "#acc267",
"suit-red": "#fb9fb1",
"on-surface-variant": "#bfc8cf",
"on-secondary": "#293500",
"on-primary-container": "#004f6c",
"surface-tint": "#7ed0fe",
"on-surface": "#e0e3e6",
"outline-variant": "#3f484e",
"on-background": "#e0e3e6",
"primary-fixed": "#c4e7ff",
"inverse-surface": "#e0e3e6",
"info": "#12cfc0",
"inverse-on-surface": "#2d3134",
"warning": "#ddb26f",
"on-tertiary-fixed-variant": "#653173",
"on-secondary-container": "#b2c86d",
"on-secondary-fixed-variant": "#3c4d00",
"highlight-celebration": "#e1a3ee",
"surface": "#151515",
"surface-container-highest": "#313538",
"outline": "#505050",
"on-primary": "#003549",
"on-error-container": "#ffdad6",
"surface-variant": "#313538",
"on-error": "#690005",
"suit-black": "#d0d0d0",
"primary": "#a1dcff",
"suit-red-cb": "#6fc2ef",
"surface-bright": "#363a3d",
"on-tertiary": "#4c195b",
"surface-dim": "#101417",
"primary-container": "#6fc2ef",
"tertiary": "#f7c3ff",
"primary-fixed-dim": "#7ed0fe",
"surface-container-high": "#272a2d",
"on-secondary-fixed": "#161e00",
"surface-container": "#1c2023",
"tertiary-fixed-dim": "#f0b0fc",
"secondary-fixed": "#d5ec8c",
"secondary-container": "#435401",
"on-tertiary-fixed": "#340043",
"on-primary-fixed": "#001e2c",
"secondary": "#bad073",
"surface-container-low": "#181c1f"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-overlap": "2rem",
"gutter-card": "0.375rem",
"touch-target-min": "48dp",
"margin-edge": "1rem",
"action-bar-height": "64px"
},
"fontFamily": {
"card-rank": ["JetBrains Mono"],
"body-md": ["Inter"],
"label-caps": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"]
},
"fontSize": {
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body {
background-color: #151515;
color: #e0e3e6;
-webkit-font-smoothing: antialiased;
}
.scanline {
background: linear-gradient(to bottom, transparent 50%, rgba(0,0,0,0.1) 50%);
background-size: 100% 2px;
pointer-events: none;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="font-body-md text-body-md overflow-x-hidden pb-[action-bar-height]">
<!-- Status Bar -->
<header class="fixed top-0 w-full h-[32px] bg-[#202020] flex items-center justify-between px-margin-edge z-[60] border-b border-outline-variant">
<div class="flex items-center gap-2 font-label-caps text-on-surface">
<span class="text-primary"></span>achievements.json
</div>
<div class="font-label-caps text-[#a0a0a0]">
8/19 UNLOCKED
</div>
</header>
<!-- Top App Bar (Shared Component Reference) -->
<nav class="fixed top-[32px] w-full h-[64px] bg-surface flex items-center justify-between px-margin-edge z-50 border-b border-outline-variant">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-primary" data-icon="terminal">terminal</span>
<h1 class="font-headline text-[20px] text-primary uppercase tracking-widest">Rusty Solitaire</h1>
</div>
<button class="w-10 h-10 flex items-center justify-center hover:bg-surface-container-highest transition-colors">
<span class="material-symbols-outlined text-on-surface-variant" data-icon="settings">settings</span>
</button>
</nav>
<main class="mt-[112px] px-margin-edge">
<!-- Hero Progress Card -->
<section class="w-full h-[100px] bg-[#202020] border border-[#353535] rounded-lg p-4 mb-6">
<div class="flex flex-col justify-between h-full">
<span class="font-label-caps text-[10px] text-[#a0a0a0]">PROGRESS</span>
<div class="flex items-baseline gap-2">
<span class="font-headline text-[28px] font-bold text-[#d0d0d0]">8/19</span>
<span class="font-label-caps text-[14px] text-highlight-celebration">(42%)</span>
</div>
<div class="w-full h-[4px] bg-[#353535] rounded-full overflow-hidden mt-1">
<div class="h-full bg-highlight-celebration" style="width: 42%;"></div>
</div>
</div>
</section>
<!-- Filter Chip Row -->
<section class="flex gap-2 mb-6 overflow-x-auto no-scrollbar">
<button class="h-[32px] px-3 flex items-center justify-center border border-[#6fc2ef] text-[#6fc2ef] rounded-[4px] font-label-caps text-[11px]">
[ ALL ]
</button>
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
UNLOCKED
</button>
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
LOCKED
</button>
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
SECRET
</button>
</section>
<!-- Achievements Grid -->
<section class="grid grid-cols-2 gap-3 mb-10">
<!-- FIRST WIN -->
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="emoji_events" style="font-variation-settings: 'FILL' 1;">emoji_events</span>
</div>
<div>
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">FIRST WIN</h3>
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Win your first game</p>
</div>
</div>
<!-- SPEED DEMON -->
<div class="h-[100px] bg-[#202020] border border-highlight-celebration p-3 flex flex-col justify-between rounded-sm relative">
<div class="absolute inset-0 border border-highlight-celebration opacity-20 pointer-events-none"></div>
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="speed" style="font-variation-settings: 'FILL' 1;">speed</span>
</div>
<div>
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">SPEED DEMON</h3>
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Win in under 3:00</p>
</div>
</div>
<!-- STREAK 10 -->
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="bolt" style="font-variation-settings: 'FILL' 1;">bolt</span>
</div>
<div>
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">STREAK 10</h3>
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">10 wins in a row</p>
</div>
</div>
<!-- DAILY DEFENDER -->
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="calendar_today" style="font-variation-settings: 'FILL' 1;">calendar_today</span>
</div>
<div>
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">DAILY DEFENDER</h3>
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Complete 7 daily seeds</p>
</div>
</div>
<!-- PERFECTIONIST (LOCKED) -->
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="undo">undo</span>
</div>
<div>
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">PERFECTIONIST</h3>
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Win without using undo</p>
</div>
</div>
<!-- CHALLENGE BEATEN (LOCKED) -->
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="military_tech">military_tech</span>
</div>
<div>
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">CHALLENGE BEATEN</h3>
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Complete CHALLENGE mode</p>
</div>
</div>
<!-- SECRET (LOCKED) -->
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="help_outline">help_outline</span>
</div>
<div>
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">????</h3>
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">SECRET · Hidden until unlocked</p>
</div>
</div>
<!-- PAR HUNTER (LOCKED) -->
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="golf_course">golf_course</span>
</div>
<div>
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">PAR HUNTER</h3>
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Beat par on 50 games</p>
</div>
</div>
</section>
</main>
<!-- Footer Status -->
<footer class="fixed bottom-[action-bar-height] w-full h-[24px] bg-background border-t border-outline-variant flex items-center justify-between px-margin-edge z-40 text-[10px] font-label-caps">
<div class="flex items-center">
<span class="text-primary mr-1"></span>
<span class="text-on-surface-variant">NORMAL</span>
<span class="mx-2 text-outline"></span>
<span class="text-on-surface-variant">achievements</span>
</div>
<div class="flex gap-3">
<div><span class="text-[#a0a0a0]">[F]</span> <span class="text-[#505050]">filter</span></div>
<div><span class="text-[#a0a0a0]">[/]</span> <span class="text-[#505050]">search</span></div>
</div>
</footer>
<!-- Bottom Navigation Bar (Shared Component Reference) -->
<nav class="fixed bottom-0 w-full h-action-bar-height bg-surface-container flex justify-around items-center px-margin-edge z-50 border-t border-outline-variant">
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
<span class="material-symbols-outlined" data-icon="power_settings_new">power_settings_new</span>
<span class="font-label-caps text-[10px] mt-1">[Q] QUIT</span>
</button>
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
<span class="material-symbols-outlined" data-icon="add_box">add_box</span>
<span class="font-label-caps text-[10px] mt-1">[N] NEW</span>
</button>
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
<span class="material-symbols-outlined" data-icon="undo">undo</span>
<span class="font-label-caps text-[10px] mt-1">[U] UNDO</span>
</button>
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
<span class="material-symbols-outlined" data-icon="lightbulb">lightbulb</span>
<span class="font-label-caps text-[10px] mt-1">[H] HINT</span>
</button>
</nav>
<!-- CRT Overlay Effect (Visual Decoration) -->
<div class="fixed inset-0 pointer-events-none z-[100] opacity-[0.03] scanline"></div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+251
View File
@@ -0,0 +1,251 @@
# Card-face artwork migration plan
**Status:** planning artifact (no code changed by this document).
**Tracks:** the "Card-face / suit / card-back artwork regeneration"
item in `SESSION_HANDOFF.md` → "Visual-identity follow-ups"
(SESSION_HANDOFF Resume prompt option D).
**Companion to:** `docs/ui-mockups/design-system.md` (Game Cards
spec, lines 214233) and `docs/ui-mockups/desktop-adaptation.md`
(rules-based companion to the mockups).
## Why this is a multi-session arc
Every post-v0.20.0 visual-identity port to date (modal scaffold,
toasts, table chrome, splash boot screen, replay overlay) was a
**single rendering path** — change tokens, change comments, ship.
Cards have **two** rendering paths that are visually identical
today and would visually disagree the moment one moves:
1. **PNG path (production).** `assets/cards/faces/<rank><suit>.png`
loaded into `CardImageSet.faces[suit][rank]` at startup; card
sprites blit the texture. 52 face PNGs + 5 back PNGs already
in `assets/`, all the legacy white-card aesthetic from the
pre-Terminal design system.
2. **Constant fallback (tests + asset-missing edge).** When
`CardImageSet` isn't a registered resource (the case under
`MinimalPlugins` test fixtures, and the bare-bones path the
first-frame of production hits before assets resolve), the
renderer falls back to solid-colour sprites driven by the
`card_plugin` constants:
- `CARD_FACE_COLOUR``(0.98, 0.98, 0.95)` cream-ish white.
- `RED_SUIT_COLOUR``(0.78, 0.12, 0.15)` warm red.
- `BLACK_SUIT_COLOUR``(0.08, 0.08, 0.08)` near-black.
- `CARD_FACE_COLOUR_RED_CBM``(0.85, 0.92, 1.0, 1.0)` light
blue (the legacy color-blind tint).
- `card_back_colour(idx)` — five legacy back themes.
A single-path migration leaves a known-broken state where tests
pass against Terminal constants while a human sees legacy artwork
on screen — the exact bisection-hostile drift the handoff's
"in lockstep" warning preempts.
## Target state — Terminal aesthetic
Per `design-system.md` § Game Cards (lines 214233):
### Card face
| Element | Spec |
|---|---|
| Background | `#1a1a1a` |
| Border | 1 px solid in **suit colour** (pink for ♥/♦, foreground gray for ♠/♣) |
| Corner radius | 8 px |
| Top-left | rank in JetBrains Mono **Bold 18 px** + small suit glyph (10 px) |
| Bottom-right | large suit glyph (32 px), rotated 180° |
| Glyph fill rule | ♥ ♠ filled; ♦ ♣ outlined (1.5 px stroke). Always on, not a toggle. |
### Suit colours (always-on glyph differentiation is the *primary*
distinguishing mechanism; colour is supplementary):
| Suit | Default | Color-blind mode |
|---|---|---|
| Hearts | `#fb9fb1` (pink) | `#6fc2ef` (cyan) |
| Diamonds | `#fb9fb1` (pink) | `#6fc2ef` (cyan) |
| Spades | `#d0d0d0` (gray) | `#d0d0d0` (unchanged) |
| Clubs | `#d0d0d0` (gray) | `#d0d0d0` (unchanged) |
### Card back ("Terminal" theme)
| Element | Spec |
|---|---|
| Background | `#151515` |
| Pattern | horizontal scanlines at 2 px pitch in `#1a1a1a` (1 px line, 1 px gap), full bleed |
| Border | 1 px solid `#353535` |
| Top-left badge | 12×16 px solid `#6fc2ef` block, 6 px from corner |
| Bottom-right monogram | `▌RS` in JetBrains Mono 12 px `#505050`, 6 px from corner |
| Corner radius | 8 px |
| Theme name / author | `"Terminal"` / `"Rusty Solitaire"` |
## Generation pipeline — programmatic SVG via the existing
`resvg` stack
### Why this path (vs. external tooling or direct `tiny_skia`)
The codebase already ships an SVG-to-PNG rasteriser at
`solitaire_engine/src/assets/svg_loader.rs`:
- Public `rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result<Image, _>`
- Backed by `usvg` (parser) + `resvg` (renderer) + `tiny_skia`
(CPU pixmap)
- Bundled font db includes JetBrains-style mono (FiraMono — same
face the splash uses; close enough to JetBrains Mono for
rasterisation purposes, and identical to what the Bevy UI
consumes in the rest of the app)
- `RenderAssetUsages::default()` is the call-site convention here
This means: **generating new card PNGs is one new file
(`solitaire_engine/examples/card_face_generator.rs`) calling an
existing public function.** No new dependencies, no asset-pipeline
changes, no build-script machinery. Anyone who runs the example
gets bit-identical artwork.
The two alternatives are weaker:
- **External tool (Inkscape / Figma / hand-design)** — produces
one-off PNGs that can't be re-generated reproducibly without
re-opening the source files in a specific tool. Iteration cost
is high; design tweaks (e.g. "make the suit glyph 2 px larger")
require a designer-in-the-loop.
- **Direct `tiny_skia` painting calls** — bypasses SVG entirely,
but loses the readability of "open the SVG to see exactly what
the card looks like." Also reinvents primitives (rounded
rectangles, text layout) that `usvg` already handles.
### Output format
PNG, RGBA8 sRGB, **dimensions 256 × 384** (2:3 aspect, half the
default `SvgLoaderSettings` of 512 × 768).
Rationale: cards never exceed ~250 px wide on desktop windows
today, and 256 × 384 PNGs are ~6 KB each at this content density
(13.4 KB total for a full deck of 52 + 5 backs). The default 512 ×
768 is 2× what's needed and quadruples the on-disk asset weight.
The existing legacy PNGs are 512 × 768 — reducing the new ones
halves the runtime asset size.
## Lockstep migration — recommended order
Each step is a separate commit; the constraint is that **steps 4
and 5 must land in the same commit** (or at most adjacent commits
on the same branch) so the rendered output never diverges between
the two paths.
1. **(Done — this commit)** Land the migration plan doc.
2. **Land the SVG generator example.** New
`solitaire_engine/examples/card_face_generator.rs`. Output
goes to `assets/cards/faces/` and `assets/cards/backs/`. Run
once locally to seed the new artwork. The example file stays
in-tree as a regenerator for future tweaks.
3. **(Optional — can land separately)** Add a one-shot regression
test that re-runs the generator into a `tempdir` and compares
the resulting bytes against the on-disk artwork; pinning the
generator output prevents silent drift if `usvg`/`resvg` ever
tweak rendering. Skip if the test runtime cost is unacceptable.
4. **Land the new artwork** (PNG bytes from step 2 committed to
`assets/cards/`) **and** the constant migration in the *same
commit*:
- `CARD_FACE_COLOUR``Color::srgb(0.102, 0.102, 0.102)` (`#1a1a1a`)
- `RED_SUIT_COLOUR``Color::srgb(0.984, 0.624, 0.694)` (`#fb9fb1`)
- `BLACK_SUIT_COLOUR``Color::srgb(0.816, 0.816, 0.816)` (`#d0d0d0`)
- `CARD_FACE_COLOUR_RED_CBM``Color::srgb(0.435, 0.761, 0.937)` (`#6fc2ef`) — note this is now the colour-blind *suit* colour, not a face tint; semantics shift slightly.
- `card_back_colour(idx)` — re-author for the Terminal palette;
index 0 stays the canonical "Terminal" back from `design-system.md`.
5. **Test updates land in step 4's commit.** The pinning tests at
`card_plugin.rs` lines 1749, 1750, 1767, 1768, 2057, 2063,
2071, 2081 all assert against the old constants. New
assertions update in lockstep with the constant changes.
## CBM (color-blind mode) semantics shift — flag
The **legacy** `CARD_FACE_COLOUR_RED_CBM` was a *face tint* — red
suits got a light-blue background wash. The **Terminal** spec
moves CBM into the *suit colour* itself (red glyphs swap to cyan).
Step 4 will rename / repurpose this constant; it's not a 1:1
replacement.
Two options:
- **Rename + repurpose:** `CARD_FACE_COLOUR_RED_CBM`
`RED_SUIT_COLOUR_CBM`. Communicates the semantic shift in the
symbol name. Requires touching every callsite.
- **Keep the name, change the meaning:** less code churn but
worse for greppability — a future reader hitting the legacy
name will assume face-tint behaviour.
Recommendation: **rename**. The CBM swap is a one-frame operation
even if it touches every existing callsite (currently lines 642,
2071, 2081 per `grep -n CARD_FACE_COLOUR_RED_CBM`).
## Theme system — out of scope here
The card-theme system (`docs/CARD_PLAN.md`, `theme/plugin.rs`)
already supports user-supplied themes via `assets/themes/<theme>/`
SVG files rasterised by `svg_loader.rs`. The new Terminal artwork
is the **default theme**, not a new entry in the theme picker —
the theme system continues to overlay user themes on top of the
default at runtime.
If the next session wants to also ship Terminal as a *named theme
slot* (so a user can switch back to the legacy artwork via the
theme picker), that's an additive change after step 4 and lives
in `theme::plugin::apply_theme_to_card_image_set`.
## Test impact summary
`grep -n CARD_FACE_COLOUR\\b\|RED_SUIT_COLOUR\\b\|BLACK_SUIT_COLOUR\\b` in
`card_plugin.rs`:
- Line 17491750: red-suit text colour assertions (♥ + ♦).
- Line 17671768: black-suit text colour assertions (♠ + ♣).
- Line 2057, 2063: face-colour assertion in default mode.
- Line 2071, 2081: face-colour assertion in CBM.
The four suit-colour and two face-colour tests are **invariant
guards** — they exist precisely so a constant tweak surfaces here
rather than in a visual review. Step 4 updates each in lockstep
with the constant value change. No new test infrastructure
needed.
## Open questions to resolve before step 4
1. **Border colour conflict.** The spec (line 218) says "Border:
1 px solid in suit colour." The fallback path doesn't draw a
border today — it draws solid-colour sprites. Step 4 either:
(a) leaves the fallback as solid-colour squares (the test
environment doesn't visually validate borders anyway), or
(b) extends the fallback renderer to paint a 1 px outline.
Recommend (a) — fallback fidelity isn't load-bearing.
2. **Glyph rendering in the constant fallback.** The fallback
today doesn't render suit glyphs at all — it's a coloured
square. The spec's filled-vs-outlined glyph differentiation
only matters in the PNG path. No change to the constant
fallback for glyphs.
3. **High-contrast mode.** `design-system.md` line 274 mentions
a high-contrast accessibility mode (boosts foreground from
`#d0d0d0` to `#f5f5f5`, suit-red from `#fb9fb1` to `#ff8aa0`).
Not currently implemented anywhere; out of scope for this
migration but worth flagging for a future accessibility pass.
## Post-migration — what's still open
- **High-contrast mode** (above).
- **Reduced-motion mode** for card lift / drop transitions
(also a `design-system.md` accessibility item, separate from
artwork).
- **The 9 missing-plugin screens** (splash, challenge,
time-attack, weekly-goals, leaderboard, sync, level-up,
replay, radial-menu) per `project_ui_overhaul` memory still
need their plugin ports — separate from the cards arc.
## Sign-off criteria for "D closed"
D from the SESSION_HANDOFF Resume prompt is closed when **all of
the following hold simultaneously**:
- The 52 face PNGs + 5 back PNGs in `assets/cards/` are the
Terminal-aesthetic artwork (regeneratable via the example).
- The five `card_plugin` constants reflect the Terminal palette.
- All pinning tests pass against the new values.
- A human boots the game and sees Terminal cards (not white
cards). This sign-off needs a real `cargo run`, not just
`cargo test`.
+219
View File
@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=390, height=844, initial-scale=1.0" name="viewport"/>
<title>Challenge Mode Menu</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400;500;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"inverse-surface": "#e0e3e6",
"error-container": "#93000a",
"tertiary": "#f7c3ff",
"on-primary-container": "#004f6c",
"on-surface": "#e0e3e6",
"surface-dim": "#101417",
"surface-container-high": "#272a2d",
"surface-container-lowest": "#0b0f11",
"secondary-container": "#435401",
"suit-red": "#fb9fb1",
"on-error": "#690005",
"surface-container-low": "#181c1f",
"surface-variant": "#313538",
"surface-tint": "#7ed0fe",
"primary-container": "#6fc2ef",
"background": "#101417",
"primary": "#a1dcff",
"outline": "#505050",
"suit-black": "#d0d0d0",
"secondary-fixed": "#d5ec8c",
"surface-container": "#202020",
"on-tertiary-fixed": "#340043",
"on-tertiary-fixed-variant": "#653173",
"outline-variant": "#3f484e",
"on-surface-variant": "#bfc8cf",
"error": "#fb9fb1",
"on-primary-fixed": "#001e2c",
"highlight-celebration": "#e1a3ee",
"highlight-valid": "#acc267",
"suit-red-cb": "#6fc2ef",
"primary-fixed-dim": "#7ed0fe",
"tertiary-fixed-dim": "#f0b0fc",
"primary-fixed": "#c4e7ff",
"on-error-container": "#ffdad6",
"tertiary-container": "#e1a3ee",
"on-secondary": "#293500",
"on-tertiary": "#4c195b",
"on-background": "#e0e3e6",
"secondary-fixed-dim": "#bad073",
"secondary": "#bad073",
"inverse-primary": "#00668a",
"surface-bright": "#363a3d",
"surface": "#151515",
"on-tertiary-container": "#683476",
"on-secondary-fixed": "#161e00",
"inverse-on-surface": "#2d3134",
"warning": "#ddb26f",
"info": "#12cfc0",
"surface-container-highest": "#313538",
"on-primary-fixed-variant": "#004c69",
"tertiary-fixed": "#fbd7ff",
"on-secondary-fixed-variant": "#3c4d00",
"on-secondary-container": "#b2c86d",
"on-primary": "#003549"
},
"fontFamily": {
"mono": ["JetBrains Mono", "monospace"],
"label-caps": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"body-md": ["Inter"],
"card-rank": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"headline": ["JetBrains Mono"]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body {
background-color: #101417;
font-family: 'JetBrains Mono', monospace;
}
.retro-scanline {
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
background-size: 100% 2px, 3px 100%;
pointer-events: none;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="flex items-center justify-center min-h-screen text-on-background overflow-hidden">
<!-- Mobile Container (390x844) -->
<div class="relative w-[390px] h-[844px] bg-background flex flex-col overflow-hidden border border-outline-variant">
<!-- Status Bar -->
<div class="h-[32px] bg-surface-container flex items-center justify-between px-4 text-[11px] font-mono border-b border-outline-variant shrink-0">
<span class="text-suit-black">▌challenge.tsx</span>
<span class="text-[#a0a0a0]">LV 12 · UNLOCKED</span>
</div>
<!-- Header -->
<header class="h-[80px] px-margin-edge flex flex-col justify-center border-b border-outline-variant shrink-0">
<h1 class="text-[24px] font-bold leading-tight text-suit-black">CHALLENGE MODE</h1>
<p class="text-[12px] text-[#a0a0a0] mt-1">Curated puzzles · Beat par for bonus XP</p>
</header>
<!-- Stats Row -->
<div class="mx-margin-edge mt-4 bg-surface-container rounded-[4px] p-3 flex items-center justify-between border border-outline-variant shrink-0">
<div class="flex items-baseline gap-1">
<span class="text-[14px] font-bold text-suit-black">DONE 8/24</span>
<span class="text-[14px] font-bold text-highlight-celebration">(33%)</span>
</div>
<span class="text-outline-variant text-[14px]"></span>
<div class="text-[14px] font-bold text-suit-black">BEST AVG 03:42</div>
<span class="text-outline-variant text-[14px]"></span>
<div class="text-[14px] font-bold text-highlight-valid">+1240 XP</div>
</div>
<!-- Scrollable List Area -->
<div class="flex-1 overflow-y-auto px-margin-edge pt-4 space-y-3 pb-6">
<!-- Card 1 -->
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
<div class="w-[6px] h-full bg-warning"></div>
<div class="flex-1 flex items-center justify-between px-4">
<div class="flex flex-col">
<span class="text-[14px] font-bold text-suit-black">DEEP STACK</span>
<span class="text-[12px] text-on-surface-variant">Win with 0 stock · ★★★☆☆</span>
</div>
<div class="bg-highlight-valid px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
✓ DONE
</div>
</div>
</div>
<!-- Card 2 -->
<div class="h-[80px] bg-surface-container border border-primary rounded-[4px] flex relative overflow-hidden">
<div class="w-[6px] h-full bg-highlight-valid"></div>
<div class="flex-1 flex items-center justify-between px-4">
<div class="flex flex-col">
<span class="text-[14px] font-bold text-suit-black">SPEED RUN</span>
<span class="text-[12px] text-on-surface-variant">Win under 2:30 · ★★☆☆☆</span>
</div>
<div class="bg-primary px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
▶ ACTIVE
</div>
</div>
</div>
<!-- Card 3 -->
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
<div class="w-[6px] h-full bg-suit-red"></div>
<div class="flex-1 flex items-center justify-between px-4">
<div class="flex flex-col">
<span class="text-[14px] font-bold text-suit-black">NO UNDO</span>
<span class="text-[12px] text-on-surface-variant">Win without undo · ★★★★☆</span>
</div>
<div class="bg-primary px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
▶ ACTIVE
</div>
</div>
</div>
<!-- Card 4 -->
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
<div class="w-[6px] h-full bg-info"></div>
<div class="flex-1 flex items-center justify-between px-4">
<div class="flex flex-col">
<span class="text-[14px] font-bold text-suit-black">FOUR SUITS</span>
<span class="text-[12px] text-on-surface-variant">1 card per suit · ★☆☆☆☆</span>
</div>
<div class="bg-highlight-valid px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
✓ DONE
</div>
</div>
</div>
<!-- Card 5 (Locked) -->
<div class="h-[80px] bg-surface-container border border-outline-variant rounded-[4px] flex relative overflow-hidden opacity-60">
<div class="w-[6px] h-full bg-highlight-celebration"></div>
<div class="flex-1 flex items-center justify-between px-4">
<div class="flex flex-col">
<span class="text-[14px] font-bold text-suit-black">PERFECT RUN</span>
<span class="text-[12px] text-on-surface-variant">Below par moves · ★★★★★</span>
</div>
<div class="bg-outline px-2 py-1 rounded-[2px] text-on-surface text-[11px] font-bold">
🔒 LOCKED
</div>
</div>
</div>
<!-- Filler Graphic for retro feel -->
<div class="flex items-center justify-center py-4">
<div class="h-[1px] flex-1 bg-outline-variant"></div>
<span class="px-4 text-[10px] text-outline text-label-caps">END OF LIST</span>
<div class="h-[1px] flex-1 bg-outline-variant"></div>
</div>
</div>
<!-- Shared Component: Terminal Context (Used as Footer) -->
<div class="h-[24px] bg-surface px-4 flex items-center justify-between text-[10px] font-mono border-t border-outline-variant shrink-0">
<div class="flex items-center gap-2">
<span class="text-primary">▌ NORMAL</span>
<span class="text-outline"></span>
<span class="text-on-surface-variant uppercase tracking-widest">challenge</span>
</div>
<div class="text-[#a0a0a0] flex items-center gap-3">
<span>[ENTER] select</span>
<span>[F] filter</span>
<span class="text-suit-red">[ESC] back</span>
</div>
</div>
<!-- Retro Scanline Overlay -->
<div class="absolute inset-0 retro-scanline z-50"></div>
</div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

+258
View File
@@ -0,0 +1,258 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=390, height=844, initial-scale=1.0" name="viewport"/>
<title>Rusty Solitaire - Daily Challenge</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&amp;family=Inter:wght@400;500;600&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body {
background-color: #101417;
color: #e0e3e6;
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
}
.scanline-bg {
background: linear-gradient(to bottom, transparent 50%, rgba(26, 26, 26, 0.5) 50%);
background-size: 100% 4px;
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-secondary-fixed": "#161e00",
"on-error": "#690005",
"on-primary-fixed": "#001e2c",
"tertiary": "#f7c3ff",
"secondary-fixed-dim": "#bad073",
"primary-container": "#6fc2ef",
"surface-dim": "#101417",
"surface-variant": "#313538",
"on-error-container": "#ffdad6",
"warning": "#ddb26f",
"on-surface": "#e0e3e6",
"inverse-on-surface": "#2d3134",
"surface-tint": "#7ed0fe",
"error-container": "#93000a",
"on-tertiary": "#4c195b",
"info": "#12cfc0",
"tertiary-fixed": "#fbd7ff",
"tertiary-fixed-dim": "#f0b0fc",
"primary": "#a1dcff",
"on-primary": "#003549",
"inverse-surface": "#e0e3e6",
"highlight-valid": "#acc267",
"surface-container-low": "#181c1f",
"surface-container": "#1c2023",
"on-surface-variant": "#bfc8cf",
"secondary-container": "#435401",
"error": "#fb9fb1",
"surface": "#151515",
"primary-fixed": "#c4e7ff",
"outline": "#505050",
"surface-container-highest": "#313538",
"on-secondary": "#293500",
"on-primary-container": "#004f6c",
"secondary-fixed": "#d5ec8c",
"background": "#101417",
"surface-container-high": "#272a2d",
"suit-red-cb": "#6fc2ef",
"surface-container-lowest": "#0b0f11",
"suit-red": "#fb9fb1",
"on-secondary-container": "#b2c86d",
"outline-variant": "#3f484e",
"on-secondary-fixed-variant": "#3c4d00",
"inverse-primary": "#00668a",
"surface-bright": "#363a3d",
"primary-fixed-dim": "#7ed0fe",
"tertiary-container": "#e1a3ee",
"on-background": "#e0e3e6",
"on-tertiary-container": "#683476",
"suit-black": "#d0d0d0",
"on-primary-fixed-variant": "#004c69",
"secondary": "#bad073",
"on-tertiary-fixed-variant": "#653173",
"on-tertiary-fixed": "#340043",
"highlight-celebration": "#e1a3ee"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"gutter-card": "0.375rem",
"stack-overlap": "2rem",
"margin-edge": "1rem",
"action-bar-height": "64px",
"touch-target-min": "48dp"
},
"fontFamily": {
"label-caps": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"card-rank": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"body-md": ["Inter"],
"headline": ["JetBrains Mono"]
},
"fontSize": {
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="flex flex-col min-h-screen max-w-[390px] mx-auto overflow-hidden shadow-2xl border-x border-outline">
<!-- 1. Status Bar -->
<div class="h-[32px] bg-[#202020] flex items-center justify-between px-margin-edge border-b border-outline">
<span class="font-hud-timer text-[12px] text-on-surface-variant">▌daily/2024-127.json</span>
<div class="bg-warning/10 border border-warning px-2 py-0.5 rounded-sm">
<span class="font-hud-timer text-[11px] text-warning font-bold tracking-tighter">EXPIRES 11:42:30</span>
</div>
</div>
<!-- Main Content Canvas -->
<main class="flex-1 p-margin-edge space-y-4 overflow-y-auto pb-8">
<!-- 2. Header Card -->
<section class="h-[130px] bg-[#1a1a1a] border border-[#353535] rounded-lg p-4 flex flex-col justify-between">
<div class="flex flex-col">
<span class="font-headline font-bold text-[24px] text-suit-black leading-none">MAY 07 · 2026</span>
<span class="font-headline font-extrabold text-[32px] text-highlight-valid -tracking-[0.01em] leading-tight">#2024-127</span>
</div>
<span class="font-label-caps text-[11px] text-on-surface-variant/70">DRAW-3 · DIFFICULTY ★★★☆☆ · PAR 04:30</span>
</section>
<!-- 3. Primary CTA -->
<button class="w-full h-[64px] bg-primary-container text-surface font-headline font-bold text-[14px] uppercase tracking-wider rounded-lg active:scale-95 transition-transform duration-80 flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-[18px]">play_arrow</span>
ATTEMPT TODAY'S SEED
</button>
<!-- 4. Your Attempts Card -->
<section class="h-[96px] bg-[#202020] rounded-lg p-4 flex flex-col justify-between">
<span class="font-label-caps text-[11px] text-on-surface-variant/60 uppercase">YOUR ATTEMPTS</span>
<div class="flex justify-between items-end">
<div class="flex flex-col">
<span class="font-hud-score text-[16px] text-suit-black">BEST 04:12</span>
<div class="flex items-center gap-2 mt-1">
<span class="bg-warning text-surface text-[10px] font-bold px-1.5 py-0.5 rounded-sm">WIN</span>
<span class="font-label-caps text-[11px] text-warning">RANK 17/2843</span>
</div>
</div>
<span class="font-hud-timer text-[13px] text-error mb-1">LAST: FAILED at move 47</span>
</div>
</section>
<!-- 5. Leaderboard Card -->
<section class="bg-[#202020] rounded-lg p-4 flex flex-col flex-grow">
<span class="font-label-caps text-[11px] text-on-surface-variant/60 uppercase mb-4">TOP TODAY · 2,843 PLAYERS</span>
<div class="space-y-0 divide-y divide-[#353535]">
<!-- Row 1 -->
<div class="h-[32px] flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="w-5 h-5 flex items-center justify-center bg-warning text-surface text-[10px] font-bold rounded-full">01</span>
<span class="font-hud-timer text-[14px]">swift_jaguar</span>
</div>
<span class="font-hud-timer text-[14px] text-on-surface-variant">02:47</span>
</div>
<!-- Row 2 -->
<div class="h-[32px] flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="w-5 h-5 flex items-center justify-center bg-[#a0a0a0] text-surface text-[10px] font-bold rounded-full">02</span>
<span class="font-hud-timer text-[14px]">base16_fan</span>
</div>
<span class="font-hud-timer text-[14px] text-on-surface-variant">03:12</span>
</div>
<!-- Row 3 -->
<div class="h-[32px] flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="w-5 h-5 flex items-center justify-center bg-[#7a5d3b] text-surface text-[10px] font-bold rounded-full">03</span>
<span class="font-hud-timer text-[14px]">cli_player</span>
</div>
<span class="font-hud-timer text-[14px] text-on-surface-variant">03:54</span>
</div>
<!-- Row 4 -->
<div class="h-[32px] flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="w-5 h-5 flex items-center justify-center bg-[#353535] text-on-surface-variant text-[10px] font-bold rounded-full">04</span>
<span class="font-hud-timer text-[14px]">tablejockey</span>
</div>
<span class="font-hud-timer text-[14px] text-on-surface-variant">04:01</span>
</div>
<!-- Row 5 -->
<div class="h-[32px] flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="w-5 h-5 flex items-center justify-center bg-[#353535] text-on-surface-variant text-[10px] font-bold rounded-full">05</span>
<span class="font-hud-timer text-[14px]">vim_motions</span>
</div>
<span class="font-hud-timer text-[14px] text-on-surface-variant">04:05</span>
</div>
<!-- Row 17 (YOU) -->
<div class="h-[36px] flex items-center justify-between bg-primary-container/10 -mx-4 px-4 border-y border-primary-container/20">
<div class="flex items-center gap-3">
<span class="w-5 h-5 flex items-center justify-center bg-primary-container text-surface text-[10px] font-bold rounded-full">17</span>
<span class="font-hud-timer text-[14px] text-primary-container font-bold">(YOU) anonymous</span>
</div>
<span class="font-hud-timer text-[14px] text-primary-container font-bold">04:12</span>
</div>
</div>
<div class="mt-4 flex-1 border-t border-[#353535] pt-4 flex flex-col items-center justify-center opacity-30 select-none">
<span class="material-symbols-outlined text-[48px]">terminal</span>
<span class="font-label-caps text-[10px] mt-2">END OF VISIBLE LOG</span>
</div>
</section>
</main>
<!-- 6. Footer Navigation -->
<footer class="h-[24px] bg-background border-t border-outline flex items-center justify-between px-3">
<div class="flex items-center gap-2">
<span class="font-label-caps text-[10px] text-on-surface-variant">▌ NORMAL │ daily</span>
</div>
<div class="flex items-center gap-3">
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[ENTER]</span> attempt</span>
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[L]</span> full leaderboard</span>
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[ESC]</span> back</span>
</div>
</footer>
<!-- Shared Component Shell Rendering Logic -->
<header class="w-full top-0 sticky bg-background border-b border-outline flex justify-between items-center px-margin-edge h-action-bar-height hidden">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-primary">terminal</span>
<h1 class="font-headline text-headline text-primary uppercase tracking-widest">RUSTY SOLITAIRE</h1>
</div>
<span class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors duration-120 cursor-pointer">settings</span>
</header>
<nav class="fixed bottom-0 w-full h-action-bar-height z-50 bg-surface-container border-t border-outline flex justify-around items-center px-2 hidden">
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
<span class="material-symbols-outlined">refresh</span>
<span class="font-label-caps text-label-caps">DEAL</span>
</div>
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
<span class="material-symbols-outlined">undo</span>
<span class="font-label-caps text-label-caps">UNDO</span>
</div>
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
<span class="material-symbols-outlined">lightbulb</span>
<span class="font-label-caps text-label-caps">HINT</span>
</div>
<div class="flex flex-col items-center justify-center text-primary dark:text-primary-fixed-dim hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
<span class="material-symbols-outlined">menu</span>
<span class="font-label-caps text-label-caps">MENU</span>
</div>
</nav>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+280
View File
@@ -0,0 +1,280 @@
---
name: Terminal
colors:
surface: '#151515'
surface-dim: '#0d0d0d'
surface-bright: '#2a2a2a'
surface-container-lowest: '#0a0a0a'
surface-container-low: '#1a1a1a'
surface-container: '#202020'
surface-container-high: '#2a2a2a'
surface-container-highest: '#353535'
on-surface: '#d0d0d0'
on-surface-variant: '#a0a0a0'
inverse-surface: '#d0d0d0'
inverse-on-surface: '#151515'
outline: '#505050'
outline-variant: '#353535'
surface-tint: '#a54242'
primary: '#a54242'
on-primary: '#151515'
primary-container: '#3a1f1f'
on-primary-container: '#d5a8a8'
inverse-primary: '#993e3e'
secondary: '#acc267'
on-secondary: '#151515'
secondary-container: '#2a3320'
on-secondary-container: '#c5d585'
tertiary: '#e1a3ee'
on-tertiary: '#151515'
tertiary-container: '#3a2a40'
on-tertiary-container: '#eec3f5'
error: '#fb9fb1'
on-error: '#151515'
error-container: '#4a2530'
on-error-container: '#fdc3ce'
background: '#151515'
on-background: '#d0d0d0'
surface-variant: '#353535'
suit-red: '#fb9fb1'
suit-black: '#d0d0d0'
suit-red-cb: '#acc267'
highlight-valid: '#acc267'
highlight-celebration: '#e1a3ee'
highlight-warning: '#ddb26f'
highlight-info: '#12cfc0'
typography:
hud-score:
fontFamily: JetBrains Mono
fontSize: 24px
fontWeight: '700'
lineHeight: 32px
letterSpacing: '-0.02em'
hud-timer:
fontFamily: JetBrains Mono
fontSize: 16px
fontWeight: '400'
lineHeight: 24px
card-rank:
fontFamily: JetBrains Mono
fontSize: 18px
fontWeight: '700'
lineHeight: 18px
body-md:
fontFamily: Inter
fontSize: 16px
fontWeight: '400'
lineHeight: 24px
label-caps:
fontFamily: JetBrains Mono
fontSize: 12px
fontWeight: '500'
lineHeight: 16px
letterSpacing: '0.08em'
headline:
fontFamily: JetBrains Mono
fontSize: 28px
fontWeight: '700'
lineHeight: 32px
letterSpacing: '-0.01em'
rounded:
sm: 0.125rem
DEFAULT: 0.25rem
md: 0.5rem
lg: 0.75rem
xl: 1rem
full: 9999px
spacing:
margin-edge: 1rem
gutter-card: 0.375rem
stack-overlap: 2rem
touch-target-min: 48dp
---
## Brand & Style
The "Terminal" design system replaces the previous "Premium Solitaire" calm-indie aesthetic with a **retro-terminal / synthwave** identity. The intent is the visual confidence of a well-tuned terminal emulator (think Berkeley Mono dotfiles, base16-eighties, CRT phosphor): monospaced, dense, legible, snappy. It is *not* casino-glitz, *not* skeuomorphic felt, and *not* whimsical.
The personality is **technical, deliberate, slightly playful**. Cards are flat with thin colored strokes; the HUD reads like a status bar; modals look like terminal panes. Motion is short and snap-easing — no bouncy springs. Long-session calm is preserved by keeping the chroma low and reserving saturated accents for *meaning* (CTAs, feedback, celebrations) rather than decoration.
Influences: base16-eighties (Chris Kempson), Berkeley Mono, Vim/Neovim status lines, the iA Writer aesthetic, classic CRT phosphor with no chromatic aberration.
## Palette
The palette is base16-eighties — a 16-slot terminal palette where indices 0007 form a monochrome ramp and 080F provide saturated accents. We map base16 slots to Material Design 3 token roles below.
### Source palette (base16-eighties)
| Slot | Hex | Role |
|---|---|---|
| base00 | `#151515` | background |
| base01 | `#202020` | surface-container |
| base02 | `#303030` | line-highlight (subtle) |
| base03 | `#505050` | outline / muted text |
| base04 | `#b0b0b0` | secondary text |
| base05 | `#d0d0d0` | foreground / on-surface |
| base06 | `#e0e0e0` | bright text |
| base07 | `#f5f5f5` | brightest highlight |
| base08 | `#fb9fb1` | red — used for `error`, `suit-red` |
| base09 | `#ddb26f` | orange — used for warning chips |
| base0A | `#acc267` | yellow/lime — used for `highlight-valid` (drag targets, valid moves) |
| base0B | `#12cfc0` | green/teal — used for `highlight-info` (toasts, neutral status) |
| base0C | `#6fc2ef` | cyan/sky — historically the primary CTA; now reserved for ad-hoc accents only |
| base0D | `#6fc2ef` | (alias) |
| base08 (project) | `#a54242` | brick red — primary CTA, focus ring, `selection` (project-specific extension; the base16-eighties `base08` slot is `#fb9fb1` pink which we keep as `error`/`suit-red`) |
| `suit-red-cb` slot | `#acc267` | lime — color-blind-mode swap for red suits (was `#6fc2ef` cyan before the 2026-05-08 primary-accent swap; lime is the next-best non-red base16-eighties accent) |
| base0E | `#e1a3ee` | violet — used for celebration (level-up, achievement unlock) |
| base0F | `#fb9fb1` | (alias) |
### Semantic assignments
- **CTA / Primary action**: brick red `#a54242`. Reserved for "Play," "New Game," "Save," "Resume," and the focus ring on selected cards. Never used decoratively. (Was cyan `#6fc2ef` before the 2026-05-08 swap.)
- **Valid-move / drag-target highlight**: lime `#acc267`. Reserved for in-game feedback only. Never appears in chrome.
- **Celebration**: lavender `#e1a3ee`. Used for level-up flashes, achievement unlock cards, and the daily-streak chip when the streak is active. Quiet otherwise.
- **Warning / soft alert**: gold `#ddb26f`. Used for "challenge expires in N minutes" chips, sync-pending status, and the daily-seed countdown.
- **Info**: teal `#12cfc0`. Used for neutral system toasts and the sync-connected indicator.
- **Error**: pink `#fb9fb1`. Used for sync conflict, server unreachable, invalid move shake.
## Suit Colors
**Two-color traditional mapping**, with mandatory color-blind support:
| Suit | Default | Color-blind mode | Glyph differentiation |
|---|---|---|---|
| Hearts | `#fb9fb1` (pink) | `#acc267` (lime) | Solid filled glyph |
| Diamonds | `#fb9fb1` (pink) | `#acc267` (lime) | **Outlined glyph (1.5px stroke)** |
| Spades | `#d0d0d0` (foreground) | `#d0d0d0` | Solid filled glyph |
| Clubs | `#d0d0d0` (foreground) | `#d0d0d0` | **Outlined glyph (1.5px stroke)** |
The outlined-glyph treatment is the **primary** differentiation mechanism. Color is supplementary. This means a player viewing the game on a monochrome display, or with severe red-green deficiency, can still distinguish all four suits without context. This is a hard requirement, not an optional setting.
The "color-blind mode" toggle in Settings only swaps red→lime; it does not turn the outlined glyphs on or off, because outlined glyphs are always on. (Was red→cyan before the 2026-05-08 primary-accent swap; CBM moved to lime to stay hue-distinct from the new red-family primary.)
## Typography
**Monospace-forward, dual-font system.**
- **JetBrains Mono** is used for: HUD (score, timer, moves), card rank/value text, all labels, all headlines, all numerals anywhere in the app, and any chip-style component. This is the dominant face.
- **Inter** is used only for: long-form body copy (Help screen, Settings descriptions, achievement tooltips, onboarding copy). It is the *exception*, not the default.
Weights: 400 regular, 500 medium for labels, 700 bold for HUD numbers and headlines. No 600 / no italics anywhere — the terminal aesthetic doesn't have them.
Letter spacing: tight (`-0.02em`) on HUD score for visual mass; wide (`+0.08em`) on uppercase labels for readability at 12px. Body uses default (0).
HUD numbers must use **tabular figures** (`font-feature-settings: 'tnum'`) so the timer and score don't reflow as digits change.
## Layout & Spacing
Optimized for **Android portrait, 390×844 (Pixel 6 baseline), API 34**.
- **Margins**: 16px (1rem) edge safety margin. *Tighter than the previous system's 24px.* Eighties palettes are dense by nature; over-padding fights the aesthetic.
- **Tableau**: 7-column layout, 32px (2rem) vertical card overlap. Tighter than before to fit a longer cascade on phone screens.
- **HUD position**: top of screen, in the system safe area. Bottom 64px holds the action bar (Undo / Hint / New Game / Auto-complete). Action bar is **always visible** in-game — no hover-fade — because there is no hover on touch.
- **Touch target minimum**: 48dp on all interactive elements. Cards in the tableau may be smaller visually but use a 48dp invisible hit area centered on the visible glyph.
## Elevation & Depth
Depth is created through **tonal layering and 1px outlines**, not blur shadows. (Synthwave-flat, not Material-soft.)
- **Level 0 (Background)**: the `#151515` base canvas.
- **Level 1 (Tableau slots, empty piles)**: 1px dashed outline in `#353535`. Empty foundations show a faint suit glyph at 12% opacity inside the outline.
- **Level 2 (Cards at rest)**: solid `#1a1a1a` fill, 1px solid border in the suit color (so the suit is detectable at a glance even if the card is partially obscured).
- **Level 3 (Active / dragged card)**: same border, but glow effect: 0 0 12px of `#a54242` at 40% opacity. **No scale transform** — flatness preserved. Z-index lifts above siblings.
- **Modals**: full-screen with backdrop `#151515` at 95% opacity (just enough to dim the table without blurring it). Modal panel is `#202020` with a 1px `#505050` border — like a terminal pane.
- **Toasts**: bottom of screen, `#202020` fill, 1px border in the toast's accent color (info=teal, warning=gold, error=pink, celebration=lavender). 16px monospaced caption.
No `box-shadow` is used anywhere. **All depth is achieved with borders and tonal value.** This is a hard constraint.
## Shapes
The shape language is **soft-rounded but tight**:
- **Cards**: `rounded-md` (8px) — slightly less rounded than the previous system's 16px to read more "technical."
- **Buttons / chips / inputs**: `rounded` (4px) default, `rounded-sm` (2px) for the smallest chips.
- **Modals / sheets**: `rounded-lg` (12px).
- **Avatars / circular indicators**: `rounded-full`.
- **Card-back pattern corners**: matches the card's `rounded-md`.
Selection highlights use a **2px inset stroke** in `#a54242` following the host shape's corner radius. Never an outer stroke — the outer stroke is reserved for the suit-color hairline.
## Motion
**Snappy, no spring.** All transitions use `ease-out` with a 120ms duration unless specified.
- Card lift (start drag): 80ms.
- Card place (drop): 120ms with a 16ms holdframe (no bounce).
- Modal enter: 200ms ease-out, fade + 8px translate-up.
- Modal exit: 120ms ease-in, fade only.
- Selection ring appear: 80ms.
- Win-summary stat reveal: 60ms each, staggered 40ms.
- HUD number tick: instant (no transition) — terminal counters don't ease.
**Optional CRT effect**: a 1-frame scanline sweep across the screen on game-state transitions (start, win, restart). User-toggleable in Settings. Off by default.
## Components
### Game Cards
Flat face design.
- Background: `#1a1a1a`
- Border: 1px solid in suit color (pink for hearts/diamonds, foreground gray for spades/clubs)
- Top-left: rank in JetBrains Mono Bold 18px + small suit glyph (10px)
- Bottom-right: large suit glyph (32px), upright (same orientation as the top-left small glyph — single-orientation digital play does not benefit from the traditional 180° inverted-corner indicator)
- Corner radius: 8px
- Suit differentiation: hearts and spades have **filled** glyphs; diamonds and clubs have **outlined** glyphs (1.5px stroke)
### Card Back ("Terminal" theme)
- Theme name: `"Terminal"`
- Author: `"Rusty Solitaire"`
- Background: `#151515`
- Pattern: horizontal scanlines at 2px pitch in `#1a1a1a` (1px line, 1px gap), full bleed
- Border: 1px solid `#353535`
- Top-left badge: a 12×16px solid `#a54242` block (the "terminal cursor"), 6px from the corner
- Bottom-right monogram: the characters `▌RS` in JetBrains Mono 12px, color `#505050`, 6px from the corner
- Corner radius: 8px (matches face)
### Primary Buttons
Solid `#a54242` fill, `#151515` text, JetBrains Mono Medium 14px uppercase with `+0.08em` tracking. 4px corner radius. Pressed state: darken to `#7a3030`. Disabled: `#353535` fill, `#505050` text.
### Secondary Buttons
Transparent fill, 1px `#505050` border, `#d0d0d0` text. Hover/press: border becomes `#a54242`, text becomes `#a54242`.
### HUD Chips
`#202020` fill, no border, 4px radius. Monospaced 16px text. Score chip pulses to `#acc267` for 200ms when score increases.
### Drag Targets
When a card is being dragged over a valid pile, the pile's empty-slot dashed outline becomes:
- Solid 1px in `#acc267`
- Plus a 0 0 8px outer glow in `#acc267` at 30% opacity
This is the *only* place glow effects appear in the system.
### Modals
Full-screen backdrop at 95% opacity. Centered panel: `#202020` fill, 1px `#505050` border, 12px corner radius. Title bar shows the screen name in monospaced 14px, color `#a0a0a0`, with a single `▌` cursor character prefix to reinforce the terminal pane motif.
### Navigation Bar
Fixed at the bottom of in-game screens. Height: 64px. `#202020` fill, 1px top border in `#353535`. Four icon buttons: Undo / Hint / New / Auto-complete. Icons: 24px, 1.5px stroke weight, color `#d0d0d0`. Active/pressed: icon color `#a54242`.
### Status / Sync Indicator
Top-right corner of the HUD: a 6px circular dot.
- Connected & synced: `#12cfc0`
- Pending: `#ddb26f` (pulsing 1.5s)
- Error: `#fb9fb1` (steady)
- Offline: `#505050`
## Accessibility
1. **Color-blind mode** (Settings → Gameplay): swaps red suits' default `#fb9fb1` for `#acc267` (lime). Outlined-glyph differentiation remains active in *all* modes.
2. **High-contrast mode** (Settings → Gameplay): boosts on-surface from `#d0d0d0` to `#f5f5f5`, outline from `#505050` to `#a0a0a0`, suit-red from `#fb9fb1` to `#ff8aa0`.
3. **Reduce-motion mode** (Settings → Gameplay): disables card-lift transition (instant z-lift), disables CRT scanline effect, disables the warning-chip pulse animation.
4. **Tabular figures** are mandatory for any number that updates live (timer, score, moves) so they don't reflow.
5. **Touch targets** are 48dp minimum even when the visual element is smaller.
6. **Text contrast**: all body text on background passes WCAG AA at minimum (`#d0d0d0` on `#151515` = 9.5:1; `#a0a0a0` on `#151515` = 5.7:1).
+283
View File
@@ -0,0 +1,283 @@
# Terminal — Desktop Adaptation Spec
> **Why this exists.** The 24 mockups in this directory are mobile
> (390 × 844 logical, iPhone 14 Pro frame) with one exception
> (`home-menu-desktop.html`). The Stitch project that produced them
> is named "Solitaire Quest *Mobile* Redesign" — the mobile-first
> framing was deliberate when the new Android target opened, but
> desktop is still the primary delivery surface. Porting the mobile
> mockups 1:1 would land a 390-px-wide column floating in the middle
> of an 1800 × 1100 window. This file is the rules-based desktop
> companion — apply these adaptations whenever you port a Bevy
> plugin against a mobile mockup in this directory.
## Status
* **Token system.** All tokens (palette, type scale, spacing,
radii, motion) in `design-system.md` are layout-agnostic and
apply unchanged on both targets. Do **not** introduce desktop-
specific token variants — adapt geometry, not tokens.
* **Already adapted in code.** v0.20.0's port is layout-agnostic
(modal scaffold, toasts, table chrome, card chrome, gameplay-
feedback, splash cursor). Those surfaces already adapt
correctly because their Bevy UI nodes use flex / percent /
stretch sizing rather than fixed pixel widths from the
mockups.
* **Not yet adapted in code.** Any future plugin port that
copies layout from a mobile mockup must apply the rules below.
## Viewport assumptions
| Range | Width × height | Source |
|---|---|---|
| Mobile target | 390 × 844 | iPhone 14 Pro logical, Stitch mockup canvas |
| Desktop minimum | 1024 × 600 | Smaller windows degrade to mobile rules |
| Desktop default | ~70 % of monitor | `apply_smart_default_window_size` (since v0.19.0) |
| Desktop typical | 1600 × 900 to 2560 × 1440 | The range we tune for |
| Desktop max | 3840 × 2160 | 4K, with HiDPI scaling already applied |
The "smart default" sizer means a 1080p monitor opens a ~1344 × 756
window, a 1440p monitor opens ~1792 × 1008, a 4K monitor opens
~2688 × 1512. Tune for the 16002400 width band as the centre of
the distribution; below 1024 width, fall back to the mobile rules
verbatim.
## Universal adaptation rules
Apply these to every screen unless the per-screen section
overrides them.
### 1. Edge margins
| Mobile | Desktop |
|---|---|
| `margin-edge: 16px` (`SPACE_4`) | `SPACE_5` (24 px) for windows < 1440 wide; `SPACE_6` (32 px) for 14402400; `SPACE_7` (48 px) for ≥ 2400 |
Engine: drive from `LayoutResource` based on `Window` size, not a
constant.
### 2. Modal max-width
| Mobile | Desktop |
|---|---|
| `100% - 2 × edge-margin` | `min(720 px, 50 % of viewport)` |
The 720 px cap is already in `ui_modal::spawn_modal`. No code
change needed; this rule documents *why* it's there.
### 3. Vertical content stacks
A mobile screen often stacks `Header → Body → Footer` vertically
to fit a tall narrow column. On desktop, prefer horizontal
distribution where the content allows:
* **Header rows that stack vertically on mobile** (title above
count above timer) → keep them in one horizontal row on
desktop.
* **Two-column flex layouts** (e.g. Settings rows: label left,
control right) — already work on both targets; no change.
* **Cards stacking with `mt-48`-style fixed gaps** — replace with
flex / percent gaps so the layout breathes.
### 4. Touch-target minimums
Mobile spec mandates 48 dp minimum touch targets. Desktop has no
such floor (mouse precision is finer), but **don't shrink below
mobile's 48 px** for primary actions — keyboard / gamepad focus
rings still need a visible target.
Secondary controls (chip-style toggles, hotkey hints, etc.) can
shrink to `TYPE_BODY` (14 px) text + `SPACE_3` (12 px) padding on
desktop where they were larger on mobile.
### 5. Bottom-anchored elements
Mobile mockups often anchor key controls (action bar, primary CTA,
toast position) to the bottom of the viewport for thumb reach.
Desktop has no thumb-reach concern:
* **Toasts** — keep bottom-anchored (already done in `a137607`),
the design language is consistent across targets and the
bottom is still the least-disruptive overlay zone.
* **Action bars** — top of viewport on desktop unless the
per-screen section says otherwise. The HUD already sits on
top.
* **Single primary CTA** — modals already right-align in the
actions row; no change.
### 6. Typography rungs unchanged
Do **not** shift `TYPE_*` tokens up a rung for desktop. The
spec's 14 / 18 / 26 / 40 progression is already calibrated for
the desktop reading distance (6090 cm). Mobile uses the same
rungs at a closer reading distance (3040 cm); same physical
angular size on the eye.
### 7. Hotkey hints become full strings
Mobile cells like `▌Esc` — the cursor block plus key letter — can
expand to `[Esc] cancel` style on desktop where horizontal
real-estate is cheap. Drives discoverability of keyboard-only
flows. Optional; only apply where horizontal space exists.
## Per-screen adaptation rules
### Game Table
Mockup: `game-table-mobile.html` (390 × 844).
| Element | Mobile | Desktop |
|---|---|---|
| HUD band | full width, 56 px tall | full width, 48 px tall |
| Foundation row | 4 piles centred, fan-tight | 4 piles centred, **gutter doubled** so the row fills ~50 % of viewport width |
| Stock + waste | left of foundations, stacked | left of foundations, **horizontal pair**: stock on the left, waste to its immediate right (the mobile vertical pair feels cramped on a wide canvas) |
| Tableau row | 7 columns, 4 % gutter | 7 columns, **6 % gutter**, total tableau block ≤ 70 % viewport width |
| Card aspect | 2 : 3 (already in `Layout::card_size`) | unchanged — card aspect is domain |
| Tableau fan | `TABLEAU_FAN_FRAC = 0.25` | unchanged — fan is in card-height units, not viewport units |
| Drag-shadow offset | small | unchanged — pinned to 0 alpha under Terminal anyway |
**Engine impact:** `solitaire_engine/src/layout.rs::compute_layout`
already drives most of this from `Window::size()`. The mobile vs.
desktop difference is the gutter percentages — bake desktop
gutters when window width ≥ 1024.
### Win Summary
Mockup: `win-summary-mobile.html` (390 × 858).
| Element | Mobile | Desktop |
|---|---|---|
| Modal width | 100 % 2 × edge | **`min(720 px, 50 % viewport)`** (already done by `ui_modal`) |
| Score row | stacked vertically (line per metric) | **3-column grid**: Score / Time / Moves in one row, breakdown rows below in single-line per row |
| Action buttons | full-width stacked (Play Again, Continue, Stats) | **right-aligned action row** — the existing `spawn_modal_actions` already does this on both targets |
**Engine impact:** `solitaire_engine/src/win_summary_plugin.rs`. The
score-breakdown-stagger animation (`MOTION_SCORE_BREAKDOWN_*`) is
unchanged across targets.
### Settings
Mockup: `settings-mobile.html` (390 × 4330 — long scroll).
| Element | Mobile | Desktop |
|---|---|---|
| Modal width | 100 % 2 × edge | `min(720 px, 50 % viewport)` |
| Sections | full-width labels above stacked controls | **section labels left, control widget right** — already the engine's pattern; no change |
| Long page | scroll the whole modal | **two-column layout**: nav (sections list) on left ~30 %, current section on right ~70 %. Reduces scroll distance on desktop |
| Sliders | full-width on mobile | cap at 320 px on desktop |
**Engine impact:** if a desktop port wants the two-column nav, it's
a `settings_plugin` rewrite. Keep the existing single-column
stacked-modal layout for now — it works on both targets and the
two-column variant is a polish item, not a blocker.
### Help & Controls
Mockup: `help-mobile.html` (390 × 2544).
| Element | Mobile | Desktop |
|---|---|---|
| Modal width | 100 % 2 × edge | `min(720 px, 50 % viewport)` |
| Section list | one column of `Heading → 2-col rows` | **two columns of section blocks** for windows ≥ 1280 wide; halves vertical scroll distance |
| Hotkey rows | `key | description` 2-col flex | unchanged; 2-col already adapts |
**Engine impact:** `help_plugin`. Single-column on mobile, 2-col
on desktop windows ≥ 1280 wide is a flex-wrap option.
### Pause Menu
Mockup: `pause-menu-mobile.html` (390 × 1768).
Already a small modal; no significant geometry change. Modal
already uses `ui_modal::spawn_modal` which caps width and centres.
No desktop-specific rule.
### Home Menu
Mockup: `home-menu-mobile.html` and `home-menu-desktop.html`
(both already in this directory — desktop variant is the
authoritative reference).
The desktop mockup already specifies the layout. Cross-check it
against the mobile version when porting; differences are
deliberate (more horizontal real-estate, larger primary CTA, the
secondary actions row).
### Splash
Mockup: `splash-mobile.html` (390 × 844).
| Element | Mobile | Desktop |
|---|---|---|
| Full-screen overlay | `inset-0` | unchanged — splash always covers the viewport |
| Cursor block (`▌`) | 96 px JetBrains Mono | unchanged — already done in `cdcadda`. The 96 px size scales fine on desktop because the splash is a brand beat, not a layout-driven element |
| Title `RUSTY SOLITAIRE` | 32 px | scale to 40 px (`TYPE_DISPLAY`) on desktop |
| Subtitle `TERMINAL EDITION` | 12 px | unchanged |
| Boot log lines | 70 % width column | cap at 480 px so the column doesn't stretch on a wide window |
| Progress bar | 100 % 2 × edge | cap at 720 px |
| Palette swatch row + version footer | bottom-anchored | unchanged; bottom-anchor still reads correctly on desktop |
**Engine impact:** `splash_plugin` already has the cursor block
(`cdcadda`). The boot log / progress bar / palette swatch rows
are the next polish increment when option D is picked up.
### Stats
Mockup: `stats-mobile.html` (390 × 2624).
| Element | Mobile | Desktop |
|---|---|---|
| Modal width | 100 % 2 × edge | `min(720 px, 50 % viewport)` |
| Big-number cards | 2 × 2 grid | **4 × 1 row** for windows ≥ 1024 wide (the four headline metrics fit in a single horizontal row at desktop scale) |
| Latest-win caption | full-width line | unchanged |
| Replay clip / share row | full-width row | unchanged |
### Profile / Achievements / Theme Picker / Daily Challenge
These follow the **standard modal pattern** (`spawn_modal` with
header / body / actions). They already work on desktop because
`ui_modal` handles modal-width capping. Per-screen tweaks are
small and listed below; no structural changes:
* **Profile** — avatar + level / streak chips can flow into a
single horizontal row on desktop instead of stacking.
* **Achievements** — 3 × N grid on mobile becomes 4 × N or 5 × N
on desktop where windows ≥ 1280 wide.
* **Theme Picker** — 2-col grid of theme cards on mobile becomes
3- or 4-col on desktop.
* **Daily Challenge** — single-column scroll on both; no change.
## Mockup parity gap
The 9 missing-plugin screens (`splash`, `challenge`, `time-attack`,
`weekly-goals`, `leaderboard`, `sync`, `level-up`, `replay-overlay`,
`radial-menu`) have only mobile mockups. When porting any of these
plugins:
1. Read the mobile mockup for content + visual hierarchy.
2. Apply the universal adaptation rules above.
3. Apply the closest matching per-screen rule (e.g. an info modal
uses the same shape as Win Summary or Stats).
4. **No new layout pattern without explicit user approval.**
Adapting an existing pattern is in scope; inventing a desktop-
specific component is design work and should be flagged as such.
## Process notes
* **Smart-default sizer is the layout's source of truth.** Before
reading the mockup, always re-read `Window::size()`
`apply_smart_default_window_size` runs at startup and the
player can resize freely. Hardcoded breakpoints in plugin code
should reference the *current* `Window` width via a
`LayoutResource` lookup, not the launch size.
* **`WindowResized` already drives layout recomputes** (CLAUDE.md
§3.4). Any per-window-width adaptation in this file should hook
into the existing recompute path, not a new system.
* **Mobile rules win at narrow desktop windows.** A user dragging
their desktop window down to 600 px width is closer to the
mobile use-case than the desktop one. Below 1024 px width,
apply the mobile rules verbatim.
* **Run on a 4K monitor before declaring a port done.** HiDPI
scaling routes through Bevy's logical sizing, but visual
polish (border thickness, motion budgets at high refresh rate)
is worth eyeballing.
+253
View File
@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.outlined-glyph {
-webkit-text-stroke: 1.5px currentColor;
color: transparent;
}
.scanline-pattern {
background: repeating-linear-gradient(
0deg,
#1a1a1a,
#1a1a1a 2px,
#151515 2px,
#151515 4px
);
}
.tabular-nums {
font-variant-numeric: tabular-nums;
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface": "#151515",
"secondary-fixed": "#d5ec8c",
"warning": "#ddb26f",
"tertiary-fixed": "#fbd7ff",
"on-tertiary-fixed-variant": "#653173",
"on-background": "#e0e3e6",
"on-primary-container": "#004f6c",
"surface-container-lowest": "#0b0f11",
"on-surface": "#e0e3e6",
"error": "#fb9fb1",
"primary-fixed-dim": "#7ed0fe",
"inverse-primary": "#00668a",
"surface-container-high": "#272a2d",
"suit-red-cb": "#6fc2ef",
"surface-bright": "#363a3d",
"on-primary": "#003549",
"on-tertiary": "#4c195b",
"error-container": "#93000a",
"on-tertiary-fixed": "#340043",
"surface-container": "#202020",
"tertiary-container": "#e1a3ee",
"on-primary-fixed-variant": "#004c69",
"surface-container-highest": "#313538",
"highlight-celebration": "#e1a3ee",
"highlight-valid": "#acc267",
"primary": "#a1dcff",
"secondary-fixed-dim": "#bad073",
"on-primary-fixed": "#001e2c",
"on-error-container": "#ffdad6",
"secondary": "#bad073",
"on-tertiary": "#293500",
"on-secondary-container": "#b2c86d",
"inverse-on-surface": "#2d3134",
"on-error": "#690005",
"info": "#12cfc0",
"suit-red": "#fb9fb1",
"surface-dim": "#101417",
"surface-tint": "#7ed0fe",
"background": "#101417",
"secondary-container": "#435401",
"surface-variant": "#313538",
"outline-variant": "#3f484e",
"on-surface-variant": "#bfc8cf",
"primary-fixed": "#c4e7ff",
"on-secondary-fixed-variant": "#3c4d00",
"on-secondary-fixed": "#161e00",
"suit-black": "#d0d0d0"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"touch-target-min": "48px",
"margin-edge": "1rem",
"action-bar-height": "64px",
"stack-overlap": "2rem",
"gutter-card": "0.375rem"
},
"fontFamily": {
"label-caps": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"card-rank": ["JetBrains Mono"],
"body-md": ["Inter"],
"hud-score": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"]
},
"fontSize": {
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface text-on-surface font-body-md overflow-hidden selection:bg-primary selection:text-surface">
<!-- TopAppBar -->
<header class="fixed top-0 w-full flex justify-between items-center px-margin-edge h-[56px] bg-surface-container border-b border-outline dark:border-outline z-50">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-primary">terminal</span>
<h1 class="font-hud-score text-[18px] text-primary">solitaire.sh</h1>
</div>
<div class="flex items-center gap-4">
<div class="w-[6px] h-[6px] rounded-full bg-info"></div>
<span class="material-symbols-outlined text-on-surface-variant">settings</span>
</div>
</header>
<!-- HUD Band -->
<div class="fixed top-[56px] left-0 w-full h-[56px] bg-surface-container border-b border-outline-variant flex items-center justify-around px-margin-edge z-40">
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center">
<span class="font-label-caps text-[10px] text-on-surface-variant">SCORE</span>
<span class="font-hud-score text-primary tabular-nums">247</span>
</div>
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center border border-outline">
<span class="font-label-caps text-[10px] text-on-surface-variant">TIME</span>
<span class="font-hud-timer text-on-surface tabular-nums">12:34</span>
</div>
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center">
<span class="font-label-caps text-[10px] text-on-surface-variant">MOVES</span>
<span class="font-hud-score text-secondary tabular-nums">87</span>
</div>
</div>
<!-- Main Game Table -->
<main class="pt-[124px] px-margin-edge h-screen w-full relative">
<!-- Top Row: Stock, Waste, Foundations -->
<div class="grid grid-cols-7 gap-gutter-card h-[110px]">
<!-- Stock -->
<div class="relative w-full h-full rounded-xl border border-outline-variant bg-surface overflow-hidden scanline-pattern">
<div class="absolute top-1 left-1 w-3 h-4 bg-suit-red-cb"></div>
<div class="absolute bottom-1 right-1 font-label-caps text-[8px] text-suit-black">▌RS</div>
<div class="absolute bottom-[-16px] left-0 w-full text-center font-label-caps text-[10px] text-on-surface-variant">STOCK · 18</div>
</div>
<!-- Waste -->
<div class="relative w-full h-full rounded-xl border border-suit-red bg-[#1a1a1a] flex flex-col justify-between p-1.5">
<div class="font-card-rank text-suit-red leading-none">10<br/><span class="font-normal"></span></div>
<div class="self-end text-[32px] font-card-rank text-suit-red rotate-180"></div>
</div>
<!-- Empty Gap -->
<div></div>
<!-- Foundation S -->
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank"></span>
</div>
<!-- Foundation H -->
<div class="relative w-full h-full rounded-xl border border-suit-red bg-[#1a1a1a] flex flex-col justify-between p-1.5">
<div class="font-card-rank text-suit-red leading-none">2<br/><span class="font-normal"></span></div>
<div class="self-end text-[32px] font-card-rank text-suit-red rotate-180"></div>
</div>
<!-- Foundation C -->
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank outlined-glyph"></span>
</div>
<!-- Foundation D -->
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank outlined-glyph"></span>
</div>
</div>
<!-- Tableau -->
<div class="mt-8 grid grid-cols-7 gap-gutter-card items-start relative h-[400px]">
<!-- Col 1 -->
<div class="relative w-full h-full">
<div class="w-full h-[96px] rounded-xl border border-suit-black bg-[#1a1a1a] p-1.5">
<div class="font-card-rank text-suit-black leading-none">K<br/><span class="font-normal"></span></div>
</div>
</div>
<!-- Col 2 -->
<div class="relative w-full h-full">
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
<div class="absolute top-[32px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5">
<div class="font-card-rank text-suit-red leading-none">Q<br/><span class="font-normal"></span></div>
</div>
</div>
<!-- Col 3 -->
<div class="relative w-full h-full">
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
<div class="absolute top-[32px] w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
<div class="absolute top-[64px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5">
<div class="font-card-rank text-suit-red leading-none">10<br/><span class="font-normal outlined-glyph"></span></div>
</div>
</div>
<!-- Col 4 -->
<div class="relative w-full h-full">
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern absolute top-[32px]"></div>
<!-- Valid Drop Target Glow -->
<div class="absolute top-[64px] w-full h-[96px] rounded-xl border border-suit-black bg-[#1a1a1a] p-1.5 ring-4 ring-highlight-valid/30">
<div class="font-card-rank text-suit-black leading-none">9<br/><span class="font-normal"></span></div>
</div>
</div>
<!-- Col 5, 6 (Empty/Filler) -->
<div class="relative w-full"></div>
<div class="relative w-full"></div>
<!-- Col 7 -->
<div class="relative w-full">
<!-- Original Position Placeholder -->
<div class="w-full h-[96px] rounded-xl border border-dashed border-outline"></div>
<!-- Being Dragged Card -->
<div class="absolute top-[-20px] left-[30px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5 shadow-[0_0_20px_rgba(111,194,239,0.4)] z-50 ring-1 ring-primary/40">
<div class="font-card-rank text-suit-red leading-none">4<br/><span class="font-normal outlined-glyph"></span></div>
<div class="absolute bottom-1 right-1 text-[24px] font-card-rank text-suit-red rotate-180 outlined-glyph"></div>
</div>
</div>
</div>
</main>
<!-- BottomNavBar / Action Bar -->
<nav class="fixed bottom-0 left-0 w-full h-action-bar-height bg-surface-container border-t border-outline-variant flex justify-around items-center px-margin-edge z-50">
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
<span class="material-symbols-outlined" data-icon="menu">menu</span>
<span class="font-label-caps text-[10px] mt-1">[ESC] MENU</span>
</button>
<button class="flex flex-col items-center justify-center text-info font-bold active:opacity-80">
<span class="material-symbols-outlined" data-icon="undo">undo</span>
<span class="font-label-caps text-[10px] mt-1">[U] UNDO</span>
</button>
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
<span class="material-symbols-outlined" data-icon="lightbulb">lightbulb</span>
<span class="font-label-caps text-[10px] mt-1">[H] HINT</span>
</button>
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
<span class="material-symbols-outlined" data-icon="add_box">add_box</span>
<span class="font-label-caps text-[10px] mt-1">[N] NEW</span>
</button>
</nav>
<!-- Drag & CRT Overlay (Visual Decoration) -->
<div class="pointer-events-none fixed inset-0 z-[100] opacity-[0.03] scanline-pattern mix-blend-overlay"></div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+200
View File
@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-secondary-container": "#b2c86d",
"secondary-fixed-dim": "#bad073",
"surface-tint": "#7ed0fe",
"on-surface-variant": "#bfc8cf",
"surface-container-low": "#181c1f",
"secondary-fixed": "#d5ec8c",
"primary-fixed-dim": "#7ed0fe",
"secondary": "#bad073",
"tertiary-container": "#e1a3ee",
"inverse-on-surface": "#2d3134",
"surface-container-lowest": "#0b0f11",
"on-error-container": "#ffdad6",
"on-primary-fixed-variant": "#004c69",
"secondary-container": "#435401",
"background": "#101417",
"surface-variant": "#313538",
"on-primary-container": "#004f6c",
"highlight-valid": "#acc267",
"outline-variant": "#3f484e",
"on-background": "#e0e3e6",
"surface-bright": "#363a3d",
"on-tertiary-fixed-variant": "#653173",
"on-secondary-fixed": "#161e00",
"surface-dim": "#101417",
"on-surface": "#e0e3e6",
"info": "#12cfc0",
"on-secondary": "#293500",
"suit-red": "#fb9fb1",
"error": "#fb9fb1",
"error-container": "#93000a",
"surface-container": "#202020",
"primary-fixed": "#c4e7ff",
"warning": "#ddb26f",
"tertiary": "#f7c3ff",
"highlight-celebration": "#e1a3ee",
"tertiary-fixed": "#fbd7ff",
"inverse-surface": "#e0e3e6",
"tertiary-fixed-dim": "#f0b0fc",
"primary-container": "#6fc2ef",
"on-secondary-fixed-variant": "#3c4d00",
"on-tertiary": "#4c195b",
"suit-red-cb": "#6fc2ef",
"surface-container-highest": "#313538",
"on-primary-fixed": "#001e2c",
"surface-container-high": "#272a2d",
"primary": "#a1dcff",
"suit-black": "#d0d0d0",
"on-tertiary-container": "#683476",
"on-error": "#690005",
"inverse-primary": "#00668a",
"on-tertiary-fixed": "#340043",
"outline": "#505050",
"on-primary": "#003549",
"surface": "#151515"
},
"fontFamily": {
"jetbrains": ["JetBrains Mono", "monospace"],
"inter": ["Inter", "sans-serif"]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
font-size: 16px;
}
.tabular-nums { font-variant-numeric: tabular-nums; }
body { background-color: #151515; }
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="flex items-center justify-center min-h-screen p-4">
<!-- Mobile Container (390x844) -->
<div class="w-[390px] h-[844px] bg-surface flex flex-col overflow-hidden relative border border-outline/20">
<!-- 1. Status Bar -->
<header class="h-[32px] bg-surface-container flex items-center justify-between px-4 shrink-0">
<span class="font-jetbrains text-[12px] font-bold text-suit-black tracking-tight">▌rusty-solitaire(1) · MAN PAGE</span>
<button class="font-jetbrains text-[12px] font-bold text-suit-black/60 hover:text-primary transition-colors">× CLOSE</button>
</header>
<!-- 2. Heading Band -->
<div class="h-[120px] px-4 pt-10 pb-4 shrink-0">
<h1 class="font-jetbrains font-bold text-[24px] text-suit-black leading-none mb-1">GESTURES &amp; SHORTCUTS</h1>
<p class="font-inter text-[13px] text-on-surface-variant/80">Touch gestures and keyboard equivalents.</p>
</div>
<!-- Scrollable Content Section -->
<main class="flex-1 overflow-y-auto px-4 pb-8 space-y-6">
<!-- 3a. TOUCH GESTURES -->
<section class="space-y-3">
<h2 class="font-jetbrains text-[11px] font-medium tracking-widest text-on-surface-variant/60 uppercase">TOUCH GESTURES</h2>
<div class="space-y-1">
<!-- Row 1 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] flex items-center gap-2">
<span class="material-symbols-outlined text-suit-black" data-icon="square">square</span>
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">TAP card</span>
</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Select / unselect for move</div>
</div>
<!-- Row 2 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] flex items-center gap-2">
<span class="material-symbols-outlined text-suit-black" data-icon="east">east</span>
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">DRAG stack</span>
</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Move with translucent ghost preview</div>
</div>
<!-- Row 3 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] flex items-center gap-2">
<span class="material-symbols-outlined text-suit-black" data-icon="double_arrow">double_arrow</span>
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">DOUBLE-TAP</span>
</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Auto-send to best foundation</div>
</div>
<!-- Row 4 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] flex items-center gap-2">
<span class="material-symbols-outlined text-suit-black" data-icon="touch_app">touch_app</span>
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">LONG-PRESS</span>
</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Highlight all legal moves for card</div>
</div>
<!-- Row 5 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] flex items-center gap-2">
<span class="material-symbols-outlined text-suit-black" data-icon="south">south</span>
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">SWIPE DOWN</span>
</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Reveal hidden action bar</div>
</div>
</div>
</section>
<!-- 3b. KEYBOARD SHORTCUTS -->
<section class="space-y-3">
<h2 class="font-jetbrains text-[11px] font-medium tracking-widest text-on-surface-variant/60 uppercase">KEYBOARD SHORTCUTS</h2>
<div class="space-y-1">
<!-- Row 1 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[U]</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Undo last move</div>
</div>
<!-- Row 2 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[H]</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Show hint</div>
</div>
<!-- Row 3 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[N]</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">New game</div>
</div>
<!-- Row 4 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[A]</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Auto-complete (when possible)</div>
</div>
<!-- Row 5 -->
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[ESC]</div>
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Pause / back</div>
</div>
</div>
</section>
</main>
<!-- 4. Footer -->
<footer class="h-[24px] bg-surface-container border-t border-outline/20 flex items-center justify-between px-2 shrink-0">
<div class="font-jetbrains text-[10px] text-suit-black">
<span class="opacity-80">▌ NORMAL │ help</span>
</div>
<div class="font-jetbrains text-[10px] uppercase tracking-wider flex items-center gap-1">
<span class="text-outline">PRESS</span>
<span class="text-on-surface-variant">[ESC]</span>
<span class="text-outline">OR TAP</span>
<span class="text-on-surface-variant">×</span>
<span class="text-outline">TO RETURN</span>
</div>
</footer>
</div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+343
View File
@@ -0,0 +1,343 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>RS_TERMINAL_OS - Rusty Solitaire</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
font-size: 18px;
}
body {
background-color: #151515;
color: #d0d0d0;
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
}
.scanline {
width: 100%;
height: 2px;
background: rgba(26, 26, 26, 0.5);
position: absolute;
pointer-events: none;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #151515;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #353535;
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-tertiary-container": "#683476",
"surface-dim": "#101417",
"primary-fixed": "#c4e7ff",
"on-error": "#690005",
"on-secondary-fixed": "#161e00",
"on-tertiary": "#4c195b",
"primary-fixed-dim": "#7ed0fe",
"outline-variant": "#3f484e",
"tertiary": "#f7c3ff",
"surface": "#151515",
"tertiary-container": "#e1a3ee",
"highlight-celebration": "#e1a3ee",
"background": "#101417",
"surface-container": "#202020",
"primary-container": "#6fc2ef",
"on-secondary-fixed-variant": "#3c4d00",
"on-surface": "#d0d0d0",
"inverse-on-surface": "#2d3134",
"on-error-container": "#ffdad6",
"surface-container-low": "#181c1f",
"on-tertiary-fixed": "#340043",
"on-secondary-container": "#b2c86d",
"on-background": "#e0e3e6",
"secondary-container": "#435401",
"error": "#fb9fb1",
"info": "#12cfc0",
"on-surface-variant": "#bfc8cf",
"warning": "#ddb26f",
"inverse-primary": "#00668a",
"tertiary-fixed-dim": "#f0b0fc",
"surface-tint": "#7ed0fe",
"suit-black": "#d0d0d0",
"tertiary-fixed": "#fbd7ff",
"on-secondary": "#293500",
"on-primary-fixed": "#001e2c",
"surface-container-highest": "#313538",
"error-container": "#93000a",
"surface-container-high": "#272a2d",
"on-primary-container": "#004f6c",
"inverse-surface": "#e0e3e6",
"on-primary": "#003549",
"suit-red-cb": "#6fc2ef",
"on-primary-fixed-variant": "#004c69",
"on-tertiary-fixed-variant": "#653173",
"secondary-fixed": "#d5ec8c",
"highlight-valid": "#acc267",
"surface-variant": "#313538",
"secondary": "#bad073",
"secondary-fixed-dim": "#bad073",
"outline": "#505050",
"surface-container-lowest": "#0b0f11",
"primary": "#a1dcff",
"surface-bright": "#363a3d",
"suit-red": "#fb9fb1"
},
"borderRadius": {
"DEFAULT": "0px",
"lg": "0px",
"xl": "0px",
"full": "0px"
},
"spacing": {
"stack-overlap": "2rem",
"touch-target-min": "48px",
"margin-edge": "1rem",
"gutter-card": "0.375rem",
"action-bar-height": "64px"
},
"fontFamily": {
"card-rank": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"body-md": ["Inter"],
"label-caps": ["JetBrains Mono"]
},
"fontSize": {
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}]
}
},
},
}
</script>
</head>
<body class="bg-surface text-on-surface h-screen flex flex-col antialiased">
<!-- TOP BAR (32px) -->
<header class="h-8 bg-surface-container border-b border-outline flex items-center justify-between px-4 z-50">
<div class="flex items-center gap-2">
<span class="text-primary-container font-bold"></span>
<h1 class="font-headline text-[14px] font-bold tracking-tight text-on-surface">RS_TERMINAL_OS</h1>
</div>
<nav class="flex gap-4 font-label-caps text-[12px] uppercase tracking-widest">
<span class="text-primary-container">[ HOME ]</span>
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· PLAY</span>
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· STATS</span>
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· SETTINGS</span>
</nav>
<div class="flex items-center gap-3 font-label-caps text-[11px] text-on-surface-variant">
<div class="flex items-center gap-1">
<span>LV 12</span>
<span class="text-outline">|</span>
<div class="flex items-center gap-2">
<span>XP 320/500</span>
<div class="w-[60px] h-1 bg-surface-container-highest">
<div class="h-full bg-primary-container w-[64%]"></div>
</div>
</div>
</div>
<span class="text-outline">|</span>
<div class="flex items-center gap-1 text-info">
<span class="w-2 h-2 rounded-full bg-info"></span>
<span class="uppercase">Synced</span>
</div>
<span class="text-outline">|</span>
<span class="text-outline">v0.20.0</span>
</div>
</header>
<!-- MAIN CONTENT AREA -->
<main class="flex-1 flex overflow-hidden">
<!-- LEFT PANE (40%) -->
<section class="w-[40%] border-r border-outline flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
<div class="space-y-1">
<p class="text-outline font-label-caps text-xs">▌play.tsx</p>
<h2 class="font-headline text-[32px] font-bold text-on-surface leading-none uppercase">Ready to play?</h2>
<p class="text-on-surface-variant font-label-caps text-sm tracking-wide">RESUME · 12:34 ELAPSED · DRAW-3</p>
</div>
<button class="w-full h-24 bg-primary-container text-surface font-headline text-[24px] font-bold flex items-center justify-center gap-4 hover:brightness-110 active:scale-[0.98] transition-all">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
CONTINUE GAME
</button>
<div class="grid grid-cols-2 gap-4">
<button class="h-12 border border-outline bg-transparent text-on-surface font-label-caps text-sm hover:border-primary-container hover:text-primary-container transition-all flex items-center justify-center gap-2">
<span class="material-symbols-outlined">add</span>
NEW GAME
</button>
<button class="h-12 border border-outline bg-transparent text-on-surface font-label-caps text-sm hover:border-primary-container hover:text-primary-container transition-all flex items-center justify-center gap-2">
<span class="material-symbols-outlined">refresh</span>
RESTART RUN
</button>
</div>
<div class="space-y-4">
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Game Modes</p>
<div class="grid grid-cols-3 gap-3">
<!-- Zen -->
<div class="aspect-square border border-outline flex flex-col items-center justify-center gap-2 hover:bg-surface-container transition-colors cursor-pointer group">
<span class="material-symbols-outlined text-outline group-hover:text-primary-container">spa</span>
<span class="font-label-caps text-[10px] uppercase">Zen</span>
</div>
<!-- Time Attack -->
<div class="aspect-square border border-outline flex flex-col items-center justify-center gap-2 hover:bg-surface-container transition-colors cursor-pointer group">
<span class="material-symbols-outlined text-outline group-hover:text-primary-container">timer</span>
<span class="font-label-caps text-[10px] uppercase text-center">Time<br/>Attack</span>
</div>
<!-- Locked Challenge -->
<div class="aspect-square bg-[#0d0d0d] border border-outline/30 flex flex-col items-center justify-center gap-2 relative opacity-60">
<span class="material-symbols-outlined text-outline">lock</span>
<span class="font-label-caps text-[10px] uppercase">Challenge</span>
<div class="absolute -top-2 -right-2 bg-warning text-surface px-1 py-0.5 text-[8px] font-bold">LV 5</div>
</div>
</div>
</div>
<!-- VISUAL DECORATION (IMAGE PLACEHOLDER) -->
<div class="mt-auto pt-8">
<div class="w-full h-40 border border-outline overflow-hidden">
<img class="w-full h-full object-cover opacity-40 grayscale hover:grayscale-0 transition-all duration-700" data-alt="A dark, high-contrast digital art piece showing an abstract terminal interface with glowing cyan scanlines and retro-futuristic grid patterns. The composition is geometric and minimalist, following a synthwave aesthetic with deep black backgrounds and crisp cyan light elements. The lighting is moody and artificial, suggesting a high-performance computer screen in a dimly lit server room. Professional, sharp-edged UI design style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAet8SrRWSacZfwd8ISRQdDC7CDGixBwRnPAVMmMcjbifq1jnHSzCGWgSSL6YPSRfCkLNWr91BxTzV4zigGjMBLlk7rCLo5I7X7F6ydinDrKJVqZkRbvHJeSo90BPANoQwZtzPvhKXVEA9C2DbBaj8KPR4ObCo24Mj25NXPvGNThOE-3BSpuU6MPC-hrUMPVCPJpZnJdI_OmSz8mT021vjTxFERN12S1PFOzXKmNUDleoTDIat-8UifyKmKg4eKilecrBW6sFqaBw"/>
</div>
</div>
</section>
<!-- CENTER PANE (30%) -->
<section class="w-[30%] border-r border-outline flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
<div class="space-y-1">
<p class="text-outline font-label-caps text-xs">▌daily.json</p>
<div class="flex items-center justify-between">
<h3 class="font-headline text-[18px] font-bold text-on-surface">MAY 07 · 2026</h3>
<span class="bg-warning/20 text-warning px-2 py-1 text-[10px] font-bold border border-warning/40">EXPIRES 11:42:30</span>
</div>
</div>
<div class="bg-surface-container p-6 border border-outline space-y-4">
<div class="space-y-1">
<p class="text-on-surface-variant font-label-caps text-[10px] uppercase tracking-tighter">Current Seed</p>
<p class="font-headline text-[24px] font-extrabold text-highlight-valid">#2024-127</p>
</div>
<button class="w-full py-3 bg-primary-container text-surface font-label-caps text-xs font-bold uppercase tracking-widest hover:brightness-110 active:scale-95 transition-all">
▶ Attempt Today
</button>
</div>
<div class="space-y-3">
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Global Standings</p>
<div class="space-y-1 text-xs font-label-caps">
<div class="flex justify-between py-2 border-b border-outline/30 text-highlight-valid">
<span>01 │ swift_jaguar</span>
<span>02:47</span>
</div>
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
<span>02 │ pixel_drifter</span>
<span>03:12</span>
</div>
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
<span>03 │ null_ptr</span>
<span>03:15</span>
</div>
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
<span>04 │ core_dump_88</span>
<span>03:44</span>
</div>
<div class="flex justify-between py-2 text-primary-container bg-primary-container/10 px-2 -mx-2">
<span>12 │ YOU (anon)</span>
<span>--:--</span>
</div>
</div>
</div>
</section>
<!-- RIGHT PANE (30%) -->
<section class="w-[30%] flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
<div class="space-y-1">
<p class="text-outline font-label-caps text-xs">▌stats.log</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="border border-outline p-4 space-y-1">
<p class="text-on-surface-variant text-[10px] uppercase">Games</p>
<p class="font-hud-score text-[28px] text-on-surface">247</p>
</div>
<div class="border border-outline p-4 space-y-1 text-highlight-valid">
<p class="text-on-surface-variant text-[10px] uppercase">Win Rate</p>
<p class="font-hud-score text-[28px]">61%</p>
</div>
<div class="border border-outline p-4 space-y-1">
<p class="text-on-surface-variant text-[10px] uppercase">Best Time</p>
<p class="font-hud-score text-[28px]">01:54</p>
</div>
<div class="border border-outline p-4 space-y-1 text-primary-container">
<p class="text-on-surface-variant text-[10px] uppercase">Streak</p>
<p class="font-hud-score text-[28px]">7</p>
</div>
</div>
<div class="space-y-3">
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Achievements (8/19)</p>
<div class="flex flex-wrap gap-2">
<!-- Filled Cyan Dots -->
<div class="w-3 h-3 bg-primary-container"></div>
<div class="w-3 h-3 bg-primary-container"></div>
<div class="w-3 h-3 bg-primary-container"></div>
<div class="w-3 h-3 bg-primary-container"></div>
<div class="w-3 h-3 bg-primary-container"></div>
<div class="w-3 h-3 bg-primary-container"></div>
<div class="w-3 h-3 bg-primary-container"></div>
<div class="w-3 h-3 bg-primary-container"></div>
<!-- Empty Dots -->
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
<div class="w-3 h-3 border border-outline"></div>
</div>
</div>
<div class="mt-auto border border-outline bg-surface-container p-4 flex items-center justify-between hover:border-primary-container transition-colors cursor-pointer group">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-primary-container text-surface flex items-center justify-center font-bold text-lg">RS</div>
<div class="space-y-0.5">
<p class="text-on-surface font-bold text-xs">anonymous@local</p>
<p class="text-on-surface-variant text-[10px]">Session: Active</p>
</div>
</div>
<span class="material-symbols-outlined text-primary-container group-hover:translate-x-1 transition-transform">arrow_forward</span>
</div>
</section>
</main>
<!-- BOTTOM BAR (24px) -->
<footer class="h-6 bg-surface-container border-t border-outline flex items-center justify-between px-4 text-[10px] font-label-caps">
<div class="flex items-center gap-2">
<span class="text-primary-container">▌ NORMAL</span>
<span class="text-outline"></span>
<span class="text-on-surface-variant">~/rusty-solitaire/home</span>
</div>
<div class="flex items-center gap-4 text-on-surface-variant">
<div class="flex items-center gap-1"><span class="text-primary-container">[SPACE]</span> play</div>
<div class="flex items-center gap-1"><span class="text-primary-container">[D]</span> daily</div>
<div class="flex items-center gap-1"><span class="text-primary-container">[S]</span> settings</div>
<div class="flex items-center gap-1"><span class="text-primary-container">[?]</span> help</div>
</div>
<div class="text-outline">
2026-05-07 17:42 EDT
</div>
</footer>
<!-- GLOBAL SCANLINE EFFECT -->
<div class="fixed inset-0 pointer-events-none z-[100] overflow-hidden opacity-10">
<div class="absolute inset-0" style="background: repeating-linear-gradient(0deg, #151515, #151515 2px, #202020 4px);"></div>
</div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+225
View File
@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Rusty Solitaire - Main Menu</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"outline": "#505050",
"suit-red-cb": "#6fc2ef",
"suit-black": "#d0d0d0",
"surface-container-high": "#272a2d",
"primary-fixed": "#c4e7ff",
"on-secondary-container": "#b2c86d",
"secondary-fixed": "#d5ec8c",
"on-tertiary-container": "#683476",
"surface-tint": "#7ed0fe",
"background": "#101417",
"primary-container": "#6fc2ef",
"inverse-surface": "#e0e3e6",
"highlight-celebration": "#e1a3ee",
"surface-container-low": "#181c1f",
"on-surface": "#d0d0d0",
"primary": "#a1dcff",
"on-tertiary-fixed": "#340043",
"secondary-container": "#435401",
"inverse-primary": "#00668a",
"tertiary-fixed": "#fbd7ff",
"surface-bright": "#363a3d",
"on-secondary-fixed-variant": "#3c4d00",
"warning": "#ddb26f",
"tertiary-container": "#e1a3ee",
"suit-red": "#fb9fb1",
"primary-fixed-dim": "#7ed0fe",
"info": "#12cfc0",
"on-primary-fixed": "#001e2c",
"surface-container-lowest": "#0b0f11",
"error": "#fb9fb1",
"surface-variant": "#313538",
"on-error": "#690005",
"surface": "#151515",
"surface-container": "#202020",
"on-primary-container": "#004f6c",
"inverse-on-surface": "#2d3134",
"on-primary-fixed-variant": "#004c69",
"on-secondary": "#293500",
"error-container": "#93000a",
"secondary": "#bad073",
"tertiary": "#f7c3ff",
"outline-variant": "#3f484e",
"on-secondary-fixed": "#161e00",
"secondary-fixed-dim": "#bad073",
"surface-container-highest": "#313538",
"on-surface-variant": "#bfc8cf",
"tertiary-fixed-dim": "#f0b0fc",
"on-tertiary-fixed-variant": "#653173",
"on-error-container": "#ffdad6",
"on-primary": "#003549",
"on-background": "#e0e3e6",
"surface-dim": "#101417",
"on-tertiary": "#4c195b",
"highlight-valid": "#acc267"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"margin-edge": "1rem",
"touch-target-min": "48px",
"stack-overlap": "2rem",
"action-bar-height": "64px",
"gutter-card": "0.375rem"
},
"fontFamily": {
"hud-timer": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"label-caps": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"body-md": ["Inter"],
"card-rank": ["JetBrains Mono"]
},
"fontSize": {
"hud-timer": ["16px", { "lineHeight": "24px", "fontWeight": "400" }],
"headline": ["28px", { "lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700" }],
"label-caps": ["12px", { "lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500" }],
"hud-score": ["24px", { "lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700" }],
"body-md": ["16px", { "lineHeight": "24px", "fontWeight": "400" }],
"card-rank": ["18px", { "lineHeight": "18px", "fontWeight": "700" }]
}
}
}
}
</script>
<style>
.scanline {
background: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.1));
background-size: 100% 4px;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface text-on-surface font-hud-timer min-h-screen flex flex-col relative overflow-hidden">
<!-- Subtle CRT scanline overlay -->
<div class="absolute inset-0 pointer-events-none scanline opacity-20 z-0"></div>
<!-- Status Bar Zone -->
<div class="h-6 w-full flex justify-end items-center px-margin-edge pt-2 z-10 relative">
<div class="w-2 h-2 rounded-full bg-info"></div>
</div>
<!-- Header -->
<header class="px-margin-edge pt-4 pb-6 flex justify-between items-center z-10 relative">
<div class="flex items-center gap-1">
<span class="font-headline text-headline text-on-surface">▌RUSTY SOLITAIRE</span>
<div class="w-2 h-6 bg-primary-container inline-block ml-1 animate-pulse"></div>
</div>
<div class="bg-surface-container px-3 py-1 flex items-center gap-2 border border-outline">
<span class="font-label-caps text-label-caps text-on-surface">LV 12</span>
<div class="w-2 h-2 rounded-full bg-highlight-celebration"></div>
</div>
</header>
<!-- Main Content Canvas -->
<main class="flex-1 px-margin-edge flex flex-col gap-8 z-10 relative pb-24 overflow-y-auto">
<!-- XP Section -->
<section class="flex flex-col gap-2">
<div class="w-full h-1 bg-surface-container border border-outline relative">
<div class="absolute top-0 left-0 h-full bg-primary-container w-[64%]"></div>
</div>
<div class="font-label-caps text-label-caps text-on-surface-variant text-right">
320 / 500 XP
</div>
</section>
<!-- Primary Action -->
<section class="flex flex-col gap-2">
<button class="w-full h-[56px] bg-primary-container text-surface flex items-center justify-center gap-2 hover:bg-surface-tint transition-colors duration-120">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
<span class="font-label-caps text-[14px] uppercase tracking-widest font-bold">PLAY</span>
</button>
<div class="font-label-caps text-label-caps text-on-surface-variant text-center">
RESUME LAST GAME · 12:34 ELAPSED
</div>
</section>
<!-- Daily Challenge Tile -->
<section>
<div class="bg-surface-container border border-outline p-4 flex justify-between items-center hover:bg-surface-container-high transition-colors cursor-pointer group">
<div class="flex flex-col gap-2">
<span class="font-label-caps text-label-caps text-primary">DAILY CHALLENGE</span>
<span class="font-body-md text-body-md text-on-surface">DRAW-3 · SEED #2024-127</span>
<div class="inline-flex">
<span class="bg-surface px-2 py-0.5 border border-warning text-warning font-label-caps text-[10px]">EXPIRES 11:42:30</span>
</div>
</div>
<span class="material-symbols-outlined text-primary group-hover:translate-x-1 transition-transform">chevron_right</span>
</div>
</section>
<!-- Special Modes Grid -->
<section class="flex flex-col gap-4">
<h2 class="font-label-caps text-label-caps text-on-surface-variant">SPECIAL MODES</h2>
<div class="grid grid-cols-3 gap-gutter-card">
<!-- ZEN -->
<button class="aspect-square bg-surface border border-outline flex flex-col items-center justify-center gap-2 hover:border-primary hover:text-primary transition-colors text-on-surface">
<span class="material-symbols-outlined text-[32px]">self_improvement</span>
<span class="font-label-caps text-label-caps">ZEN</span>
</button>
<!-- TIME ATTACK -->
<button class="aspect-square bg-surface border border-outline flex flex-col items-center justify-center gap-2 hover:border-primary hover:text-primary transition-colors text-on-surface">
<span class="material-symbols-outlined text-[32px]">timer</span>
<span class="font-label-caps text-label-caps">TIME ATTACK</span>
</button>
<!-- CHALLENGE (Locked) -->
<button class="aspect-square bg-[#0d0d0d] border border-surface-container-high flex flex-col items-center justify-center gap-2 text-on-surface-variant opacity-75 cursor-not-allowed relative">
<span class="material-symbols-outlined text-[32px]">lock</span>
<span class="font-label-caps text-label-caps">CHALLENGE</span>
<div class="absolute top-2 right-2 bg-surface px-1 py-0.5 border border-warning text-warning font-label-caps text-[10px]">
LV 5
</div>
</button>
</div>
</section>
<!-- Secondary Nav Grid -->
<section class="grid grid-cols-2 gap-y-4 gap-x-6 pb-6">
<button class="flex items-center gap-3 h-[56px] border-l-2 border-outline pl-3 hover:border-primary hover:text-primary transition-colors text-on-surface justify-start">
<span class="material-symbols-outlined">bar_chart</span>
<span class="font-label-caps text-label-caps">STATS</span>
</button>
<button class="flex items-center gap-3 h-[56px] border-l-2 border-outline pl-3 hover:border-primary hover:text-primary transition-colors text-on-surface justify-start relative">
<span class="material-symbols-outlined">emoji_events</span>
<span class="font-label-caps text-label-caps">ACHIEVEMENTS</span>
<div class="absolute right-2 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-highlight-celebration"></div>
</button>
<button class="flex items-center gap-3 h-[56px] border-l-2 border-outline pl-3 hover:border-primary hover:text-primary transition-colors text-on-surface justify-start">
<span class="material-symbols-outlined">format_list_numbered</span>
<span class="font-label-caps text-label-caps">LEADERBOARD</span>
</button>
<button class="flex items-center gap-3 h-[56px] border-l-2 border-outline pl-3 hover:border-primary hover:text-primary transition-colors text-on-surface justify-start">
<span class="material-symbols-outlined">account_circle</span>
<span class="font-label-caps text-label-caps">PROFILE</span>
</button>
</section>
<!-- Footer Links -->
<footer class="flex flex-col items-center gap-4 mt-auto">
<div class="flex items-center gap-4 font-label-caps text-label-caps text-primary cursor-pointer hover:text-surface-tint">
<span>SETTINGS</span>
<span class="text-on-surface-variant">·</span>
<span>HELP</span>
</div>
<div class="font-label-caps text-[10px] text-on-surface-variant text-center opacity-60">
v0.20.0 — TERMINAL THEME · BUILD 2026.05
</div>
</footer>
</main>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+315
View File
@@ -0,0 +1,315 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Rusty Solitaire - Leaderboard</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
body {
background-color: #151515;
color: #e0e3e6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.scanline-overlay {
background: linear-gradient(to bottom, rgba(21, 21, 21, 0) 50%, rgba(26, 26, 26, 0.2) 50%);
background-size: 100% 4px;
pointer-events: none;
}
.terminal-glow {
box-shadow: 0 0 10px rgba(111, 194, 239, 0.1);
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"outline": "#505050",
"on-surface-variant": "#bfc8cf",
"secondary-container": "#435401",
"surface-container-lowest": "#0b0f11",
"primary": "#a1dcff",
"secondary-fixed": "#d5ec8c",
"on-secondary-fixed": "#161e00",
"on-error": "#690005",
"inverse-primary": "#00668a",
"surface-container": "#202020",
"highlight-valid": "#acc267",
"suit-black": "#d0d0d0",
"on-secondary-fixed-variant": "#3c4d00",
"on-primary-fixed": "#001e2c",
"on-tertiary-fixed-variant": "#653173",
"primary-fixed": "#c4e7ff",
"inverse-on-surface": "#2d3134",
"secondary-fixed-dim": "#bad073",
"on-secondary": "#293500",
"on-surface": "#e0e3e6",
"on-tertiary-container": "#683476",
"secondary": "#bad073",
"surface-bright": "#363a3d",
"tertiary-container": "#e1a3ee",
"surface-variant": "#313538",
"suit-red": "#fb9fb1",
"primary-fixed-dim": "#7ed0fe",
"surface-container-low": "#181c1f",
"surface": "#151515",
"suit-red-cb": "#6fc2ef",
"on-primary": "#003549",
"primary-container": "#6fc2ef",
"on-background": "#e0e3e6",
"tertiary": "#f7c3ff",
"surface-container-highest": "#313538",
"tertiary-fixed-dim": "#f0b0fc",
"tertiary-fixed": "#fbd7ff",
"info": "#12cfc0",
"error": "#fb9fb1",
"warning": "#ddb26f",
"on-primary-container": "#004f6c",
"surface-container-high": "#272a2d",
"inverse-surface": "#e0e3e6",
"error-container": "#93000a",
"on-tertiary-fixed": "#340043",
"surface-tint": "#7ed0fe",
"on-tertiary": "#4c195b",
"background": "#101417",
"on-error-container": "#ffdad6",
"on-secondary-container": "#b2c86d",
"outline-variant": "#3f484e",
"highlight-celebration": "#e1a3ee",
"surface-dim": "#101417",
"on-primary-fixed-variant": "#004c69"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"touch-target-min": "48px",
"action-bar-height": "64px",
"stack-overlap": "2rem",
"gutter-card": "0.375rem",
"margin-edge": "1rem"
},
"fontFamily": {
"card-rank": ["JetBrains Mono"],
"body-md": ["Inter"],
"headline": ["JetBrains Mono"],
"label-caps": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"]
},
"fontSize": {
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="font-body-md overflow-hidden h-[844px] w-[390px] mx-auto relative border-x border-outline/20">
<div class="scanline-overlay absolute inset-0 z-0"></div>
<!-- Top AppBar (Identity Anchor) -->
<header class="fixed top-0 w-full h-action-bar-height z-50 flex items-center px-margin-edge justify-between bg-surface dark:bg-surface text-primary dark:text-primary border-b border-outline dark:border-outline">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-primary">terminal</span>
<h1 class="font-headline text-headline text-primary dark:text-primary uppercase tracking-tighter">Rusty Solitaire</h1>
</div>
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-on-surface-variant hover:bg-surface-variant transition-colors duration-120 p-2 rounded-lg cursor-pointer">sync</span>
</div>
</header>
<main class="pt-[64px] h-[calc(100%-64px)] flex flex-col z-10 relative">
<!-- Pseudo Status Bar -->
<div class="h-[32px] bg-surface-container flex items-center justify-between px-4 font-label-caps text-[10px] tracking-tight">
<div class="text-[#a0a0a0]">▌leaderboard.tsx</div>
<div class="flex items-center gap-2">
<span class="flex items-center gap-1">
<span class="w-1.5 h-1.5 rounded-full bg-info"></span>
<span class="text-on-surface-variant">SYNCED</span>
</span>
<span class="text-outline">v0.20.0</span>
</div>
</div>
<!-- Tab Strip -->
<nav class="h-[40px] bg-[#1a1a1a] border-b border-[#353535] flex items-center">
<div class="flex-1 flex flex-col items-center justify-center relative">
<span class="font-label-caps text-[11px] text-[#6fc2ef]">[ TODAY ]</span>
<div class="absolute bottom-0 w-full h-[2px] bg-[#6fc2ef]"></div>
</div>
<div class="flex-1 flex items-center justify-center">
<span class="font-label-caps text-[11px] text-[#a0a0a0]">WEEK</span>
</div>
<div class="flex-1 flex items-center justify-center">
<span class="font-label-caps text-[11px] text-[#a0a0a0]">ALL-TIME</span>
</div>
<div class="flex-1 flex items-center justify-center">
<span class="font-label-caps text-[11px] text-[#a0a0a0]">FRIENDS</span>
</div>
</nav>
<div class="flex-1 overflow-y-auto px-margin-edge pt-4 space-y-4 pb-[88px]">
<!-- Hero Podium Card -->
<section class="h-[120px] bg-surface-container border border-[#353535] rounded-lg p-2 flex flex-col justify-between">
<div class="font-label-caps text-[10px] text-[#a0a0a0]">TOP 3 · TODAY</div>
<div class="flex gap-2 items-end justify-between flex-1 mt-1">
<!-- 2nd -->
<div class="flex-1 border border-[#a0a0a0] h-full rounded flex flex-col items-center justify-center relative py-1">
<span class="font-card-rank text-[16px] text-[#a0a0a0]">02</span>
<span class="text-[9px] font-mono text-[#d0d0d0] truncate w-full text-center px-1">base16_fan</span>
<span class="text-[10px] font-mono text-[#a0a0a0]">03:12</span>
</div>
<!-- 1st -->
<div class="flex-[1.2] border border-warning h-[110%] mb-[-2px] rounded-lg bg-surface flex flex-col items-center justify-center relative py-1 terminal-glow">
<span class="absolute top-1 right-1 text-warning material-symbols-outlined text-[14px]">star</span>
<span class="font-card-rank text-[24px] text-warning leading-none">01</span>
<span class="text-[11px] font-mono text-[#d0d0d0] font-bold truncate w-full text-center px-1">swift_jaguar</span>
<span class="text-[12px] font-mono text-[#d0d0d0]">02:47</span>
</div>
<!-- 3rd -->
<div class="flex-1 border border-[#7a5d3b] h-full rounded flex flex-col items-center justify-center relative py-1">
<span class="font-card-rank text-[16px] text-[#7a5d3b]">03</span>
<span class="text-[9px] font-mono text-[#d0d0d0] truncate w-full text-center px-1">cli_player</span>
<span class="text-[10px] font-mono text-[#a0a0a0]">03:54</span>
</div>
</div>
</section>
<!-- Search/Filter Row -->
<div class="flex items-center gap-2 h-[40px]">
<div class="px-3 h-8 border border-outline rounded flex items-center justify-center bg-surface-container-low">
<span class="font-label-caps text-[10px] text-[#6fc2ef]">[ ALL TIMES ]</span>
</div>
<div class="flex-1 h-8 border border-outline rounded flex items-center px-2 bg-surface gap-2">
<span class="font-mono text-[12px] text-outline">/ search players</span>
</div>
</div>
<!-- Leaderboard List -->
<div class="space-y-0.5 font-mono text-[12px]">
<!-- Header -->
<div class="flex justify-between px-2 pb-1 border-b border-outline/20 text-outline text-[10px] uppercase font-bold tracking-widest">
<span>Rank &amp; User</span>
<span>Time</span>
</div>
<!-- Rank 04 -->
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
<div class="flex gap-4">
<span class="text-[#a0a0a0] w-8">004</span>
<span class="text-on-surface">tablejockey</span>
</div>
<span class="text-[#a0a0a0]">04:01</span>
</div>
<!-- Rank 05 -->
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
<div class="flex gap-4">
<span class="text-[#a0a0a0] w-8">005</span>
<span class="text-on-surface">vim_motions</span>
</div>
<span class="text-[#a0a0a0]">04:05</span>
</div>
<!-- Rank 06 -->
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
<div class="flex gap-4">
<span class="text-[#a0a0a0] w-8">006</span>
<span class="text-on-surface">tmux_lover</span>
</div>
<span class="text-[#a0a0a0]">04:18</span>
</div>
<!-- Rank 07 -->
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
<div class="flex gap-4">
<span class="text-[#a0a0a0] w-8">007</span>
<span class="text-on-surface">nvim_dotfiles</span>
</div>
<span class="text-[#a0a0a0]">04:23</span>
</div>
<!-- Rank 08 -->
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
<div class="flex gap-4">
<span class="text-[#a0a0a0] w-8">008</span>
<span class="text-on-surface">dark_theme</span>
</div>
<span class="text-[#a0a0a0]">04:31</span>
</div>
<!-- Spacer for truncated view -->
<div class="flex justify-center py-2 text-outline/30 tracking-[1em]">...</div>
<!-- YOU (Rank 17) -->
<div class="flex items-center justify-between px-2 py-2 bg-[#1f3a4a]/30 border border-[#6fc2ef]/40 rounded-sm">
<div class="flex gap-4">
<span class="text-[#6fc2ef] w-8 font-bold">▶ 017</span>
<span class="text-[#6fc2ef] font-bold">anonymous (YOU)</span>
</div>
<span class="text-[#6fc2ef] font-bold">04:12</span>
</div>
<!-- Rank 18 -->
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
<div class="flex gap-4">
<span class="text-[#a0a0a0] w-8">018</span>
<span class="text-on-surface">bash_brawler</span>
</div>
<span class="text-[#a0a0a0]">05:01</span>
</div>
<!-- Rank 19 -->
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
<div class="flex gap-4">
<span class="text-[#a0a0a0] w-8">019</span>
<span class="text-on-surface">curl_master</span>
</div>
<span class="text-[#a0a0a0]">05:14</span>
</div>
</div>
</div>
<!-- CLI Style Footer -->
<footer class="fixed bottom-0 w-full h-[24px] bg-[#202020] border-t border-[#353535] px-2 flex items-center justify-between font-mono text-[9px] z-50">
<div class="text-[#a0a0a0]">
<span class="text-info font-bold"></span> NORMAL │ leaderboard
</div>
<div class="text-[#a0a0a0] flex gap-3">
<span>[1-4] tab</span>
<span>[/] search</span>
<span>[ESC] back</span>
</div>
</footer>
<!-- Shared Component: BottomNavBar -->
<nav class="fixed bottom-[24px] w-full h-action-bar-height z-50 flex justify-around items-center bg-surface-container dark:bg-surface-container border-t border-outline dark:border-outline">
<button class="flex flex-col items-center justify-center text-on-surface-variant dark:text-on-surface-variant p-2 hover:text-primary dark:hover:text-primary transition-all duration-120 ease-linear active:bg-surface-container-highest">
<span class="material-symbols-outlined">playing_cards</span>
<span class="font-label-caps text-label-caps">DEAL [F1]</span>
</button>
<button class="flex flex-col items-center justify-center text-on-surface-variant dark:text-on-surface-variant p-2 hover:text-primary dark:hover:text-primary transition-all duration-120 ease-linear active:bg-surface-container-highest">
<span class="material-symbols-outlined">undo</span>
<span class="font-label-caps text-label-caps">UNDO [Z]</span>
</button>
<button class="flex flex-col items-center justify-center text-on-surface-variant dark:text-on-surface-variant p-2 hover:text-primary dark:hover:text-primary transition-all duration-120 ease-linear active:bg-surface-container-highest">
<span class="material-symbols-outlined">lightbulb</span>
<span class="font-label-caps text-label-caps">HINT [H]</span>
</button>
<button class="flex flex-col items-center justify-center bg-primary-container dark:bg-primary-container text-on-primary-container dark:text-on-primary-container rounded-none p-2 transition-all duration-120 ease-linear active:bg-surface-container-highest">
<span class="material-symbols-outlined">analytics</span>
<span class="font-label-caps text-label-caps">STATS [S]</span>
</button>
<button class="flex flex-col items-center justify-center text-on-surface-variant dark:text-on-surface-variant p-2 hover:text-primary dark:hover:text-primary transition-all duration-120 ease-linear active:bg-surface-container-highest">
<span class="material-symbols-outlined">menu</span>
<span class="font-label-caps text-label-caps">MENU [ESC]</span>
</button>
</nav>
</main>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+259
View File
@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>ROOT@SOLITAIRE:~ | LEVEL UP</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.scanline-overlay {
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
background-size: 100% 2px, 3px 100%;
pointer-events: none;
}
.card-glow {
box-shadow: 0 0 24px rgba(225, 163, 238, 0.25);
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed": "#fbd7ff",
"secondary-container": "#435401",
"on-tertiary-fixed": "#340043",
"inverse-surface": "#e0e3e6",
"tertiary-container": "#e1a3ee",
"background": "#101417",
"on-primary": "#003549",
"info": "#12cfc0",
"surface-container-highest": "#313538",
"secondary-fixed": "#d5ec8c",
"secondary-fixed-dim": "#bad073",
"on-surface-variant": "#bfc8cf",
"on-secondary": "#293500",
"on-tertiary-fixed-variant": "#653173",
"surface-container-low": "#181c1f",
"surface-container-high": "#272a2d",
"secondary": "#bad073",
"outline-variant": "#3f484e",
"on-surface": "#e0e3e6",
"surface-tint": "#7ed0fe",
"on-tertiary": "#4c195b",
"on-secondary-fixed": "#161e00",
"primary-fixed": "#c4e7ff",
"on-tertiary-container": "#683476",
"on-secondary-container": "#b2c86d",
"surface-container-lowest": "#0b0f11",
"inverse-primary": "#00668a",
"primary-container": "#6fc2ef",
"surface-container": "#1c2023",
"on-background": "#e0e3e6",
"suit-red-cb": "#6fc2ef",
"surface-dim": "#101417",
"on-primary-fixed-variant": "#004c69",
"tertiary": "#f7c3ff",
"on-secondary-fixed-variant": "#3c4d00",
"highlight-celebration": "#e1a3ee",
"on-primary-fixed": "#001e2c",
"primary-fixed-dim": "#7ed0fe",
"tertiary-fixed-dim": "#f0b0fc",
"inverse-on-surface": "#2d3134",
"error": "#fb9fb1",
"error-container": "#93000a",
"surface-bright": "#363a3d",
"on-primary-container": "#004f6c",
"warning": "#ddb26f",
"surface": "#151515",
"suit-black": "#d0d0d0",
"highlight-valid": "#acc267",
"outline": "#505050",
"surface-variant": "#313538",
"on-error-container": "#ffdad6",
"on-error": "#690005",
"primary": "#a1dcff",
"suit-red": "#fb9fb1"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"gutter-card": "0.375rem",
"action-bar-height": "64px",
"stack-overlap": "2rem",
"margin-edge": "1rem"
},
"fontFamily": {
"card-rank": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"body-md": ["Inter"],
"headline": ["JetBrains Mono"],
"label-caps": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"]
},
"fontSize": {
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background font-body-md overflow-hidden h-screen select-none">
<!-- Top App Bar -->
<header class="fixed top-0 w-full z-50 flex justify-between items-center px-margin-edge h-action-bar-height bg-background dark:bg-background border-b border-outline-variant dark:border-outline-variant">
<div class="font-headline text-headline text-primary dark:text-primary uppercase tracking-tighter">ROOT@SOLITAIRE:~</div>
<div class="flex gap-4">
<span class="material-symbols-outlined text-primary" data-icon="memory">memory</span>
<span class="material-symbols-outlined text-primary" data-icon="settings_ethernet">settings_ethernet</span>
<span class="material-symbols-outlined text-primary" data-icon="wifi_tethering">wifi_tethering</span>
</div>
</header>
<!-- Main Tableau (Dimmed Background) -->
<main class="pt-24 px-4 flex flex-col gap-8 opacity-20 filter grayscale">
<!-- HUD Chips -->
<div class="flex justify-between items-center">
<div class="bg-surface-container p-3 flex flex-col">
<span class="font-label-caps text-label-caps text-on-surface-variant uppercase">SCORE</span>
<span class="font-hud-score text-hud-score text-primary">04,820</span>
</div>
<div class="bg-surface-container p-3 flex flex-col items-end">
<span class="font-label-caps text-label-caps text-on-surface-variant uppercase">TIMER</span>
<span class="font-hud-timer text-hud-timer text-on-surface">04:12</span>
</div>
</div>
<!-- Foundation & Stock -->
<div class="flex gap-gutter-card justify-between">
<div class="flex gap-gutter-card">
<div class="w-[64px] h-[88px] border border-dashed border-outline rounded-DEFAULT"></div>
<div class="w-[64px] h-[88px] bg-surface border border-outline rounded-DEFAULT relative overflow-hidden">
<div class="absolute inset-0 scanline-overlay"></div>
<div class="absolute top-2 left-2 w-3 h-4 bg-suit-red-cb"></div>
<div class="absolute bottom-2 right-2 font-card-rank text-[12px] text-on-surface">▌RS</div>
</div>
</div>
<div class="flex gap-gutter-card">
<div class="w-[64px] h-[88px] border border-outline rounded-DEFAULT bg-surface"></div>
<div class="w-[64px] h-[88px] border border-outline rounded-DEFAULT bg-surface"></div>
<div class="w-[64px] h-[88px] border border-outline rounded-DEFAULT bg-surface"></div>
<div class="w-[64px] h-[88px] border border-outline rounded-DEFAULT bg-surface"></div>
</div>
</div>
<!-- Cascades -->
<div class="grid grid-cols-7 gap-gutter-card">
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
</div>
</main>
<!-- CELEBRATION OVERLAY SCREEM -->
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-surface/95 backdrop-blur-sm">
<!-- Celebration Card -->
<div class="w-[340px] h-[480px] bg-[#202020] border border-highlight-celebration rounded-xl card-glow flex flex-col overflow-hidden relative">
<!-- Title Bar -->
<div class="h-[28px] bg-[#1a1a1a] border-b border-outline flex items-center px-4 shrink-0">
<span class="text-primary mr-1"></span>
<span class="font-headline text-[12px] text-[#a0a0a0]">level-up.tsx</span>
</div>
<!-- Content Area -->
<div class="flex-grow p-6 flex flex-col">
<!-- Hero Band -->
<div class="text-center mb-6">
<div class="font-headline text-[14px] text-highlight-celebration uppercase tracking-[0.08em] mb-2">▲ LEVEL UP</div>
<div class="flex items-baseline justify-center gap-2">
<span class="font-headline font-bold text-[96px] text-suit-black leading-none tracking-tighter">13</span>
<div class="flex flex-col items-start">
<span class="font-label-caps text-[11px] text-outline uppercase">FROM 12</span>
</div>
</div>
<div class="mt-2 font-headline font-medium text-[13px] text-highlight-celebration tracking-[0.08em]">█ NEW PERKS UNLOCKED</div>
</div>
<!-- Perks List -->
<div class="space-y-2 mb-6">
<!-- Item 1 -->
<div class="h-[48px] bg-[#1a1a1a] rounded-[4px] px-3 flex items-center justify-between">
<span class="font-headline text-[13px] text-suit-black">▢ +1 daily challenge slot</span>
<span class="bg-highlight-celebration/10 text-highlight-celebration px-2 py-0.5 rounded-full font-label-caps text-[10px] border border-highlight-celebration/30">NEW</span>
</div>
<!-- Item 2 -->
<div class="h-[48px] bg-[#1a1a1a] rounded-[4px] px-3 flex items-center justify-between">
<span class="font-headline text-[13px] text-suit-black">▢ Background: Forest</span>
<span class="bg-highlight-valid/10 text-highlight-valid px-2 py-0.5 rounded-full font-label-caps text-[10px] border border-highlight-valid/30">UNLOCKED</span>
</div>
<!-- Item 3 -->
<div class="h-[48px] bg-[#1a1a1a] rounded-[4px] px-3 flex items-center justify-between">
<span class="font-headline text-[13px] text-suit-black">▢ Card-back: Stripes</span>
<span class="bg-highlight-valid/10 text-highlight-valid px-2 py-0.5 rounded-full font-label-caps text-[10px] border border-highlight-valid/30">UNLOCKED</span>
</div>
</div>
<!-- XP Recap -->
<div class="h-[48px] bg-[#1a1a1a] rounded-[4px] px-4 flex items-center justify-between mt-auto">
<span class="font-headline text-[12px] text-[#a0a0a0]">XP</span>
<span class="font-headline font-bold text-[14px] text-highlight-valid uppercase">+200 XP THIS LEVEL</span>
<div class="w-[60px] h-[4px] bg-outline-variant rounded-full overflow-hidden">
<div class="w-[0%] h-full bg-suit-red-cb"></div>
</div>
</div>
</div>
<!-- Action Button -->
<button class="h-[56px] w-full bg-suit-red-cb flex items-center justify-center gap-2 hover:opacity-90 active:opacity-75 transition-opacity">
<span class="font-headline font-bold text-[14px] text-background tracking-wider">▶ CONTINUE</span>
</button>
<!-- Scanline layer inside card -->
<div class="absolute inset-0 scanline-overlay opacity-20 pointer-events-none"></div>
</div>
<!-- Caption -->
<div class="absolute bottom-8 w-full text-center">
<span class="font-body-md text-[11px] text-[#a0a0a0] uppercase tracking-widest opacity-60">Tap anywhere to dismiss</span>
</div>
</div>
<!-- Bottom Nav Bar -->
<nav class="fixed bottom-0 w-full z-50 flex justify-between items-center h-action-bar-height bg-surface-container dark:bg-surface-container border-t border-outline dark:border-outline">
<div class="flex flex-col items-center justify-center bg-primary text-background p-2 w-1/5 h-full transition-colors font-label-caps text-label-caps uppercase tracking-widest">
<span class="material-symbols-outlined" data-icon="videogame_asset">videogame_asset</span>
<span>NORMAL</span>
</div>
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 w-1/5 h-full hover:text-primary transition-colors font-label-caps text-label-caps uppercase tracking-widest">
<span class="material-symbols-outlined" data-icon="edit">edit</span>
<span>INSERT</span>
</div>
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 w-1/5 h-full hover:text-primary transition-colors font-label-caps text-label-caps uppercase tracking-widest">
<span class="material-symbols-outlined" data-icon="visibility">visibility</span>
<span>VISUAL</span>
</div>
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 w-1/5 h-full hover:text-primary transition-colors font-label-caps text-label-caps uppercase tracking-widest">
<span class="material-symbols-outlined" data-icon="auto_fix_high">auto_fix_high</span>
<span>SOLVE</span>
</div>
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 w-1/5 h-full hover:text-primary transition-colors font-label-caps text-label-caps uppercase tracking-widest">
<span class="material-symbols-outlined" data-icon="power_settings_new">power_settings_new</span>
<span>QUIT</span>
</div>
</nav>
<!-- Persistent Background Overlay (CRT Effect) -->
<div class="fixed inset-0 pointer-events-none z-[200] scanline-overlay opacity-30"></div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+206
View File
@@ -0,0 +1,206 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&amp;family=Inter:wght@400;500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#f7c3ff",
"surface": "#151515",
"surface-container-lowest": "#0b0f11",
"surface-variant": "#313538",
"surface-tint": "#7ed0fe",
"primary": "#a1dcff",
"on-tertiary-fixed-variant": "#653173",
"on-error-container": "#ffdad6",
"on-tertiary-container": "#683476",
"secondary": "#bad073",
"surface-dim": "#101417",
"inverse-on-surface": "#2d3134",
"on-tertiary": "#4c195b",
"surface-container-high": "#272a2d",
"on-primary-fixed-variant": "#004c69",
"surface-container": "#202020",
"highlight-celebration": "#e1a3ee",
"background": "#101417",
"suit-red": "#fb9fb1",
"on-secondary-container": "#b2c86d",
"inverse-surface": "#e0e3e6",
"error": "#fb9fb1",
"on-surface": "#d0d0d0",
"info": "#12cfc0",
"secondary-container": "#435401",
"tertiary-fixed-dim": "#f0b0fc",
"surface-bright": "#363a3d",
"outline-variant": "#3f484e",
"on-secondary-fixed-variant": "#3c4d00",
"warning": "#ddb26f",
"surface-container-highest": "#313538",
"secondary-fixed": "#d5ec8c",
"highlight-valid": "#acc267",
"secondary-fixed-dim": "#bad073",
"tertiary-container": "#e1a3ee",
"suit-red-cb": "#6fc2ef",
"on-error": "#690005",
"on-primary-container": "#004f6c",
"suit-black": "#d0d0d0",
"inverse-primary": "#00668a",
"surface-container-low": "#181c1f",
"on-primary-fixed": "#001e2c",
"on-secondary": "#293500",
"primary-fixed": "#c4e7ff",
"on-tertiary-fixed": "#340043",
"outline": "#505050",
"error-container": "#93000a",
"tertiary-fixed": "#fbd7ff",
"primary-container": "#6fc2ef",
"on-background": "#e0e3e6",
"on-primary": "#003549",
"on-surface-variant": "#bfc8cf",
"on-secondary-fixed": "#161e00",
"primary-fixed-dim": "#7ed0fe"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"margin-edge": "1rem",
"gutter-card": "0.375rem",
"touch-target-min": "48dp",
"stack-overlap": "2rem",
"action-bar-height": "64px"
},
"fontFamily": {
"card-rank": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"body-md": ["Inter"],
"headline": ["JetBrains Mono"],
"label-caps": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"]
},
"fontSize": {
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body {
background-color: #151515;
color: #d0d0d0;
-webkit-font-smoothing: antialiased;
}
.card-scanline {
background: linear-gradient(rgba(21, 21, 21, 0) 50%, rgba(26, 26, 26, 1) 50%);
background-size: 100% 4px;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="flex items-center justify-center min-h-screen">
<!-- Mobile Canvas (390x844 simulated) -->
<main class="w-[390px] h-[844px] bg-surface relative overflow-hidden flex flex-col">
<!-- Status Bar -->
<header class="h-8 bg-surface-container flex items-center justify-between px-margin-edge border-b border-outline-variant">
<div class="font-hud-timer text-[11px] text-primary tracking-tight">▌onboard/01-draw.tsx</div>
<div class="font-hud-timer text-[11px] text-on-surface-variant font-bold">STEP 1 OF 3</div>
</header>
<!-- Content Canvas -->
<div class="flex-1 overflow-y-auto pb-action-bar-height">
<!-- Hero Section -->
<section class="h-[140px] flex flex-col items-center justify-center mt-8">
<div class="w-8 h-12 bg-primary animate-pulse mb-2"></div>
<h1 class="font-headline text-headline text-on-surface uppercase tracking-tighter">
WELCOME <span class="text-primary">▌_</span>
</h1>
</section>
<!-- Headline -->
<section class="px-margin-edge mt-4 text-center">
<h2 class="font-headline text-[22px] leading-tight text-on-surface mb-1">CHOOSE A DRAW MODE</h2>
<p class="font-body-md text-[12px] text-on-surface-variant">You can change this any time in Settings.</p>
</section>
<!-- Choice Cards -->
<section class="px-margin-edge mt-8 space-y-4">
<!-- DRAW-3 Card -->
<div class="h-[120px] bg-surface-container border border-primary p-4 relative flex items-start gap-4">
<div class="absolute top-0 right-0 bg-primary px-2 py-0.5">
<span class="font-label-caps text-[10px] text-surface font-bold">RECOMMENDED</span>
</div>
<div class="w-12 h-16 flex items-center justify-center border border-outline-variant bg-surface-dim">
<span class="material-symbols-outlined text-primary" data-icon="filter_3">filter_3</span>
</div>
<div class="flex-1">
<h3 class="font-headline text-[16px] text-primary mb-1">DRAW-3 (CLASSIC)</h3>
<p class="font-hud-timer text-[12px] leading-snug text-on-surface-variant">
Cycle 3 cards at a time. Standard solitaire rules for a tactical challenge.
</p>
</div>
</div>
<!-- DRAW-1 Card -->
<div class="h-[120px] bg-surface-container border border-outline-variant p-4 flex items-start gap-4">
<div class="w-12 h-16 flex items-center justify-center border border-outline-variant bg-surface-dim">
<span class="material-symbols-outlined text-on-surface-variant" data-icon="filter_1">filter_1</span>
</div>
<div class="flex-1">
<h3 class="font-headline text-[16px] text-on-surface mb-1">DRAW-1 (EASY)</h3>
<p class="font-hud-timer text-[12px] leading-snug text-on-surface-variant">
Cycle one card at a time. More winnable, faster pace, perfect for quick sessions.
</p>
</div>
</div>
</section>
<!-- Step Indicator -->
<section class="mt-12 flex flex-col items-center">
<div class="flex gap-2 mb-2">
<div class="w-8 h-1.5 bg-primary"></div>
<div class="w-8 h-1.5 bg-outline-variant"></div>
<div class="w-8 h-1.5 bg-outline-variant"></div>
</div>
<div class="font-hud-timer text-[12px] flex gap-4">
<span class="text-primary font-bold">[1]</span>
<span class="text-outline-variant">[2]</span>
<span class="text-outline-variant">[3]</span>
</div>
</section>
</div>
<!-- Bottom Action Bar -->
<footer class="h-action-bar-height bg-surface-container border-t border-outline flex items-center justify-between px-margin-edge fixed bottom-0 w-[390px] z-50">
<!-- Back Button (Disabled/Muted) -->
<button class="w-[48%] h-12 border border-outline-variant flex items-center justify-center gap-2 opacity-40 cursor-not-allowed">
<span class="material-symbols-outlined text-outline-variant text-[18px]" data-icon="arrow_back">arrow_back</span>
<span class="font-label-caps text-outline-variant">BACK</span>
</button>
<!-- Next Button -->
<button class="w-[48%] h-12 bg-primary flex items-center justify-center gap-2 active:opacity-80 transition-opacity">
<span class="font-headline text-[14px] text-surface font-bold uppercase tracking-widest">NEXT</span>
<span class="material-symbols-outlined text-surface text-[18px]" data-icon="arrow_forward">arrow_forward</span>
</button>
</footer>
<!-- Terminal Overlay (Faint scanlines for atmosphere) -->
<div class="pointer-events-none absolute inset-0 opacity-[0.03] card-scanline z-[100]"></div>
</main>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+211
View File
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400;500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"outline-variant": "#3f484e",
"tertiary-fixed-dim": "#f0b0fc",
"surface-bright": "#363a3d",
"secondary-container": "#435401",
"info": "#12cfc0",
"on-secondary-fixed-variant": "#3c4d00",
"highlight-celebration": "#e1a3ee",
"surface-container": "#202020",
"on-primary-fixed-variant": "#004c69",
"on-surface": "#d0d0d0",
"on-secondary-container": "#b2c86d",
"inverse-surface": "#e0e3e6",
"error": "#fb9fb1",
"background": "#101417",
"suit-red": "#fb9fb1",
"surface-dim": "#101417",
"inverse-on-surface": "#2d3134",
"on-tertiary-container": "#683476",
"secondary": "#bad073",
"primary": "#a1dcff",
"on-tertiary-fixed-variant": "#653173",
"on-error-container": "#ffdad6",
"surface-container-high": "#272a2d",
"on-tertiary": "#4c195b",
"tertiary": "#f7c3ff",
"surface": "#151515",
"surface-tint": "#7ed0fe",
"surface-variant": "#313538",
"surface-container-lowest": "#0b0f11",
"on-surface-variant": "#bfc8cf",
"on-primary": "#003549",
"primary-fixed-dim": "#7ed0fe",
"on-secondary-fixed": "#161e00",
"on-tertiary-fixed": "#340043",
"outline": "#505050",
"on-secondary": "#293500",
"primary-fixed": "#c4e7ff",
"primary-container": "#6fc2ef",
"on-background": "#e0e3e6",
"tertiary-fixed": "#fbd7ff",
"error-container": "#93000a",
"suit-red-cb": "#6fc2ef",
"surface-container-low": "#181c1f",
"on-primary-fixed": "#001e2c",
"on-primary-container": "#004f6c",
"suit-black": "#d0d0d0",
"inverse-primary": "#00668a",
"on-error": "#690005",
"secondary-fixed-dim": "#bad073",
"surface-container-highest": "#313538",
"secondary-fixed": "#d5ec8c",
"highlight-valid": "#acc267",
"warning": "#ddb26f",
"tertiary-container": "#e1a3ee"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"margin-edge": "1rem",
"gutter-card": "0.375rem",
"touch-target-min": "48dp",
"stack-overlap": "2rem",
"action-bar-height": "64px"
},
"fontFamily": {
"label-caps": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"card-rank": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"body-md": ["Inter"]
},
"fontSize": {
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.scanline-bg {
background: linear-gradient(to bottom, #1a1a1a 1px, transparent 1px);
background-size: 100% 2px;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-surface font-body-md select-none overflow-hidden h-screen flex flex-col">
<!-- Top Navigation Bar -->
<header class="fixed top-0 w-full bg-background z-50 border-b border-outline flex justify-between items-center px-margin-edge h-action-bar-height">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-primary" data-icon="terminal">terminal</span>
<span class="font-headline text-headline text-primary uppercase tracking-tighter text-sm md:text-base">▌onboard/03-demo.tsx</span>
</div>
<div class="font-label-caps text-label-caps text-on-surface-variant">STEP 3 OF 3</div>
</header>
<main class="flex-1 mt-[64px] mb-[64px] flex flex-col items-center px-margin-edge pt-6 space-y-6 overflow-y-auto">
<!-- Header Text -->
<div class="w-full text-center space-y-2">
<h1 class="font-headline text-headline text-on-surface">TRY IT OUT</h1>
<p class="font-body-md text-on-surface-variant max-w-xs mx-auto">Tap a face-up card to auto-move it to the best legal pile.</p>
</div>
<!-- Demo Panel -->
<div class="w-full max-w-sm bg-surface border border-outline p-6 rounded-lg relative overflow-hidden">
<!-- Subtle scanline background effect for "terminal" pane feel -->
<div class="absolute inset-0 scanline-bg opacity-10 pointer-events-none"></div>
<div class="relative z-10 flex flex-col items-center">
<!-- Foundation Slot -->
<div class="w-20 h-28 border border-dashed border-outline-variant flex items-center justify-center mb-12">
<span class="material-symbols-outlined text-outline-variant opacity-40 text-4xl" data-icon="spades">playing_cards</span>
</div>
<!-- Path Indicator (The Arrow) -->
<div class="absolute top-[84px] left-1/2 -translate-x-1/2 flex flex-col items-center">
<div class="font-label-caps text-secondary text-[10px] mb-1">MOVES HERE</div>
<span class="material-symbols-outlined text-secondary text-3xl font-bold" data-icon="arrow_upward">arrow_upward</span>
</div>
<!-- Mini-Cards Row -->
<div class="flex gap-gutter-card">
<!-- A-Spades (Target) -->
<div class="w-20 h-28 bg-surface border-2 border-primary rounded flex flex-col justify-between p-2 relative ring-1 ring-primary ring-offset-2 ring-offset-surface">
<div class="font-card-rank text-card-rank text-suit-black">A</div>
<span class="material-symbols-outlined self-end text-3xl text-suit-black" data-icon="spades" style="font-variation-settings: 'FILL' 1;">playing_cards</span>
<!-- Pulse Icon -->
<div class="absolute inset-0 flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-4xl opacity-80" data-icon="touch_app">touch_app</span>
</div>
</div>
<!-- K-Hearts -->
<div class="w-20 h-28 bg-surface border border-suit-red rounded flex flex-col justify-between p-2 opacity-50">
<div class="font-card-rank text-card-rank text-suit-red">K</div>
<span class="material-symbols-outlined self-end text-3xl text-suit-red" data-icon="favorite" style="font-variation-settings: 'FILL' 1;">favorite</span>
</div>
<!-- Q-Clubs -->
<div class="w-20 h-28 bg-surface border border-outline rounded flex flex-col justify-between p-2 opacity-50">
<div class="font-card-rank text-card-rank text-on-surface">Q</div>
<span class="material-symbols-outlined self-end text-3xl text-on-surface" data-icon="clubs">groups</span>
</div>
</div>
</div>
</div>
<!-- CLI Prompt -->
<div class="w-full max-w-sm flex items-center gap-2 font-label-caps text-on-surface py-2">
<span class="text-primary"></span>
<span class="tracking-widest">TAP THE A♠ TO CONTINUE</span>
<span class="w-3 h-5 bg-primary animate-pulse"></span>
</div>
<!-- Feature List -->
<div class="w-full max-w-sm space-y-3 pt-2">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-secondary text-sm" data-icon="check_circle" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-label-caps text-label-caps">TAP TO AUTO-MOVE</span>
</div>
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-secondary text-sm" data-icon="check_circle" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-label-caps text-label-caps">DRAG TO TARGET PILE</span>
</div>
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-secondary text-sm" data-icon="check_circle" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-label-caps text-label-caps">DOUBLE-TAP TO FOUNDATION</span>
</div>
</div>
<!-- Step Indicators -->
<div class="flex gap-2 py-4">
<div class="w-8 h-1 bg-primary"></div>
<div class="w-8 h-1 bg-primary"></div>
<div class="w-8 h-1 bg-primary"></div>
</div>
</main>
<!-- Bottom Action Bar -->
<footer class="fixed bottom-0 w-full h-[64px] bg-surface-container border-t border-outline flex items-center px-margin-edge justify-between z-50">
<button class="px-6 py-2 border border-outline text-on-surface-variant font-label-caps text-label-caps transition-colors duration-120 active:bg-surface-bright flex items-center gap-2">
<span class="material-symbols-outlined text-sm" data-icon="arrow_back">arrow_back</span>
BACK
</button>
<button class="px-6 py-2 bg-primary text-on-primary font-label-caps text-label-caps transition-colors duration-120 active:bg-primary-container flex items-center gap-2">
<span class="material-symbols-outlined text-sm" data-icon="play_arrow" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
START PLAYING
</button>
</footer>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -0,0 +1,218 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=390, height=844, initial-scale=1.0" name="viewport"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&amp;family=Inter:wght@400;500;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"highlight-valid": "#acc267",
"outline": "#505050",
"highlight-celebration": "#e1a3ee",
"error-container": "#93000a",
"surface-container": "#202020",
"on-secondary-fixed": "#161e00",
"tertiary": "#f7c3ff",
"inverse-on-surface": "#2d3134",
"tertiary-fixed-dim": "#f0b0fc",
"on-primary-fixed": "#001e2c",
"info": "#12cfc0",
"on-tertiary": "#4c195b",
"secondary-container": "#435401",
"surface": "#151515",
"tertiary-container": "#e1a3ee",
"outline-variant": "#3f484e",
"suit-red": "#fb9fb1",
"secondary-fixed": "#d5ec8c",
"error": "#fb9fb1",
"primary-container": "#6fc2ef",
"surface-container-lowest": "#0b0f11",
"on-surface": "#e0e3e6",
"tertiary-fixed": "#fbd7ff",
"on-secondary-container": "#b2c86d",
"on-primary-fixed-variant": "#004c69",
"on-primary": "#003549",
"on-secondary": "#293500",
"on-primary-container": "#004f6c",
"secondary": "#bad073",
"surface-container-highest": "#313538",
"primary": "#a1dcff",
"surface-container-low": "#181c1f",
"secondary-fixed-dim": "#bad073",
"warning": "#ddb26f",
"suit-black": "#d0d0d0",
"surface-variant": "#313538",
"on-tertiary-container": "#683476",
"on-tertiary-fixed": "#340043",
"on-secondary-fixed-variant": "#3c4d00",
"on-background": "#e0e3e6",
"surface-bright": "#363a3d",
"on-error": "#690005",
"primary-fixed-dim": "#7ed0fe",
"on-tertiary-fixed-variant": "#653173",
"suit-red-cb": "#6fc2ef",
"inverse-surface": "#e0e3e6",
"on-surface-variant": "#bfc8cf",
"background": "#101417",
"primary-fixed": "#c4e7ff",
"on-error-container": "#ffdad6",
"inverse-primary": "#00668a",
"surface-dim": "#101417",
"surface-container-high": "#272a2d",
"surface-tint": "#7ed0fe",
"cyan-terminal": "#6fc2ef"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"touch-target-min": "48dp",
"stack-overlap": "2rem",
"gutter-card": "0.375rem",
"action-bar-height": "64px",
"margin-edge": "1rem"
},
"fontFamily": {
"headline": ["JetBrains Mono"],
"label-caps": ["JetBrains Mono"],
"body-md": ["Inter"],
"hud-score": ["JetBrains Mono"],
"card-rank": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"]
}
}
}
}
</script>
<style>
.scanline-pattern {
background: repeating-linear-gradient(
0deg,
#151515,
#151515 2px,
#1a1a1a 2px,
#1a1a1a 4px
);
}
.checker-pattern {
background-color: #ffffff;
background-image:
linear-gradient(45deg, #004c69 25%, transparent 25%),
linear-gradient(-45deg, #004c69 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #004c69 75%),
linear-gradient(-45deg, transparent 75%, #004c69 75%);
background-size: 8px 8px;
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
}
.stripe-pattern {
background: repeating-linear-gradient(
0deg,
#fb9fb1,
#fb9fb1 4px,
#151515 4px,
#151515 8px
);
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface text-on-surface min-h-screen flex flex-col items-center overflow-hidden selection:bg-cyan-terminal selection:text-surface">
<!-- 1. Status Bar -->
<header class="w-full h-8 bg-surface-container flex items-center justify-between px-4 border-b border-outline-variant">
<span class="font-label-caps text-[12px] text-on-surface uppercase tracking-tight">▌onboard/02-theme.tsx</span>
<span class="font-label-caps text-[12px] text-[#a0a0a0] uppercase tracking-widest">STEP 2 OF 3</span>
</header>
<!-- 2. Hero Illustration Band -->
<section class="w-full flex flex-col items-center pt-8 pb-4">
<div class="h-[100px] flex items-center justify-center relative">
<span class="text-cyan-terminal font-headline text-[48px] mr-4 select-none"></span>
<div class="flex -space-x-4">
<div class="w-[24px] h-[34px] border border-outline bg-surface scanline-pattern transform -rotate-12 translate-y-2"></div>
<div class="w-[24px] h-[34px] border border-outline bg-surface checker-pattern transform rotate-0 z-10"></div>
<div class="w-[24px] h-[34px] border border-outline bg-surface stripe-pattern transform rotate-12 translate-y-2"></div>
</div>
</div>
<h2 class="font-headline text-[28px] font-bold text-suit-black tracking-tight leading-none">PICK YOUR DECK</h2>
</section>
<!-- 3. Headline & Description -->
<section class="w-full px-margin-edge text-center mb-6">
<h3 class="font-headline text-[22px] font-bold text-suit-black mb-1">CHOOSE A CARD-BACK</h3>
<p class="font-body-md text-[12px] text-[#a0a0a0] leading-tight">
You can swap or import more themes from Settings later.
</p>
</section>
<!-- 4. Theme Selection Grid -->
<main class="w-full px-margin-edge grid grid-cols-3 gap-2 flex-grow max-h-[220px]">
<!-- Tile 1: Terminal (Active) -->
<div class="flex flex-col items-center">
<div class="w-full aspect-[110/150] bg-surface-container border-2 border-cyan-terminal rounded-lg p-3 relative flex items-center justify-center overflow-hidden">
<div class="w-full h-full scanline-pattern border border-outline-variant relative">
<div class="absolute top-1 left-1 w-2 h-3 bg-cyan-terminal"></div>
<div class="absolute bottom-1 right-1 font-headline text-[10px] text-on-surface opacity-50">▌RS</div>
</div>
<div class="absolute top-1 right-1 bg-cyan-terminal text-surface w-4 h-4 flex items-center justify-center rounded-full">
<span class="material-symbols-outlined text-[12px] font-bold">check</span>
</div>
</div>
<span class="mt-2 font-label-caps text-[12px] font-bold text-suit-black tracking-widest uppercase">TERMINAL</span>
</div>
<!-- Tile 2: Classic -->
<div class="flex flex-col items-center opacity-70">
<div class="w-full aspect-[110/150] bg-surface-container border border-outline rounded-lg p-3 relative flex items-center justify-center overflow-hidden">
<div class="w-full h-full checker-pattern border border-outline-variant"></div>
</div>
<span class="mt-2 font-label-caps text-[12px] font-bold text-suit-black tracking-widest uppercase">CLASSIC</span>
</div>
<!-- Tile 3: Stripes -->
<div class="flex flex-col items-center opacity-70">
<div class="w-full aspect-[110/150] bg-surface-container border border-outline rounded-lg p-3 relative flex items-center justify-center overflow-hidden">
<div class="w-full h-full stripe-pattern border border-outline-variant"></div>
</div>
<span class="mt-2 font-label-caps text-[12px] font-bold text-suit-black tracking-widest uppercase">STRIPES</span>
</div>
</main>
<!-- 5. More Info -->
<div class="w-full text-center mt-4">
<span class="font-label-caps text-[11px] font-medium text-[#a0a0a0] tracking-widest">
<span class="text-cyan-terminal">+</span> MORE IN SETTINGS
</span>
</div>
<!-- 6. Step Indicator -->
<section class="w-full flex flex-col items-center mt-6">
<div class="flex gap-1 h-2 mb-2">
<div class="w-8 h-1 bg-cyan-terminal rounded-full"></div>
<div class="w-8 h-1 bg-cyan-terminal rounded-full"></div>
<div class="w-8 h-1 bg-outline rounded-full"></div>
</div>
<div class="font-headline text-[12px] font-medium tracking-[0.2em]">
<span class="text-cyan-terminal">[1]</span>
<span class="text-cyan-terminal">[2]</span>
<span class="text-outline">[3]</span>
</div>
</section>
<!-- 7. Bottom Action Bar -->
<footer class="fixed bottom-0 w-full h-[64px] bg-surface-container flex items-center justify-between px-margin-edge z-50">
<button class="w-[48%] h-12 border border-outline bg-transparent text-suit-black font-label-caps text-[13px] font-medium uppercase rounded-lg active:bg-surface-variant transition-colors">
← BACK
</button>
<button class="w-[48%] h-12 bg-cyan-terminal text-surface font-label-caps text-[14px] font-bold uppercase rounded-lg active:opacity-80 transition-opacity">
NEXT →
</button>
</footer>
<!-- Image descriptive data for the model (hidden visually) -->
<div class="hidden" data-alt="A detailed user interface screen for a retro-terminal themed solitaire game called Rusty Solitaire. The background is a deep black with cyan and gray accents. In the center, a card theme selection grid displays three different card back designs: a scanline pattern, a checker pattern, and a striped pattern. The visual style is crisp, technical, and uses monospaced typography to evoke a command-line interface or professional developer environment. The mood is minimalist, efficient, and technologically nostalgic."></div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+212
View File
@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Rouge Solitaire - Pause</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;500;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-secondary-fixed": "#161e00",
"secondary-fixed": "#d5ec8c",
"warning": "#ddb26f",
"surface-container-low": "#181c1f",
"surface-container": "#1c2023",
"on-primary-fixed-variant": "#004c69",
"outline-variant": "#3f484e",
"on-tertiary-container": "#683476",
"surface-container-high": "#272a2d",
"on-primary-fixed": "#001e2c",
"primary-fixed": "#c4e7ff",
"surface-bright": "#363a3d",
"outline": "#505050",
"tertiary-fixed-dim": "#f0b0fc",
"tertiary": "#f7c3ff",
"on-surface": "#e0e3e6",
"secondary": "#bad073",
"tertiary-fixed": "#fbd7ff",
"info": "#12cfc0",
"primary": "#a1dcff",
"secondary-fixed-dim": "#bad073",
"surface-tint": "#7ed0fe",
"background": "#101417",
"surface-container-highest": "#313538",
"on-tertiary-fixed": "#340043",
"highlight-valid": "#acc267",
"inverse-primary": "#00668a",
"surface-dim": "#101417",
"error": "#fb9fb1",
"on-error": "#690005",
"inverse-surface": "#e0e3e6",
"suit-red": "#fb9fb1",
"suit-black": "#d0d0d0",
"inverse-on-surface": "#2d3134",
"highlight-celebration": "#e1a3ee",
"on-error-container": "#ffdad6",
"on-primary": "#003549",
"surface": "#151515",
"surface-container-lowest": "#0b0f11",
"primary-fixed-dim": "#7ed0fe",
"on-secondary": "#293500",
"suit-red-cb": "#6fc2ef",
"on-tertiary": "#4c195b",
"error-container": "#93000a",
"on-secondary-fixed-variant": "#3c4d00",
"on-secondary-container": "#b2c86d",
"on-surface-variant": "#bfc8cf",
"on-primary-container": "#004f6c",
"primary-container": "#6fc2ef",
"on-background": "#e0e3e6",
"surface-variant": "#313538",
"secondary-container": "#435401",
"on-tertiary-fixed-variant": "#653173",
"tertiary-container": "#e1a3ee"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"touch-target-min": "48dp",
"action-bar-height": "64px",
"stack-overlap": "2rem",
"margin-edge": "1rem",
"gutter-card": "0.375rem"
},
"fontFamily": {
"body-md": ["Inter"],
"headline": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"card-rank": ["JetBrains Mono"],
"label-caps": ["JetBrains Mono"]
},
"fontSize": {
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}]
}
},
},
}
</script>
<style>
.scanline {
background: linear-gradient(
to bottom,
transparent 50%,
rgba(0, 0, 0, 0.05) 50%
);
background-size: 100% 4px;
pointer-events: none;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-surface font-body-md overflow-hidden antialiased">
<!-- Background Tableau (Simulated by Dimmed Image Overlay) -->
<div class="fixed inset-0 z-0">
<img alt="Game Tableau Background" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDJSHHDQ5Y5qul5C_xabnOSM9aS3uxcWSTk47AOHrS_KIlQi0Ur7YhtL0BomjEWTDc8vRLpytWeG4kf5xgxBzpORahTtsWyXOUPsVRg6_H_qp0QjM6DDo57rOPwjU6TFdfK3Pi7cO9rg-xnUSSu1wu29WyKVwSWDDaA5cZ4QN_9L81YMTCTMKAwDTGsY3eGsj1b1i1X2CdF211aepkhmX8xf4bnV35WSB3QuYxUwlPct0Met7iLFf-AGBeizhK6IAboW5u-Wpg8Ag"/>
<!-- 95% Opacity Scrim -->
<div class="absolute inset-0 bg-surface opacity-95"></div>
<!-- Scanline Overlay for Texture -->
<div class="absolute inset-0 scanline"></div>
</div>
<!-- Modal Container -->
<div class="fixed inset-0 z-10 flex items-center justify-center p-margin-edge">
<!-- Modal Panel -->
<div class="w-[330px] h-[480px] bg-[#202020] border border-outline rounded-xl flex flex-col overflow-hidden">
<!-- Title Bar -->
<div class="h-[28px] bg-[#1a1a1a] border-b border-[#353535] px-3 flex items-center justify-between">
<div class="flex items-center gap-1.5">
<span class="text-primary-container font-headline text-[12px] leading-none mt-px"></span>
<span class="font-headline text-[12px] text-[#a0a0a0] leading-none">pause.tsx</span>
</div>
<button class="flex items-center justify-center">
<span class="material-symbols-outlined text-[16px] text-[#505050]">close</span>
</button>
</div>
<!-- Content Canvas -->
<div class="flex-1 flex flex-col items-center pt-8 px-4">
<!-- Headline -->
<h1 class="font-headline text-[24px] font-bold text-[#d0d0d0] tracking-tight text-center">
GAME PAUSED
</h1>
<!-- Subline -->
<p class="font-headline text-[12px] text-[#a0a0a0] mt-1 text-center">
12:34 ELAPSED · 87 MOVES · DRAW-3
</p>
<!-- Mini-Stat Chips -->
<div class="flex gap-2 mt-4 justify-center">
<div class="bg-[#1a1a1a] border border-[#353535] rounded-sm px-2 py-1 flex flex-col items-center">
<span class="font-headline text-[11px] text-[#d0d0d0]">SCORE 247</span>
</div>
<div class="bg-[#1a1a1a] border border-[#353535] rounded-sm px-2 py-1 flex flex-col items-center">
<span class="font-headline text-[11px] text-[#d0d0d0]">STOCK 18</span>
</div>
<div class="bg-[#1a1a1a] border border-[#353535] rounded-sm px-2 py-1 flex flex-col items-center">
<span class="font-headline text-[11px] text-[#d0d0d0]">MOVES 87</span>
</div>
</div>
<!-- Action Buttons Cluster -->
<div class="w-full mt-6 space-y-3">
<!-- Primary CTA -->
<button class="w-full h-[48px] bg-primary-container text-surface flex items-center justify-center rounded-lg active:scale-95 transition-transform duration-75">
<span class="font-headline text-[14px] font-bold tracking-[0.08em] uppercase">▶ RESUME GAME</span>
</button>
<!-- Secondary Buttons -->
<button class="w-full h-[48px] bg-transparent border border-outline text-[#d0d0d0] flex items-center justify-center rounded-lg hover:border-primary-container hover:text-primary-container transition-colors duration-120">
<span class="font-headline text-[13px] font-medium tracking-[0.08em] uppercase">↻ RESTART</span>
</button>
<button class="w-full h-[48px] bg-transparent border border-outline text-[#d0d0d0] flex items-center justify-center rounded-lg hover:border-primary-container hover:text-primary-container transition-colors duration-120">
<span class="font-headline text-[13px] font-medium tracking-[0.08em] uppercase">✕ FORFEIT</span>
</button>
<button class="w-full h-[48px] bg-transparent border border-outline text-[#d0d0d0] flex items-center justify-center rounded-lg hover:border-primary-container hover:text-primary-container transition-colors duration-120">
<span class="font-headline text-[13px] font-medium tracking-[0.08em] uppercase">⌂ QUIT TO MENU</span>
</button>
</div>
</div>
<!-- Footer Status Line -->
<div class="h-[24px] border-t border-[#353535] px-3 flex items-center justify-between">
<div class="flex items-center gap-1">
<span class="text-primary-container font-headline text-[11px]"></span>
<span class="font-headline text-[11px] text-[#a0a0a0]">NORMAL</span>
<span class="text-[#505050] text-[11px]"></span>
<span class="font-headline text-[11px] text-[#a0a0a0]">pause</span>
</div>
<div class="flex items-center gap-1 font-headline text-[11px]">
<span class="text-[#a0a0a0]">[ESC]</span>
<span class="text-[#505050]">resume</span>
</div>
</div>
</div>
</div>
<!-- Hidden Navigation Shell (Suppressed due to Task-Focused Modal Context) -->
<!-- But included visually as per the brand anchor hierarchy for TopAppBar identity if it were visible -->
<header class="hidden fixed top-0 w-full h-action-bar-height flex items-center justify-between px-margin-edge w-full bg-background border-b border-outline-variant">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-primary">terminal</span>
<span class="font-headline text-headline text-primary uppercase tracking-tighter">▌ROUGE_SOLITAIRE</span>
</div>
</header>
<!-- Bottom Nav Suppression Logic: Not rendered to prioritize the focus canvas -->
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+274
View File
@@ -0,0 +1,274 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400;500;700&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
/* CRT Scanline Overlay Effect */
.crt-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
background-size: 100% 3px, 3px 100%;
pointer-events: none;
z-index: 100;
}
.custom-scrollbar::-webkit-scrollbar {
height: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #151515;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #505050;
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-container-low": "#181c1f",
"on-primary-fixed": "#001e2c",
"on-error": "#690005",
"suit-black": "#d0d0d0",
"inverse-primary": "#00668a",
"on-primary-container": "#004f6c",
"suit-red-cb": "#6fc2ef",
"tertiary-container": "#e1a3ee",
"secondary-fixed": "#d5ec8c",
"highlight-valid": "#acc267",
"surface-container-highest": "#313538",
"secondary-fixed-dim": "#bad073",
"warning": "#ddb26f",
"on-secondary-fixed": "#161e00",
"primary-fixed-dim": "#7ed0fe",
"on-surface-variant": "#bfc8cf",
"on-primary": "#003549",
"tertiary-fixed": "#fbd7ff",
"primary-container": "#6fc2ef",
"on-background": "#e0e3e6",
"error-container": "#93000a",
"outline": "#505050",
"on-tertiary-fixed": "#340043",
"primary-fixed": "#c4e7ff",
"on-surface": "#e0e3e6",
"on-secondary": "#293500",
"surface-container-high": "#272a2d",
"on-tertiary": "#4c195b",
"secondary": "#bad073",
"on-tertiary-container": "#683476",
"inverse-on-surface": "#2d3134",
"surface-dim": "#101417",
"primary": "#a1dcff",
"on-tertiary-fixed-variant": "#653173",
"on-error-container": "#ffdad6",
"surface-variant": "#313538",
"surface-tint": "#7ed0fe",
"surface-container-lowest": "#0b0f11",
"surface": "#151515",
"tertiary": "#f7c3ff",
"on-secondary-fixed-variant": "#3c4d00",
"surface-bright": "#363a3d",
"tertiary-fixed-dim": "#f0b0fc",
"outline-variant": "#3f484e",
"info": "#12cfc0",
"secondary-container": "#435401",
"on-secondary-container": "#b2c86d",
"inverse-surface": "#e0e3e6",
"error": "#fb9fb1",
"suit-red": "#fb9fb1",
"background": "#101417",
"highlight-celebration": "#e1a3ee",
"on-primary-fixed-variant": "#004c69",
"surface-container": "#202020"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"stack-overlap": "2rem",
"action-bar-height": "64px",
"touch-target-min": "48dp",
"gutter-card": "0.375rem",
"margin-edge": "1rem"
},
"fontFamily": {
"headline": ["JetBrains Mono"],
"label-caps": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"body-md": ["Inter"],
"card-rank": ["JetBrains Mono"]
},
"fontSize": {
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background font-body-md selection:bg-primary selection:text-background overflow-x-hidden">
<div class="crt-overlay"></div>
<!-- Status Bar -->
<header class="bg-surface-container h-[32px] w-full flex items-center justify-between px-margin-edge z-50">
<div class="font-headline text-[12px] text-on-surface-variant tracking-wider">
▌profile.tsx
</div>
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-info"></span>
<span class="font-label-caps text-[10px] text-on-surface">● SYNCED</span>
</div>
</header>
<!-- Main Canvas -->
<main class="flex-1 overflow-y-auto pb-24">
<!-- Profile Header -->
<section class="h-[120px] bg-surface-container border-b border-outline-variant flex items-center px-margin-edge gap-4">
<div class="w-[64px] h-[64px] bg-[#1a1a1a] border border-outline flex items-center justify-center shrink-0">
<span class="font-headline text-[28px] text-primary-container">RS</span>
</div>
<div class="flex flex-col gap-1 overflow-hidden">
<h1 class="font-headline text-[18px] text-on-surface truncate">anonymous@local</h1>
<p class="font-label-caps text-on-surface-variant text-[10px]">MEMBER SINCE 2026-04-22</p>
<div class="flex gap-2 mt-1">
<span class="px-2 py-0.5 bg-[#1a1a1a] font-label-caps text-[10px] text-suit-black">247 GAMES</span>
<span class="px-2 py-0.5 bg-[#1a1a1a] font-label-caps text-[10px] text-suit-black">61% WR</span>
<span class="px-2 py-0.5 bg-[#1a1a1a] font-label-caps text-[10px] text-suit-black">12 STREAK</span>
</div>
</div>
</section>
<!-- Level/XP Section -->
<section class="p-margin-edge bg-surface-container border-b border-outline-variant">
<div class="flex justify-between items-baseline mb-2">
<span class="font-headline text-[24px] text-on-surface">LEVEL 12</span>
<span class="font-hud-timer text-on-surface-variant">320/500 XP</span>
</div>
<div class="h-3 w-full bg-[#353535] relative overflow-hidden">
<div class="h-full bg-primary-container" style="width: 64%;"></div>
</div>
<div class="flex items-center gap-2 mt-3">
<span class="w-1.5 h-1.5 rounded-full bg-highlight-celebration"></span>
<span class="font-label-caps text-[10px] text-on-surface-variant">180 XP TO LEVEL 13</span>
</div>
</section>
<!-- Unlocked Cards -->
<section class="mt-6">
<h2 class="px-margin-edge font-headline text-[14px] text-on-surface mb-4">▌ unlocked.cards</h2>
<div class="flex overflow-x-auto gap-4 px-margin-edge pb-4 custom-scrollbar">
<!-- Terminal (Active) -->
<div class="shrink-0 flex flex-col gap-2">
<div class="w-[60px] h-[84px] bg-surface-container-low border-2 border-primary-container relative flex items-center justify-center p-1">
<div class="w-full h-full bg-[#151515] overflow-hidden flex flex-col p-1">
<div class="w-2 h-2.5 bg-primary-container mb-auto"></div>
<div class="self-end text-[8px] font-headline text-on-surface-variant opacity-50">▌RS</div>
</div>
</div>
<span class="font-label-caps text-[9px] text-primary-container text-center">ACTIVE</span>
</div>
<!-- Classic -->
<div class="shrink-0 flex flex-col gap-2">
<div class="w-[60px] h-[84px] bg-white border border-outline relative p-1">
<div class="w-full h-full border border-red-200 bg-red-50 opacity-20"></div>
</div>
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">TAP TO USE</span>
</div>
<!-- Stripes -->
<div class="shrink-0 flex flex-col gap-2">
<div class="w-[60px] h-[84px] bg-surface-container border border-outline p-1">
<div class="w-full h-full bg-gradient-to-br from-secondary-container via-surface to-secondary-container opacity-40"></div>
</div>
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">TAP TO USE</span>
</div>
<!-- Polka -->
<div class="shrink-0 flex flex-col gap-2">
<div class="w-[60px] h-[84px] bg-surface-container border border-outline p-1 overflow-hidden relative">
<div class="w-full h-full opacity-30" style="background-image: radial-gradient(#505050 1px, transparent 0); background-size: 6px 6px;"></div>
</div>
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">TAP TO USE</span>
</div>
</div>
</section>
<!-- Unlocked Backgrounds -->
<section class="mt-6">
<h2 class="px-margin-edge font-headline text-[14px] text-on-surface mb-4">▌ unlocked.backgrounds</h2>
<div class="flex overflow-x-auto gap-4 px-margin-edge pb-4 custom-scrollbar">
<!-- Default (Active) -->
<div class="shrink-0 flex flex-col gap-2">
<div class="w-[80px] h-[56px] bg-[#151515] border-2 border-primary-container"></div>
<span class="font-label-caps text-[9px] text-primary-container text-center">ACTIVE</span>
</div>
<!-- Forest -->
<div class="shrink-0 flex flex-col gap-2">
<div class="w-[80px] h-[56px] bg-[#0d160d] border border-outline"></div>
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">FOREST</span>
</div>
<!-- Slate -->
<div class="shrink-0 flex flex-col gap-2">
<div class="w-[80px] h-[56px] bg-[#1c2128] border border-outline"></div>
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">SLATE</span>
</div>
<!-- Midnight -->
<div class="shrink-0 flex flex-col gap-2">
<div class="w-[80px] h-[56px] bg-[#09090b] border border-outline"></div>
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">MIDNIGHT</span>
</div>
</div>
</section>
<!-- Sign-in Card -->
<section class="mt-8 px-margin-edge">
<button class="w-full h-[64px] bg-surface-container border border-dashed border-outline flex items-center justify-between px-6 hover:bg-surface-variant transition-colors group">
<span class="font-label-caps text-on-surface-variant tracking-widest">+ SIGN IN TO SYNC PROGRESS</span>
<span class="material-symbols-outlined text-primary-container group-hover:translate-x-1 transition-transform">arrow_forward</span>
</button>
</section>
</main>
<!-- TopAppBar (from Shared Components - as Terminal Header) -->
<div class="fixed top-[32px] left-0 w-full z-40 bg-background border-b border-outline-variant flex items-center justify-between px-margin-edge h-action-bar-height">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-primary">terminal</span>
<span class="font-headline text-headline text-primary tracking-tighter">~/root/usr/settings</span>
</div>
<div class="flex items-center">
<button class="p-2 hover:bg-surface-variant text-primary transition-colors">
<span class="material-symbols-outlined">close</span>
</button>
</div>
</div>
<!-- BottomNavBar (from Shared Components - as Terminal Footer) -->
<nav class="fixed bottom-0 left-0 w-full z-50 h-[24px] bg-surface-container-lowest border-t border-outline-variant flex justify-between items-center px-4">
<div class="flex items-center gap-4">
<span class="font-label-caps text-[10px] text-on-surface-variant">▌ NORMAL │ profile</span>
</div>
<div class="flex items-center gap-4">
<button class="flex items-center gap-1 group">
<span class="font-label-caps text-[10px] text-on-surface-variant group-hover:text-primary">[ESC] back</span>
</button>
</div>
</nav>
<!-- Decorative CRT Scanline overlay line -->
<div class="fixed top-0 left-0 w-full h-[1px] bg-primary opacity-20 pointer-events-none animate-pulse"></div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+271
View File
@@ -0,0 +1,271 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-variant": "#313538",
"surface-dim": "#101417",
"secondary-fixed-dim": "#bad073",
"surface-bright": "#363a3d",
"secondary-fixed": "#d5ec8c",
"secondary-container": "#435401",
"on-tertiary-fixed-variant": "#653173",
"surface-container-highest": "#313538",
"outline-variant": "#3f484e",
"error": "#fb9fb1",
"surface-container": "#202020",
"inverse-on-surface": "#2d3134",
"on-primary-fixed-variant": "#004c69",
"outline": "#505050",
"on-secondary": "#293500",
"suit-red": "#fb9fb1",
"inverse-primary": "#00668a",
"on-secondary-container": "#b2c86d",
"highlight-celebration": "#e1a3ee",
"warning": "#ddb26f",
"primary-fixed-dim": "#7ed0fe",
"info": "#12cfc0",
"primary-fixed": "#c4e7ff",
"highlight-valid": "#acc267",
"on-surface-variant": "#bfc8cf",
"on-tertiary": "#4c195b",
"background": "#101417",
"tertiary-container": "#e1a3ee",
"suit-black": "#d0d0d0",
"on-error-container": "#ffdad6",
"on-surface": "#d0d0d0",
"primary": "#a1dcff",
"error-container": "#93000a",
"secondary": "#bad073",
"surface": "#151515",
"primary-container": "#6fc2ef",
"suit-red-cb": "#6fc2ef",
"on-primary": "#003549",
"surface-container-low": "#181c1f",
"tertiary-fixed-dim": "#f0b0fc",
"surface-tint": "#7ed0fe",
"on-tertiary-container": "#683476",
"on-secondary-fixed": "#161e00",
"surface-container-lowest": "#0b0f11",
"on-tertiary-fixed": "#340043",
"surface-container-high": "#272a2d",
"on-error": "#690005",
"tertiary-fixed": "#fbd7ff",
"tertiary": "#f7c3ff",
"on-background": "#e0e3e6",
"on-secondary-fixed-variant": "#3c4d00",
"on-primary-fixed": "#001e2c",
"on-primary-container": "#004f6c",
"inverse-surface": "#e0e3e6"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"action-bar-height": "64px",
"stack-overlap": "2rem",
"gutter-card": "0.375rem",
"touch-target-min": "48dp",
"margin-edge": "1rem"
},
"fontFamily": {
"label-caps": ["JetBrains Mono"],
"hud-timer": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"body-md": ["Inter"],
"headline": ["JetBrains Mono"],
"card-rank": ["JetBrains Mono"]
},
"fontSize": {
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
.radial-segment {
clip-path: polygon(50% 50%, 100% 0, 100% 100%);
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
display: inline-block;
vertical-align: middle;
}
.scanline-bg {
background: repeating-linear-gradient(
0deg,
rgba(26, 26, 26, 1) 0px,
rgba(26, 26, 26, 1) 2px,
rgba(21, 21, 21, 1) 2px,
rgba(21, 21, 21, 1) 4px
);
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface font-body-md text-on-surface select-none overflow-hidden h-screen w-screen flex flex-col">
<!-- Underlying Game Tableau (Dimmed Background) -->
<main class="relative flex-grow opacity-30 grid grid-cols-7 gap-2 p-margin-edge pointer-events-none">
<!-- Top row: Foundation/Stock -->
<div class="col-span-1 aspect-[2/3] border border-dashed border-outline-variant bg-surface-container flex items-center justify-center">
<span class="material-symbols-outlined text-outline-variant">terminal</span>
</div>
<div class="col-span-1 aspect-[2/3] border border-dashed border-outline-variant"></div>
<div class="col-span-1"></div>
<div class="col-span-1 aspect-[2/3] border border-outline border-suit-red-cb flex items-center justify-center">
<span class="material-symbols-outlined text-suit-red-cb">favorite</span>
</div>
<div class="col-span-1 aspect-[2/3] border border-outline border-suit-black flex items-center justify-center">
<span class="material-symbols-outlined text-suit-black">backspace</span>
</div>
<div class="col-span-1 aspect-[2/3] border border-outline border-suit-red-cb flex items-center justify-center">
<span class="material-symbols-outlined text-suit-red-cb">diamond</span>
</div>
<div class="col-span-1 aspect-[2/3] border border-outline border-suit-black flex items-center justify-center">
<span class="material-symbols-outlined text-suit-black">spa</span>
</div>
<!-- Tableau piles -->
<div class="col-span-7 grid grid-cols-7 gap-2 mt-4">
<div class="space-y-[-120%]">
<div class="aspect-[2/3] bg-surface-container-high border border-outline p-1 flex flex-col justify-between">
<span class="font-card-rank text-card-rank text-suit-red-cb">K</span>
<span class="material-symbols-outlined self-end text-3xl rotate-180 text-suit-red-cb">diamond</span>
</div>
</div>
<div class="space-y-[-120%]">
<div class="aspect-[2/3] scanline-bg border border-outline relative">
<div class="absolute top-1 left-1 w-3 h-4 bg-primary"></div>
<span class="absolute bottom-1 right-1 font-label-caps text-[10px] text-primary">▌RS</span>
</div>
<div class="aspect-[2/3] bg-surface-container-high border border-outline p-1 flex flex-col justify-between">
<span class="font-card-rank text-card-rank text-suit-black">Q</span>
<span class="material-symbols-outlined self-end text-3xl rotate-180 text-suit-black" style="font-variation-settings: 'FILL' 1;">spa</span>
</div>
</div>
<div class="space-y-[-120%]">
<div class="aspect-[2/3] scanline-bg border border-outline"></div>
<div class="aspect-[2/3] scanline-bg border border-outline"></div>
<div class="aspect-[2/3] bg-surface-container-high border border-outline p-1 flex flex-col justify-between">
<span class="font-card-rank text-card-rank text-suit-red-cb">J</span>
<span class="material-symbols-outlined self-end text-3xl rotate-180 text-suit-red-cb" style="font-variation-settings: 'FILL' 1;">favorite</span>
</div>
</div>
<!-- More stacks... omitted for brevity as background -->
</div>
</main>
<!-- Radial Menu Overlay -->
<div class="fixed inset-0 z-50 bg-[#151515]/70 flex items-center justify-center overflow-hidden">
<div class="relative w-[280px] h-[280px] flex items-center justify-center">
<!-- Outer Circular Ring Shell -->
<div class="absolute inset-0 rounded-full border border-outline bg-surface-container overflow-hidden">
<!-- SVG Segments Construction -->
<svg class="w-full h-full transform -rotate-22.5" viewbox="0 0 100 100">
<!-- Slice 1 (UNDO) - Top / 12:00 -->
<!-- Active state: bg-primary-container/15, stroke-primary -->
<path d="M 50 50 L 50 0 A 50 50 0 0 1 85.35 14.65 Z" fill="#6fc2ef26" stroke="#6fc2ef" stroke-width="0.5" transform="rotate(-22.5, 50, 50)"></path>
<!-- Slice 2 (REDO) -->
<path d="M 50 50 L 85.35 14.65 A 50 50 0 0 1 100 50 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
<!-- Slice 3 (HINT) -->
<path d="M 50 50 L 100 50 A 50 50 0 0 1 85.35 85.35 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
<!-- Slice 4 (AUTO) -->
<path d="M 50 50 L 85.35 85.35 A 50 50 0 0 1 50 100 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
<!-- Slice 5 (NEW) -->
<path d="M 50 50 L 50 100 A 50 50 0 0 1 14.65 85.35 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
<!-- Slice 6 (PAUSE) -->
<path d="M 50 50 L 14.65 85.35 A 50 50 0 0 1 0 50 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
<!-- Slice 7 (STATS) -->
<path d="M 50 50 L 0 50 A 50 50 0 0 1 14.65 14.65 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
<!-- Slice 8 (SETTINGS) -->
<path d="M 50 50 L 14.65 14.65 A 50 50 0 0 1 50 0 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
</svg>
</div>
<!-- Labels and Icons Overlay -->
<div class="absolute inset-0 pointer-events-none">
<!-- 12:00 UNDO (ACTIVE) -->
<div class="absolute top-[12%] left-1/2 -translate-x-1/2 flex flex-col items-center text-primary">
<span class="material-symbols-outlined text-[24px]" data-icon="undo">undo</span>
<span class="font-label-caps text-[11px] mt-1">UNDO</span>
</div>
<!-- 1:30 REDO -->
<div class="absolute top-[22%] right-[12%] flex flex-col items-center text-on-surface">
<span class="material-symbols-outlined text-[24px]" data-icon="redo">redo</span>
<span class="font-label-caps text-[11px] mt-1">REDO</span>
</div>
<!-- 3:00 HINT -->
<div class="absolute top-1/2 right-[8%] -translate-y-1/2 flex flex-col items-center text-on-surface">
<span class="material-symbols-outlined text-[24px]" data-icon="lightbulb">lightbulb</span>
<span class="font-label-caps text-[11px] mt-1">HINT</span>
</div>
<!-- 4:30 AUTO -->
<div class="absolute bottom-[22%] right-[12%] flex flex-col items-center text-on-surface">
<span class="material-symbols-outlined text-[24px]" data-icon="double_arrow">double_arrow</span>
<span class="font-label-caps text-[11px] mt-1">AUTO</span>
</div>
<!-- 6:00 NEW -->
<div class="absolute bottom-[12%] left-1/2 -translate-x-1/2 flex flex-col items-center text-on-surface">
<span class="material-symbols-outlined text-[24px]" data-icon="add">add</span>
<span class="font-label-caps text-[11px] mt-1">NEW</span>
</div>
<!-- 7:30 PAUSE -->
<div class="absolute bottom-[22%] left-[12%] flex flex-col items-center text-on-surface">
<span class="material-symbols-outlined text-[24px]" data-icon="pause">pause</span>
<span class="font-label-caps text-[11px] mt-1">PAUSE</span>
</div>
<!-- 9:00 STATS -->
<div class="absolute top-1/2 left-[8%] -translate-y-1/2 flex flex-col items-center text-on-surface">
<span class="material-symbols-outlined text-[24px]" data-icon="bar_chart">bar_chart</span>
<span class="font-label-caps text-[11px] mt-1">STATS</span>
</div>
<!-- 10:30 SETTINGS -->
<div class="absolute top-[22%] left-[12%] flex flex-col items-center text-on-surface">
<span class="material-symbols-outlined text-[24px]" data-icon="settings">settings</span>
<span class="font-label-caps text-[11px] mt-1">SETTINGS</span>
</div>
</div>
<!-- Inner Hole -->
<div class="absolute w-20 h-20 rounded-full bg-surface-container border border-outline-variant flex flex-col items-center justify-center z-10">
<div class="font-headline text-[32px] text-primary leading-none"></div>
<div class="font-label-caps text-[10px] text-on-surface-variant tracking-widest mt-1">RADIAL</div>
</div>
</div>
<!-- Instructions (Bottom Floating) -->
<div class="absolute bottom-12 left-0 w-full flex flex-col items-center gap-4">
<div class="font-label-caps text-[12px] text-on-surface-variant tracking-wider">
DRAG TO SELECT · RELEASE TO ACTIVATE
</div>
<!-- Status Line (Vim style) -->
<div class="w-full h-8 bg-surface-container border-t border-outline-variant flex items-center justify-center">
<span class="font-label-caps text-[11px] text-on-surface-variant">
<span class="text-primary"></span> NORMAL │ radial · 1/8 selected
</span>
</div>
</div>
</div>
<!-- Hidden image for standard requirement compliance, though not visually used in this specific overlay task -->
<div class="hidden">
<img data-alt="A macro shot of a vintage terminal screen displaying green computer code and technical data. The lighting is low-key, with a soft glow emanating from the screen, highlighting the CRT scanlines and subtle reflections. The aesthetic is purely technical and retro-futuristic, focusing on precision and high-contrast digital artifacts. Deep blacks and vibrant green neon tones dominate the color palette, evoking a high-performance system environment." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAQuJUCOQev_BN72KyX0c-ylmW3DMZD-gOUlylYo3w1SrSpGnvorMvSUwe5oGPAgBgc050cCowC8f1QaxHEDN-DUkyCynOLhzrZHXyCJh2ebCWd6x1quLQwp0ffwbHsZW1-J2zAMuUydMNpEVmpHFQDij0yjVg6lxc6JdsC0etMoAWMhb61S3HUoDffSl-Q23N8Oc77r3dSf6kLFKAMAJCbXFz4nTaJKCKAwtMs62pLr6fd1jzMZrItH43RaO28uzMzvnGGZj3Miw"/>
</div>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+284
View File
@@ -0,0 +1,284 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Solitaire Replay Overlay</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-background": "#e0e3e6",
"suit-red-cb": "#6fc2ef",
"surface-container": "#202020",
"primary": "#a1dcff",
"tertiary-fixed-dim": "#f0b0fc",
"on-tertiary-fixed-variant": "#653173",
"surface-tint": "#7ed0fe",
"outline": "#505050",
"suit-black": "#d0d0d0",
"secondary": "#bad073",
"on-surface": "#d0d0d0",
"on-primary": "#003549",
"on-error-container": "#ffdad6",
"on-secondary-fixed-variant": "#3c4d00",
"surface-bright": "#363a3d",
"surface-variant": "#313538",
"secondary-container": "#435401",
"surface-container-highest": "#313538",
"surface-container-low": "#181c1f",
"primary-container": "#6fc2ef",
"on-error": "#690005",
"primary-fixed-dim": "#7ed0fe",
"tertiary-container": "#e1a3ee",
"on-tertiary-container": "#683476",
"suit-red": "#fb9fb1",
"highlight-celebration": "#e1a3ee",
"on-primary-fixed-variant": "#004c69",
"secondary-fixed": "#d5ec8c",
"primary-fixed": "#c4e7ff",
"on-primary-container": "#004f6c",
"secondary-fixed-dim": "#bad073",
"warning": "#ddb26f",
"on-secondary": "#293500",
"info": "#12cfc0",
"on-tertiary-fixed": "#340043",
"background": "#101417",
"surface-container-high": "#272a2d",
"surface-dim": "#101417",
"surface": "#151515",
"inverse-surface": "#e0e3e6",
"on-surface-variant": "#bfc8cf",
"error-container": "#93000a",
"tertiary": "#f7c3ff",
"inverse-primary": "#00668a",
"surface-container-lowest": "#0b0f11",
"inverse-on-surface": "#2d3134",
"on-primary-fixed": "#001e2c",
"highlight-valid": "#acc267",
"outline-variant": "#3f484e",
"tertiary-fixed": "#fbd7ff",
"on-secondary-fixed": "#161e00",
"error": "#fb9fb1",
"on-tertiary": "#4c195b",
"on-secondary-container": "#b2c86d"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"touch-target-min": "48dp",
"stack-overlap": "2rem",
"margin-edge": "1rem",
"action-bar-height": "64px",
"gutter-card": "0.375rem"
},
"fontFamily": {
"hud-timer": ["JetBrains Mono"],
"body-md": ["Inter"],
"label-caps": ["JetBrains Mono"],
"hud-score": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"card-rank": ["JetBrains Mono"]
},
"fontSize": {
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.card-back-pattern {
background-color: #151515;
background-image: repeating-linear-gradient(0deg, transparent, transparent 2px, #1a1a1a 2px, #1a1a1a 4px);
}
/* Custom mechanical transition style */
.mechanical-transition {
transition: all 120ms cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface text-on-surface font-body-md select-none overflow-hidden flex flex-col h-[844px] w-[390px] mx-auto border-x border-outline-variant">
<!-- Status Bar -->
<header class="h-8 bg-surface-container flex items-center justify-between px-4 border-b border-outline-variant flex-shrink-0">
<div class="flex items-center gap-2">
<span class="text-primary font-headline text-[14px]">▌replay.tsx</span>
</div>
<div class="flex items-center text-on-surface-variant font-label-caps text-[10px] tracking-wider uppercase">
GAME #2024-127 · 87 MOVES
</div>
</header>
<!-- Game Peek Band (Tableau) -->
<main class="h-[240px] relative bg-background overflow-hidden border-b border-outline-variant">
<!-- 7-Column Tableau (Dimmed 50%) -->
<div class="absolute inset-0 opacity-50 flex justify-around p-2 gap-1">
<!-- Tableau Columns 1-7 -->
<div class="flex-1 flex flex-col items-center">
<div class="w-full aspect-[2/3] border border-dashed border-outline-variant mb-1"></div>
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
</div>
<div class="flex-1 flex flex-col items-center">
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline relative">
<!-- Central Focused Card (Move 47) -->
<div class="absolute inset-0 z-20 opacity-100">
<!-- Shadow-less highlight using glow outline -->
<div class="w-full h-full bg-[#1a1a1a] border border-suit-red-cb ring-2 ring-suit-red-cb/40 flex flex-col justify-between p-1">
<div class="flex flex-col">
<span class="font-card-rank text-card-rank text-suit-red-cb">4</span>
<span class="material-symbols-outlined text-[14px] text-suit-red-cb" data-icon="diamond">diamond</span>
</div>
<div class="self-end rotate-180 flex flex-col">
<span class="font-card-rank text-card-rank text-suit-red-cb">4</span>
<span class="material-symbols-outlined text-[14px] text-suit-red-cb" data-icon="diamond">diamond</span>
</div>
</div>
<!-- Move Chip -->
<div class="absolute -top-6 left-1/2 -translate-x-1/2 bg-suit-red-cb px-2 py-0.5 rounded-sm">
<span class="text-surface font-label-caps text-[9px] font-bold">MOVE 47/87</span>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Playback Toolbar -->
<div class="h-16 bg-surface-container flex items-center justify-between px-4 border-b border-outline-variant">
<!-- Left: Timer -->
<div class="flex flex-col">
<span class="text-on-surface font-hud-timer text-[18px] font-bold leading-none">00:42</span>
<span class="text-[11px] text-[#a0a0a0] font-label-caps tracking-tighter">/ 02:18</span>
</div>
<!-- Center: Controls -->
<div class="flex items-center gap-4">
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary mechanical-transition" data-icon="skip_previous">skip_previous</button>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary mechanical-transition" data-icon="arrow_left">arrow_left</button>
<button class="material-symbols-outlined text-suit-red-cb text-[32px] mechanical-transition" data-icon="play_arrow">play_arrow</button>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary mechanical-transition" data-icon="arrow_right">arrow_right</button>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary mechanical-transition" data-icon="skip_next">skip_next</button>
</div>
<!-- Right: Speed -->
<div class="flex items-center bg-surface-variant border border-outline px-2 py-1 gap-1">
<span class="font-label-caps text-[14px] font-bold text-on-surface">1.0x</span>
<span class="material-symbols-outlined text-[16px] text-on-surface-variant" data-icon="unfold_more">unfold_more</span>
</div>
</div>
<!-- Scrub Bar Area -->
<div class="px-margin-edge pt-6 pb-8 bg-surface-container-low border-b border-outline-variant">
<div class="relative w-full h-1 bg-outline rounded-full">
<!-- Cyan Progress Track -->
<div class="absolute left-0 top-0 h-full bg-suit-red-cb" style="width: 54%;"></div>
<!-- Notches & Labels -->
<div class="absolute inset-0 flex justify-between">
<div class="relative">
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">0%</span>
</div>
<div class="relative">
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">25%</span>
</div>
<div class="relative">
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">50%</span>
</div>
<div class="relative">
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">75%</span>
</div>
<div class="relative">
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">100%</span>
</div>
</div>
<!-- Current Marker (54%) -->
<div class="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-suit-red-cb border border-surface" style="left: 54%;"></div>
<div class="absolute -top-4 left-[54%] -translate-x-1/2 text-[10px] text-suit-red-cb font-label-caps font-bold">47/87</div>
<!-- Win Marker (72%) -->
<div class="absolute top-0 w-[2px] h-3 bg-highlight-valid -translate-y-1" style="left: 72%;"></div>
<div class="absolute -bottom-5 left-[72%] -translate-x-1/2 text-[8px] text-highlight-valid font-label-caps font-bold whitespace-nowrap">WIN MOVE</div>
</div>
</div>
<!-- Move Log Card -->
<section class="flex-1 bg-surface-container p-4 overflow-y-auto">
<h3 class="font-label-caps text-label-caps text-on-surface-variant mb-4 flex items-center gap-2">
<span class="w-1.5 h-3 bg-primary block"></span>
MOVE LOG · 47/87
</h3>
<div class="flex flex-col font-label-caps text-[12px]">
<!-- Log Rows -->
<div class="flex items-center h-6 px-2 text-[#a0a0a0] border-b border-outline-variant/30">
<span class="w-8">44 |</span>
<span>5♥ → tableau col 3</span>
</div>
<div class="flex items-center h-6 px-2 text-[#a0a0a0] border-b border-outline-variant/30">
<span class="w-8">45 |</span>
<span>8♣ → tableau col 1</span>
</div>
<div class="flex items-center h-6 px-2 text-[#a0a0a0] border-b border-outline-variant/30">
<span class="w-8">46 |</span>
<span>stock cycle</span>
</div>
<!-- Highlighted Active Move -->
<div class="flex items-center h-6 px-2 bg-suit-red-cb text-surface-container font-bold">
<span class="w-8">▶ 47 |</span>
<span>4♦ → 5♣ on col 7</span>
</div>
<div class="flex items-center h-6 px-2 text-on-surface border-b border-outline-variant/30">
<span class="w-8">48 |</span>
<span class="material-symbols-outlined text-[14px] align-middle mr-1" data-icon="foundation">foundation</span> A♠ → foundation
</div>
<div class="flex items-center h-6 px-2 text-on-surface border-b border-outline-variant/30">
<span class="w-8">49 |</span>
<span class="material-symbols-outlined text-[14px] align-middle mr-1" data-icon="foundation">foundation</span> 2♠ → foundation
</div>
</div>
</section>
<!-- Footer -->
<footer class="h-6 bg-surface-container flex items-center justify-between px-4 border-t border-outline-variant flex-shrink-0 text-[10px] font-label-caps tracking-wider">
<div class="text-on-surface-variant">
<span class="text-primary"></span> NORMAL │ replay
</div>
<div class="text-on-surface-variant opacity-70">
[SPACE] play · [← →] scrub · [ESC]
</div>
</footer>
<!-- Overlay Background (For visualization of depth) -->
<div class="fixed inset-0 pointer-events-none border-[16px] border-surface-dim/40 z-50"></div>
</body></html>

Some files were not shown because too many files have changed in this diff Show More