diff --git a/src/apps/billboard/tests/integrated/test_views.py b/src/apps/billboard/tests/integrated/test_views.py index 858d2ad..02bec10 100644 --- a/src/apps/billboard/tests/integrated/test_views.py +++ b/src/apps/billboard/tests/integrated/test_views.py @@ -919,12 +919,12 @@ class MySignViewTest(TestCase): face = parsed.cssselect(f".sig-stat-block .{face_cls}") self.assertEqual(len(face), 1, f"expected one {face_cls}") [header] = face[0].cssselect(".stat-face-header") - [chip] = header.cssselect(".stat-face-chip") - [_rank] = chip.cssselect(".stat-chip-rank") - [_icon] = chip.cssselect("i.stat-chip-icon") - # The label still lives inside the header (now flex-laid-out - # inline w. the chip rather than as a bare child of the face). - [_label] = header.cssselect(".stat-face-label") + # Polish-4 — header is a 2-row vertical stack: rank on row 1 + # (direct child), icon+label inside `.stat-chip-tag` on row 2. + [_rank] = header.cssselect(".stat-chip-rank") + [_tag] = header.cssselect(".stat-chip-tag") + [_icon] = _tag.cssselect("i.stat-chip-icon") + [_label] = _tag.cssselect(".stat-face-label") def test_save_sign_get_redirects_back_to_picker(self): response = self.client.get(reverse("billboard:save_sign")) @@ -1128,13 +1128,15 @@ class BillboardAppletMySignTest(TestCase): parsed = lxml.html.fromstring(response.content) [block] = parsed.cssselect(".my-sign-applet-stat-block") [header] = block.cssselect(".stat-face-header") - [chip] = header.cssselect(".stat-face-chip") - [rank] = chip.cssselect(".stat-chip-rank") + # Polish-4 — rank is a direct child of header (own row); icon lives + # inside `.stat-chip-tag` (row-2 inline w. the EMANATION label). + [rank] = header.cssselect(".stat-chip-rank") # Court middle cards have single-letter corner ranks (M/J/Q/K) per # TarotCard.corner_rank — pin presence, not the exact value (which # depends on which middle court personal_sig_cards returns first). self.assertTrue(rank.text and rank.text.strip()) + [tag] = header.cssselect(".stat-chip-tag") + [icon] = tag.cssselect("i.stat-chip-icon") # Middle court has a suit, so the suit-icon `` is present + carries # the canonical FA class for the suit (fa-wand-sparkles for BRANDS etc). - [icon] = chip.cssselect("i.stat-chip-icon") self.assertTrue(any(cls.startswith("fa-") for cls in (icon.get("class") or "").split())) diff --git a/src/apps/gameboard/static/apps/gameboard/game-kit.js b/src/apps/gameboard/static/apps/gameboard/game-kit.js index 88c7dcf..d415198 100644 --- a/src/apps/gameboard/static/apps/gameboard/game-kit.js +++ b/src/apps/gameboard/static/apps/gameboard/game-kit.js @@ -389,12 +389,13 @@ var GameKit = (function () { stageBlock.classList.toggle('is-reversed'); var active = cards[currentIndex]; if (!active) return; - // Sprint A.7.5 — image-mode cards don't rotate 180° on SPIN. - // The artwork is the canonical card; spinning it would invert - // the image visually w. no equivalent text-content swap (the - // text scaffold isn't rendered in image-mode). Stat block still - // flips via `.is-reversed` above to swap EMANATION ↔ REVERSAL. - if (active.classList.contains('fan-card--image')) return; + // Sprint A.7.5-polish-4 — image-mode + text-mode share the + // same SPIN path: toggle `.stage-card--reversed` + reset + // inline transform. The existing `transition: transform + // 0.18s ease-out` on `.fan-card` (set in updateFan) handles + // the visual rotation — no Element.animate layered on top, + // which had caused a double/triple-flip when the JS anim + // overlay + the CSS transition both fired. active.classList.toggle('stage-card--reversed'); var t = cardTransform(0); var spin = active.classList.contains('stage-card--reversed') ? ' rotate(180deg)' : ''; diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index 6f99100..5301907 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -781,10 +781,13 @@ class GameKitViewTest(TestCase): face = self.parsed.cssselect(f"#id_fan_stage_block .{face_cls}") self.assertEqual(len(face), 1, f"expected one {face_cls}") [header] = face[0].cssselect(".stat-face-header") - [_chip] = header.cssselect(".stat-face-chip") + # Polish-4 — rank is a direct child of the header (own row); + # icon + label live in `.stat-chip-tag` (row-2 inline). No + # `.stat-face-chip` wrapper. [_rank] = header.cssselect(".stat-chip-rank") - [_icon] = header.cssselect("i.stat-chip-icon") - [_label] = header.cssselect(".stat-face-label") + [_tag] = header.cssselect(".stat-chip-tag") + [_icon] = _tag.cssselect("i.stat-chip-icon") + [_label] = _tag.cssselect(".stat-face-label") [_title] = face[0].cssselect(".stat-face-title") [_arcana] = face[0].cssselect(".stat-face-arcana") @@ -1007,10 +1010,10 @@ class MySeaViewTest(TestCase): face = parsed.cssselect(f".sea-stat-block .{face_cls}") self.assertEqual(len(face), 1, f"expected one {face_cls}") [header] = face[0].cssselect(".stat-face-header") - [chip] = header.cssselect(".stat-face-chip") - [_rank] = chip.cssselect(".stat-chip-rank") - [_icon] = chip.cssselect("i.stat-chip-icon") - [_label] = header.cssselect(".stat-face-label") + [_rank] = header.cssselect(".stat-chip-rank") + [_tag] = header.cssselect(".stat-chip-tag") + [_icon] = _tag.cssselect("i.stat-chip-icon") + [_label] = _tag.cssselect(".stat-face-label") class MySeaDrawSeaLandingViewTest(TestCase): diff --git a/src/static_src/scss/_billboard.scss b/src/static_src/scss/_billboard.scss index 55789ea..d4e181b 100644 --- a/src/static_src/scss/_billboard.scss +++ b/src/static_src/scss/_billboard.scss @@ -631,36 +631,44 @@ body.page-billposts { // 3 stat-block surfaces (sig-stat-block, sea-stat-block, fan-stage- // block) updated to match. color: rgba(var(--secUser), 1); - margin: 0 0 calc(var(--applet-card-w) * 0.06); - text-decoration: underline; - text-underline-offset: 0.15em; + margin: 0; + // text-decoration dropped in polish-4 — the `.stat-face-header` + // border-bottom (below) now underscores the whole header unit. } - // Sprint A.7.5 — same shape as the shared mixin's `.stat-face-header` - // + `.stat-face-chip` but sized off the applet's `--applet-card-w` - // container-query var rather than `--sig-card-w`. Chip mirrors the - // `.fan-card-corner` pattern (vertically stacked rank + suit-icon, - // --secUser, no chrome) per user-spec 2026-05-25 PM. + // Sprint A.7.5-polish-4 — header is a 2-row vertical stack: rank on + // row 1 (room for long Roman numerals), icon + EMANATION/REVERSAL on + // row 2 inline. Border-bottom underscores both rows as one header + // unit, separating it from the title block below. Sized off the + // applet's `--applet-card-w` container-query var rather than `--sig- + // card-w`. Mirrors the shared mixin shape; deliberate parallel since + // applet sizing is independent. .stat-face-header { display: flex; - align-items: flex-start; - gap: calc(var(--applet-card-w) * 0.06); - margin: 0 0 calc(var(--applet-card-w) * 0.06); - - .stat-face-label { margin: 0; } - } - .stat-face-chip { - display: inline-flex; flex-direction: column; - align-items: center; - gap: calc(var(--applet-card-w) * 0.012); + align-items: flex-start; + gap: calc(var(--applet-card-w) * 0.015); + margin: 0 0 calc(var(--applet-card-w) * 0.06); + padding-bottom: calc(var(--applet-card-w) * 0.035); + border-bottom: 0.05rem solid rgba(var(--secUser), 0.4); + } + .stat-chip-rank { + font-size: calc(var(--applet-card-w) * 0.12); + font-weight: bold; line-height: 1; color: rgba(var(--secUser), 1); - font-weight: bold; + &:empty { display: none; } + } + .stat-chip-tag { + display: inline-flex; + align-items: baseline; + gap: calc(var(--applet-card-w) * 0.04); + line-height: 1; - .stat-chip-rank { font-size: calc(var(--applet-card-w) * 0.105); } - i { font-size: calc(var(--applet-card-w) * 0.105); align-self: flex-start; } - .stat-chip-rank:empty { display: none; } + i { + font-size: calc(var(--applet-card-w) * 0.085); + color: rgba(var(--secUser), 1); + } } // Sprint A.7-polish-3 — title + arcana in applet stat-block per diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 968b415..8ef0614 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -53,7 +53,13 @@ .stat-face { display: none; - padding: calc(var(--sig-card-w, 120px) * 0.37) + // Sprint A.7.5-polish-4 — top-pinned content per user-spec 2026-05-25 PM + // ("pin the number/alphanumeric at the top and the rest of the content + // cascades down from it, instead of pinning the arcana type in the center + // and stacking the rest of the content atop it"). Was top: 0.37 of card-w + // (visually-centered the arcana mid-stat-block); now uniform 0.1 so the + // chip header sits at the actual top edge + cascade flows down. + padding: calc(var(--sig-card-w, 120px) * 0.1) calc(var(--sig-card-w, 120px) * 0.1) calc(var(--sig-card-w, 120px) * 0.08); } @@ -77,72 +83,65 @@ // 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 / - // Reversal headers in the image-mode stat block; same spec re- - // applied universally here so non-image-mode stat blocks get the - // same visual treatment). - text-decoration: underline; - text-underline-offset: 0.15em; + margin: 0; + // text-decoration: underline dropped in polish-4 — the new + // `.stat-face-header` border-bottom underscores the whole header + // (chip + icon + label) as a single visual unit, separating it + // from the title block below. } - // 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). + // Sprint A.7.5-polish-4 — header is now a two-row vertical stack so + // long Roman numerals (e.g. XXVIII) get their own line w. room to + // breathe; row-2 holds the suit-icon + EMANATION/REVERSAL label + // inline (the icon is always 1 char so it never overflows). The + // border-bottom underscores both rows as one header unit, replacing + // the prior per-label text-decoration: underline. .stat-face-header { display: flex; + flex-direction: column; align-items: flex-start; - gap: calc(var(--sig-card-w, 120px) * 0.05); + gap: calc(var(--sig-card-w, 120px) * 0.02); margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07); - - .stat-face-label { margin: 0; } // header owns the bottom margin now + padding-bottom: calc(var(--sig-card-w, 120px) * 0.04); + border-bottom: 0.05rem solid rgba(var(--secUser), 0.4); } - .stat-face-chip { - display: inline-flex; - flex-direction: column; - align-items: center; - gap: calc(var(--sig-card-w, 120px) * 0.012); + .stat-chip-rank { + font-size: calc(var(--sig-card-w, 120px) * 0.105); + font-weight: bold; line-height: 1; color: rgba(var(--secUser), 1); - font-weight: bold; + &:empty { display: none; } + } + + .stat-chip-tag { + display: inline-flex; + align-items: baseline; + gap: calc(var(--sig-card-w, 120px) * 0.04); + line-height: 1; - .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; + font-size: calc(var(--sig-card-w, 120px) * 0.083); + color: rgba(var(--secUser), 1); } - - // 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): // - MAJOR → --terUser (gold) - // - MINOR / MIDDLE → --quiUser (cream) - // Matches the contour-stroke color on the image-mode card so card + - // title read as a coordinated pair. + // - MINOR / MIDDLE → --quaUser (bright yellow-gold, user-spec 2026- + // 05-25 PM "only the My Sign applet has --quaUser as a font color; + // the rest are --quiUser. Let's change the latter to match the + // former" — applet's `.stat-face-title` was already --quaUser; + // shared mixin now matches so all 4 stat-block surfaces unify). .stat-face-title { font-size: calc(var(--sig-card-w, 120px) * 0.105); font-weight: 700; line-height: 1.15; margin: 0 0 calc(var(--sig-card-w, 120px) * 0.03); text-wrap: balance; - color: rgba(var(--quiUser), 1); + color: rgba(var(--quaUser), 1); } [data-arcana-key="MAJOR"] .stat-face-title { color: rgba(var(--terUser), 1); @@ -1301,6 +1300,14 @@ html:has(.sig-backdrop) { background: rgba(var(--secUser), 1); border-color: rgba(var(--priUser), 0.6); color: rgba(var(--priUser), 1); // currentColor propagates to .fan-corner-rank + i + + // Sprint A.7.5-polish-4 — same image-mode override as the base rule + // above. Without this the 0,3,0 levity rule's --secUser bg would + // re-clothe the sea-sig-card under levity even in image mode. + &.sig-stage-card--image { + background: transparent; + border: 0; + } } // ─── Sig select: landscape overrides ───────────────────────────────────────── @@ -1697,6 +1704,19 @@ $sea-card-h: 6.5rem; padding: 0.25rem; overflow: hidden; + // Sprint A.7.5-polish-4 — image-mode override. `.sig-stage-card.sea-sig- + // card` (0,2,0) matches the shared `.sig-stage-card.sig-stage-card--image` + // comma-list rule's specificity but source-loses to it — so we re-state + // the transparency here AT 0,3,0 (parent + 2 sibling classes). Mirrors + // the my_sign-applet pattern in `_billboard.scss`. Filter chain on + // `.sig-stage-card-img` is still inherited from the shared rule (the + // image-mode rule's img descendant selector doesn't lose anywhere). + &.sig-stage-card--image { + background: transparent; + border: 0; + padding: 0; + } + .fan-card-face { flex: 1; display: flex; diff --git a/src/templates/apps/billboard/_partials/_applet-my-sign.html b/src/templates/apps/billboard/_partials/_applet-my-sign.html index 4a5fed0..928c900 100644 --- a/src/templates/apps/billboard/_partials/_applet-my-sign.html +++ b/src/templates/apps/billboard/_partials/_applet-my-sign.html @@ -79,11 +79,11 @@ {# rendering]]'s A.3 Q3 spec. Server-rendered (read-only #} {# applet — no JS populate path). #}
- - {{ card.corner_rank }} + {{ card.corner_rank }} +
{% if card.suit_icon %}{% endif %} - -

Emanation

+

Emanation

+

{{ card.name }}

{{ card.get_arcana_display }}

diff --git a/src/templates/apps/billboard/my_sign.html b/src/templates/apps/billboard/my_sign.html index 556047a..158742e 100644 --- a/src/templates/apps/billboard/my_sign.html +++ b/src/templates/apps/billboard/my_sign.html @@ -90,11 +90,11 @@ {# populateStatExtras fills `.stat-chip-rank` + `.stat-chip-icon`.#}
- - + +
- -

Emanation

+

Emanation

+

@@ -102,11 +102,11 @@
- - + +
- -

Reversal

+

Reversal

+

diff --git a/src/templates/apps/gameboard/_partials/_sea_stage.html b/src/templates/apps/gameboard/_partials/_sea_stage.html index aa35547..17c9d23 100644 --- a/src/templates/apps/gameboard/_partials/_sea_stage.html +++ b/src/templates/apps/gameboard/_partials/_sea_stage.html @@ -51,11 +51,11 @@ {# card.js populateStatExtras fills both faces' chips identically.#}
- - + +
- -

Emanation

+

Emanation

+

@@ -63,11 +63,11 @@
- - + +
- -

Reversal

+

Reversal

+

diff --git a/src/templates/apps/gameboard/game_kit.html b/src/templates/apps/gameboard/game_kit.html index 426e0d4..a26e6be 100644 --- a/src/templates/apps/gameboard/game_kit.html +++ b/src/templates/apps/gameboard/game_kit.html @@ -30,11 +30,11 @@ {# stage-card.js's populateStatExtras on each card focus change. #}
- - + +
- -

Emanation

+

Emanation

+

@@ -42,11 +42,11 @@
- - + +
- -

Reversal

+

Reversal

+