A.7.5 Game Kit carousel image-mode + universal stat-block top-left chip + EMANATION/REVERSAL --secUser convention — TDD. Mid-session 2026-05-25 PM (Sprint A.7.5 of [[project-image-based-deck-face-rendering]] — slotted between A.7 polish + tomorrow's A.8 room.html). Three threads bundled: (1) Game Kit _tarot_fan.html carousel modal gets the image-mode branch + per-card FLIP-to-back for non-polarized image-equipped decks (Minchiate today; brings the carousel into parity w. the other 5 image-mode surfaces shipped in A.3-A.7); (2) the A.3 Q3-spec top-left rank+suit chip lands across all 4 stat-block surfaces (my_sign main / _applet-my-sign / _sea_stage modal / new game_kit fan stage), retrofitting work that A.3 explicitly deferred per the "Lower-priority follow-ups" list in the project memory; (3) chip + EMANATION/REVERSAL label adopt --secUser as the new universal color convention so the title (--quaUser/--terUser per arcana) stays the focal text + the chip-and-label header recedes visually.

(1) _tarot_fan.html image-mode branch — server-side `{% if card.deck_variant.has_card_images %}` gate: image-mode renders `<img class="sig-stage-card-img">` + (for non-polarized decks) a sibling `<img class="sig-stage-card-back-img">` for the FLIP-to-back affordance; text-mode keeps the existing `.fan-card-corner --tl/--br` + `.fan-card-face` scaffold unchanged (Earthman + RWS today; will be removed once both decks get artwork — user's plan: scrape RWS art tonight + Earthman public-domain paintings to follow; "shabby cardstock" non-equippable Earthman variant retains text rendering as legacy preservation). New `.fan-card.fan-card--image` marker class added to the shared image-mode comma-list selector (`_card-deck.scss:705-765`) so the carousel cards pick up the contour-stroke + depth-shadow filter chain + `.is-flipped-to-back` toggle for free — single SCSS source of truth across all 5 image-mode surfaces. Also added `data-arcana-key="{{ card.arcana }}"` + `data-image-url="{{ card.image_url|default:'' }}"` data-attrs to every fan-card so `StageCard.fromDataset` + `_setImageMode` flow w. no extra plumbing.

(2) Game Kit carousel JS rewiring (`game-kit.js`): `_populateStage` now also calls `StageCard.populateStatExtras(stageBlock, card)` so the carousel stat block gets title + arcana + chip populated on every card focus (previously the stage block had only the keyword list; the call site simply wasn't wired). SPIN handler gates the 180° card rotation behind `!active.classList.contains('fan-card--image')` — for image-mode cards SPIN now just toggles `.is-reversed` on the stat block to swap EMANATION ↔ REVERSAL content w/o rotating the artwork (user-spec 2026-05-25 PM: "monodecks shouldn't have gravity and levity polarity"; image artwork is symmetric + shouldn't be inverted by a UI cycle). New `_flipToBack` helper mirrors the my_sign.html A.5-polish-2 FLIP-to-back animation (rotateY 0→90→0 over 500ms, `.is-flipped-to-back` toggle at 250ms midpoint, `data-flipping` cleared at 500ms); the existing `_flipActive` dispatches to it via `active.querySelector('.sig-stage-card-back-img')` presence check (the back-img element is only server-rendered for non-polarized image-equipped decks, so its presence is the gate). Polarized text-mode (Earthman) keeps the existing polarity-cycle FLIP. Per-card-change cleanup also clears `.is-flipped-to-back` on every card so a back-flipped card returns to front when it leaves focus (mirrors the SPIN reset semantics).

(3) Top-left rank+suit chip retrofit (4 stat-block surfaces): the A.3 Q3 spec called for a chip but explicitly deferred to "Lower-priority follow-ups" in the project memory; user pulled it in this sprint as part of the carousel rewrite. New `.stat-face-header` flex wrapper holds the chip + EMANATION/REVERSAL label inline (chip is 2 rows tall, label is 1 — flex `align-items: flex-start` keeps them "vaguely inline" per spec). Chip mirrors the existing `.fan-card-corner` pattern: vertically stacked rank + suit-icon, no chrome (initial draft had a bordered pill — corrected per user clarification 2026-05-25 PM "vertically stacked, --secUser, in the top-left corner"). All 4 stat-block templates (my_sign.html / _applet-my-sign.html / _sea_stage.html / game_kit.html's `#id_fan_stage_block`) get the new header wrapper around their existing `.stat-face-label`. Applet renders the chip server-side from `card.corner_rank` + `card.suit_icon`; the other 3 surfaces leave the chip elements empty + populated by `StageCard.populateStatExtras` on each card focus (the helper now also walks `.stat-chip-rank` + `.stat-chip-icon` w. the same find-all + textContent / className pattern it already uses for title + arcana). Chip color is --secUser by default; polarity-aware overrides for surfaces whose gravity bg flips to --secUser (sig-stat-block / sea-stat-block / fan-stage-block) flip the chip to --priUser for visibility — same logical inversion the keyword list rules already use.

(4) Trump fa-hand-dots fallback in `TarotCard.suit_icon` — was reading the per-card `icon` field then returning `''` for any major arcana w/o an explicit override. Earthman's seed migration 0007 set `icon="fa-hand-dots"` on trumps 2+ as the universal trump symbol, but trumps 0/1 + every Minchiate trump fell through to empty + rendered the chip as just a number/numeral w. no icon below. Promoted the fallback into the model property (per-card override still wins via the `self.icon` branch), so every trump everywhere — chip, text-mode corner, future surfaces — gets a hand-with-dots glyph for free. Updated `TarotCardSuitIconTest.test_major_without_icon_returns_empty` → `test_major_without_icon_defaults_to_hand_dots`.

(5) EMANATION/REVERSAL → --secUser (user-spec 2026-05-25 PM, mid-sprint): label color was --terUser (gold) across all 4 surfaces; flipped to --secUser everywhere so the label recedes against the title (gold/--quaUser per arcana stays the focal text). Default in the shared `stat-block-shared` mixin + applet bespoke `.stat-face-label` rule both updated. Per-polarity overrides: levity (bg --priUser) → label --secUser everywhere; gravity overrides preserved at --quiUser on the 3 surfaces whose gravity bg flips to --secUser (sig-stat-block / sea-stat-block / fan-stage-block — --secUser label would be invisible against --secUser bg, so --quiUser stays for contrast); applet gravity bg is --priUser (just full alpha vs. the default 0.8 — different from the other surfaces) so its gravity override removed entirely, label uses the shared --secUser default in both polarities. User-confirmed visually 2026-05-25 PM: applet EMANATION now in --secUser (`rgb(162, 170, 173)`) matching the chip color — chip + label read as a coordinated header pair rather than competing w. the title.

Tests: 1314/1314 IT+UT total green (76s; +8 new in this sprint — 4 chip-presence ITs across the 4 stat-block surfaces, 3 _tarot_fan image-mode-branch ITs covering image-equipped + text-mode + polarized-image-equipped permutations, 1 UT-rename for the trump fa-hand-dots default). Surfaces NOT covered by ITs: SCSS layout (visual-only — verified live via Claudezilla on /gameboard/game-kit/ Minchiate carousel, /billboard/my-sign/ stage card, /billboard/ applet preview); JS-side chip-fill via populateStatExtras (covered transitively by the populateStatExtras existing call sites — no new test for the chip-specific code path since the test surface for stage-card.js is currently Jasmine-only via FanStageSpec.js, deferred). No new FT runs per [[feedback-ft-run-discipline]] — all changes are template / SCSS / JS / model property; IT coverage is comprehensive for the server-rendered surfaces + the visual verify covered the JS-populated surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-25 14:25:41 -04:00
parent 5a1acbd9ca
commit a9ad422b35
13 changed files with 475 additions and 67 deletions

View File

@@ -71,7 +71,12 @@
text-transform: uppercase;
letter-spacing: 0.09em;
opacity: 0.7;
color: rgba(var(--terUser), 1);
// Sprint A.7.5 user-spec 2026-05-25 PM — label color flipped from
// --terUser to --secUser so EMANATION/REVERSAL recedes visually and
// lets the title stay the focal text. Gravity-polarity overrides
// below still flip to --quiUser since the gravity stat-block bg is
// --secUser (a --secUser label would be invisible).
color: rgba(var(--secUser), 1);
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
// Sprint A.7-polish-3 — underline per user spec 2026-05-25 PM
// (the original A.3 Q3 lock referred to underlined Emanation /
@@ -82,6 +87,48 @@
text-underline-offset: 0.15em;
}
// Sprint A.7.5 — `.stat-face-header` is the flex wrapper holding the
// new top-left rank+suit chip inline w. the EMANATION/REVERSAL label.
// Per [[project-image-based-deck-face-rendering]]'s A.3 Q3 spec the
// chip is the chosen home for rank+suit on image-mode decks (where
// the card itself has no corners). On text-mode decks the chip is a
// benign duplicate of the corner rank+suit — won't bite until we
// delete the text-mode rendering entirely. User-spec 2026-05-25 PM:
// mirror the `.fan-card-corner` pattern (vertically stacked rank +
// icon, --secUser, no bg/border) rather than the original bordered-
// pill draft. The label sits inline-right of the chip, top-aligned
// (chip is 2 rows tall, label is 1 — "vaguely inline" per spec).
.stat-face-header {
display: flex;
align-items: flex-start;
gap: calc(var(--sig-card-w, 120px) * 0.05);
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
.stat-face-label { margin: 0; } // header owns the bottom margin now
}
.stat-face-chip {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: calc(var(--sig-card-w, 120px) * 0.012);
line-height: 1;
color: rgba(var(--secUser), 1);
font-weight: bold;
.stat-chip-rank {
font-size: calc(var(--sig-card-w, 120px) * 0.092);
}
i {
font-size: calc(var(--sig-card-w, 120px) * 0.092);
align-self: flex-start;
}
// Empty rank (JS-populated surfaces before first paint) — collapse
// the rank line so the chip doesn't leave a stray empty row.
.stat-chip-rank:empty { display: none; }
}
// Sprint A.7-polish-3 — title + arcana fields per locked Q3 spec.
// Title color keys off the stat-block's `data-arcana-key` attr (set by
// stage-card.js populateStatExtras OR server-side in the applet partial):
@@ -311,6 +358,11 @@
border-color: rgba(var(--priUser), 0.15);
color: rgba(var(--priUser), 1);
.stat-face-label { color: rgba(var(--quiUser), 1); }
// Sprint A.7.5 — chip uses --secUser by default; under gravity the
// stat-block bg IS --secUser, so the chip would be invisible. Flip
// to --priUser to stay on the opposite-polarity side per the
// [[feedback-card-polarity-convention]].
.stat-face-chip { color: rgba(var(--priUser), 1); }
.stat-keywords li {
color: rgba(var(--priUser), 1);
border-bottom-color: rgba(var(--priUser), 0.18);
@@ -320,7 +372,11 @@
background: rgba(var(--priUser), 1);
border-color: rgba(var(--terUser), 0.15);
color: rgba(var(--secUser), 1);
.stat-face-label { color: rgba(var(--terUser), 1); }
// Sprint A.7.5 — label drops to --secUser to match the new applet
// convention. Was --terUser; --secUser still has comfortable contrast
// against the --priUser bg + lets the title (--quaUser/--terUser per
// arcana) stay the focal text.
.stat-face-label { color: rgba(var(--secUser), 1); }
.stat-keywords li {
color: rgba(var(--quiUser), 1);
border-bottom-color: rgba(var(--terUser), 0.18);
@@ -705,7 +761,8 @@ html:has(.sig-backdrop) {
.sig-stage-card.sig-stage-card--image,
.my-sign-applet-card.my-sign-applet-card--image,
.my-sea-slot.my-sea-slot--image,
.sea-card-slot.sea-card-slot--image {
.sea-card-slot.sea-card-slot--image,
.fan-card.fan-card--image {
--img-stroke-color: rgba(var(--quiUser), 1);
background: transparent;
border: 0;
@@ -1176,10 +1233,10 @@ html:has(.sig-backdrop) {
.sig-stage-card .fan-card-reversal-qualifier,
.sig-stage-card .sig-qualifier-above,
.sig-stage-card .sig-qualifier-below { color: rgba(var(--quiUser), 1); }
// Stat-face label: levity stat-block bg is --priUser (opposite of levity card's
// --secUser bg), so the label takes the gravity-card text color (--terUser) to
// stay legible against the dark stat-block.
.sig-stat-block .stat-face-label { color: rgba(var(--terUser), 1); }
// Stat-face label: levity stat-block bg is --priUser. Per A.7.5 user-spec
// 2026-05-25 PM the label uses --secUser (was --terUser) so EMANATION /
// REVERSAL recedes against the title — same convention as the applet.
.sig-stat-block .stat-face-label { color: rgba(var(--secUser), 1); }
// Upright + reversal title glow — levity. Drop-shadow is WHITE here (was 0,0,0
// at 0.55) because the inverted-frame levity card uses a light --secUser bg,
// so a dark drop shadow reads as harsh smudge under the --quiUser title text.
@@ -1218,6 +1275,9 @@ html:has(.sig-backdrop) {
// --priUser bg), so the label takes the levity-card text color (--quiUser) to
// stay legible against the lighter stat-block.
.sig-stat-block .stat-face-label { color: rgba(var(--quiUser), 1); }
// Sprint A.7.5 — chip flips to --priUser under gravity (default --secUser
// would be invisible on the --secUser stat-block bg).
.sig-stat-block .stat-face-chip { color: rgba(var(--priUser), 1); }
// Upright + reversal title glow — gravity
.sig-stage-card .fan-card-name,
.sig-stage-card .sig-qualifier-above,
@@ -2089,6 +2149,8 @@ $_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .f
background: rgba(var(--secUser), 0.85);
border-color: rgba(var(--priUser), 0.15);
.stat-face-label { color: rgba(var(--quiUser), 1); }
// Sprint A.7.5 — chip flips to --priUser under gravity (bg is --secUser).
.stat-face-chip { color: rgba(var(--priUser), 1); }
.stat-keywords li {
color: rgba(var(--priUser), 1);
border-bottom-color: rgba(var(--priUser), 0.18);
@@ -2097,7 +2159,9 @@ $_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .f
.sea-stage--levity .sea-stat-block {
background: rgba(var(--priUser), 0.85);
border-color: rgba(var(--terUser), 0.15);
.stat-face-label { color: rgba(var(--terUser), 1); }
// Sprint A.7.5 — label flipped to --secUser (was --terUser) to match
// the new applet convention. --secUser still contrasts well w. --priUser bg.
.stat-face-label { color: rgba(var(--secUser), 1); }
.stat-keywords li {
color: rgba(var(--quiUser), 1);
border-bottom-color: rgba(var(--terUser), 0.18);