From dd99364b78c289f97c19794a5b5977d38bffc71a Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 25 May 2026 01:58:36 -0400 Subject: [PATCH] =?UTF-8?q?A.6=20+=20A.7=20billboard=20My=20Sign=20applet?= =?UTF-8?q?=20+=20gameboard=20My=20Sea=20applet=20image-rendering=20+=20ap?= =?UTF-8?q?plet-level=20FLIP-to-back=20=E2=80=94=20TDD.=20Sprints=20A.6=20?= =?UTF-8?q?+=20A.7=20of=20[[project-image-based-deck-face-rendering]]:=20r?= =?UTF-8?q?olls=20image-mode=20out=20to=20the=20two=20card-rendering=20app?= =?UTF-8?q?lets=20(My=20Sign=20on=20/billboard/,=20My=20Sea=20on=20/gamebo?= =?UTF-8?q?ard/).=20Both=20reuse=20the=20shared=20`.sig-stage-card.sig-sta?= =?UTF-8?q?ge-card--image`=20SCSS=20contract=20via=20a=20comma-list=20sele?= =?UTF-8?q?ctor=20extension=20covering=20the=20parallel=20container=20clas?= =?UTF-8?q?ses=20(`.my-sign-applet-card.my-sign-applet-card--image`=20+=20?= =?UTF-8?q?`.my-sea-slot.my-sea-slot--image`)=20=E2=80=94=20single=20sourc?= =?UTF-8?q?e=20of=20truth=20for=20the=20contour-stroke=20drop-shadow=20cha?= =?UTF-8?q?in=20+=20tray-card=20silhouette=20black=20depth=20shadow=20+=20?= =?UTF-8?q?.is-flipped-to-back=20visibility=20toggle=20+=20the=20`--img-st?= =?UTF-8?q?roke-color`=20arcana-keyed=20CSS=20prop.=20Templates=20branch?= =?UTF-8?q?=20server-side=20on=20`card.deck=5Fvariant.has=5Fcard=5Fimages`?= =?UTF-8?q?:=20image-mode=20renders=20``=20w.=20the=20marker=20c?= =?UTF-8?q?lass=20+=20`data-arcana-key`=20attr;=20text=20mode=20keeps=20th?= =?UTF-8?q?e=20existing=20fan-card-corner=20+=20fan-card-face=20scaffold?= =?UTF-8?q?=20unchanged.=20SCSS=20import-order=20quirk:=20`=5Fcard-deck.sc?= =?UTF-8?q?ss`=20imports=20BEFORE=20both=20`=5Fbillboard.scss`=20(which=20?= =?UTF-8?q?nests=20`.my-sign-applet-card`=20inside=20`.my-sign-applet-body?= =?UTF-8?q?`=20for=20container=20queries)=20and=20`=5Fgameboard.scss`=20(w?= =?UTF-8?q?hich=20nests=20`.my-sea-slot--filled.--gravity/--levity`=20insi?= =?UTF-8?q?de=20`#id=5Fapplet=5Fmy=5Fsea`=20w.=20specificity=201,2,0).=20T?= =?UTF-8?q?he=20shared=20top-level=20image-mode=20rule=20at=200,2,0=20lose?= =?UTF-8?q?s=20on=20bg/border/padding=20to=20those=20nested=20base=20rules?= =?UTF-8?q?,=20so=20each=20app's=20stylesheet=20gets=20a=20parallel=20`&.-?= =?UTF-8?q?-image=20{=20background:=20transparent;=20border:=200;=20paddin?= =?UTF-8?q?g:=200=20}`=20override=20inside=20its=20own=20nest.=20The=20fil?= =?UTF-8?q?ter-chain=20rules=20on=20`.sig-stage-card-img`=20(descendant=20?= =?UTF-8?q?selector=20inside=20the=20shared=20rule)=20DO=20win=20since=20t?= =?UTF-8?q?he=20apps=20don't=20restyle=20that=20class=20=E2=80=94=20only?= =?UTF-8?q?=20the=20outer=20container=20needs=20the=20parallel=20override.?= =?UTF-8?q?=20Sprint=20A.6=20bonus:=20applet-level=20FLIP=20btn=20for=20no?= =?UTF-8?q?n-polarized=20image-equipped=20decks=20(Minchiate=20today).=20M?= =?UTF-8?q?irrors=20the=20my=5Fsign.html=20main=20page=20A.5-polish-2=20FL?= =?UTF-8?q?IP-to-back=20contract=20=E2=80=94=20`.my-sign-applet-flip-btn`?= =?UTF-8?q?=20nested=20inside=20the=20.--image=20card=20so=20absolute=20po?= =?UTF-8?q?sitioning=20anchors=20to=20the=20card=20bounds;=20inline=20``=20IIFE=20(gated=20inside=20the=20sig-present=20{%=20with?= =?UTF-8?q?=20card=20%}=20scope=20to=20keep=20`card`=20in=20lexical=20reac?= =?UTF-8?q?h=20+=20prevent=20the=20JS=20selector=20string=20leaking=20into?= =?UTF-8?q?=20the=20no-sig=20DOM=20where=20`assertNotContains=20"my-sign-a?= =?UTF-8?q?pplet-card"`=20ITs=20catch=20it)=20attaches=20a=20click=20handl?= =?UTF-8?q?er=20that=20runs=20the=20same=20rotateY=200=E2=86=9290=E2=86=92?= =?UTF-8?q?0=20animation,=20toggles=20`.is-flipped-to-back`=20at=20the=20h?= =?UTF-8?q?alfway=20point,=20and=20clears=20`data-flipping`=20at=20end;=20?= =?UTF-8?q?SCSS=20`.my-sign-applet-card[data-flipping]=20.my-sign-applet-f?= =?UTF-8?q?lip-btn=20{=20opacity:=200;=20pointer-events:=20none=20}`=20hid?= =?UTF-8?q?es=20the=20btn=20mid-spin.=20Critical=20scope=20bug=20caught=20?= =?UTF-8?q?+=20fixed=20during=20browser=20verify:=20initial=20draft=20had?= =?UTF-8?q?=20the=20script=20BLOCK=20+=20its=20`{%=20if=20card.deck=5Fvari?= =?UTF-8?q?ant.has=5Fcard=5Fimages=20%}`=20gate=20placed=20AFTER=20the=20`?= =?UTF-8?q?{%=20endwith=20%}`=20closing=20tag=20=E2=80=94=20`card`=20was?= =?UTF-8?q?=20out=20of=20scope=20at=20the=20`{%=20if=20%}`=20evaluation,?= =?UTF-8?q?=20Django=20treats=20undefined=20vars=20as=20empty=20string,=20?= =?UTF-8?q?the=20gate=20evaluated=20falsy,=20and=20the=20script=20NEVER=20?= =?UTF-8?q?rendered=20(the=20FLIP=20btn=20rendered=20fine=20since=20it=20w?= =?UTF-8?q?as=20inside=20the=20with=20block,=20but=20no=20JS=20handler=20?= =?UTF-8?q?=E2=86=92=20click=20did=20nothing=20but=20the=20CSS=20depress?= =?UTF-8?q?=20animation).=20Fix:=20move=20`{%=20endwith=20%}`=20to=20AFTER?= =?UTF-8?q?=20the=20script=20gate=20so=20`card`=20is=20still=20in=20scope.?= =?UTF-8?q?=207=20new=20ITs=20total:=202=20in=20`BillboardAppletMySignTest?= =?UTF-8?q?`=20(image-equipped=20Minchiate=20renders=20`--image`=20class?= =?UTF-8?q?=20+=20img=20+=20correct=20asset=20URL=20+=20lacks=20text=20sca?= =?UTF-8?q?ffold;=20Earthman=20keeps=20the=20text=20scaffold=20+=20lacks?= =?UTF-8?q?=20`--image`);=203=20in=20`BillboardMySignViewTest`=20(data-dec?= =?UTF-8?q?k-polarized=20attr=20present;=20back-img=20element=20renders=20?= =?UTF-8?q?for=20non-polarized=20image=20deck;=20polarized=20deck=20omits?= =?UTF-8?q?=20it);=201=20in=20`GameboardViewTest`=20(image-equipped=20Minc?= =?UTF-8?q?hiate=20slot=20renders=20`--image`=20+=20img=20+=20lacks=20text?= =?UTF-8?q?=20scaffold);=20plus=20regression=20coverage=20on=20the=20no-si?= =?UTF-8?q?g=20empty-state=20assertion=20that=20originally=20caught=20the?= =?UTF-8?q?=20script-scope=20bug=20(assertNotContains=20validates=20the=20?= =?UTF-8?q?script=20doesn't=20leak=20in=20the=20no-sig=20case).=20Tests:?= =?UTF-8?q?=206=20new=20ITs=20green;=201306/1306=20IT+UT=20total=20green?= =?UTF-8?q?=20(72s;=20+6=20from=20bdf6a25's=201303=20=E2=80=94=20minus=203?= =?UTF-8?q?=20dups=20since=20some=20ITs=20were=20counted=20across=20both?= =?UTF-8?q?=20A.6=20+=20A.5-polish-2=20runs).=20Visual=20verify=20by=20use?= =?UTF-8?q?r=202026-05-25=20PM:=20stage=20card=20image=20renders=20cleanly?= =?UTF-8?q?;=20FLIP=20cycles=20to=20back=20image=20+=20back=20via=20animat?= =?UTF-8?q?ion;=20FLIP=20btn=20hides=20during=20500ms=20spin;=20placeholde?= =?UTF-8?q?r=20dim=20styling=20correctly=20distinguishes=20no-deck=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../billboard/tests/integrated/test_views.py | 59 +++++++++ .../gameboard/tests/integrated/test_views.py | 41 ++++++ src/static_src/scss/_billboard.scss | 31 +++++ src/static_src/scss/_card-deck.scss | 4 +- src/static_src/scss/_gameboard.scss | 19 ++- .../billboard/_partials/_applet-my-sign.html | 121 +++++++++++++----- .../gameboard/_partials/_applet-my-sea.html | 72 ++++++----- 7 files changed, 284 insertions(+), 63 deletions(-) diff --git a/src/apps/billboard/tests/integrated/test_views.py b/src/apps/billboard/tests/integrated/test_views.py index 7ac59c3..1a63643 100644 --- a/src/apps/billboard/tests/integrated/test_views.py +++ b/src/apps/billboard/tests/integrated/test_views.py @@ -1034,3 +1034,62 @@ class BillboardAppletMySignTest(TestCase): self.assertContains(response, "my-sign-applet-card--gravity") if target.gravity_qualifier: self.assertContains(response, target.gravity_qualifier) + + def test_my_sign_applet_renders_image_when_deck_has_card_images(self): + """Sprint A.6 — applet card carries `.my-sign-applet-card--image` + + an child when the user's equipped deck is + image-equipped (Minchiate today). Shares the contour-stroke + depth + shadow SCSS w. my_sign.html's stage-card-image via comma-list selector. + Text scaffold (fan-card-corner / fan-card-face) is NOT rendered in + image mode — server-side template `{% if/else %}` branch.""" + from apps.epic.models import DeckVariant, TarotCard + import lxml.html + minchiate = DeckVariant.objects.get(slug="minchiate-fiorentine-1860-1890") + self.user.is_superuser = True + self.user.save() + from apps.drama.models import Note + Note.grant_if_new(self.user, "super-nomad") + Note.grant_if_new(self.user, "super-schizo") + self.user.unlocked_decks.add(minchiate) + self.user.equipped_deck = minchiate + il_matto = TarotCard.objects.get(deck_variant=minchiate, slug="il-matto") + self.user.significator = il_matto + self.user.save(update_fields=["equipped_deck", "significator"]) + + response = self.client.get("/billboard/") + parsed = lxml.html.fromstring(response.content) + [card_el] = parsed.cssselect(".my-sign-applet-card") + self.assertIn("my-sign-applet-card--image", card_el.get("class", "")) + self.assertEqual(card_el.get("data-arcana-key"), "MAJOR") + [img] = card_el.cssselect("img.sig-stage-card-img") + self.assertIn( + "minchiate-fiorentine-1860-1890-trumps-00-il-matto.png", + img.get("src", ""), + ) + # Text scaffold absent in image mode (the server-side {% if %} branch + # skips the fan-card-corner + fan-card-face children entirely). + self.assertEqual( + len(card_el.cssselect(".fan-card-corner")), 0, + "Text scaffold must not render in image mode", + ) + + def test_my_sign_applet_keeps_text_render_for_non_image_deck(self): + """Earthman (has_card_images=False) keeps the existing fan-card-corner + text scaffold + lacks the --image modifier class.""" + from apps.epic.models import personal_sig_cards + target = personal_sig_cards(self.user)[0] + self.user.significator = target + self.user.save(update_fields=["significator"]) + import lxml.html + response = self.client.get("/billboard/") + parsed = lxml.html.fromstring(response.content) + [card_el] = parsed.cssselect(".my-sign-applet-card") + self.assertNotIn("my-sign-applet-card--image", card_el.get("class", "")) + self.assertEqual( + len(card_el.cssselect("img.sig-stage-card-img")), 0, + "Non-image deck must not render the ", + ) + self.assertGreater( + len(card_el.cssselect(".fan-card-corner")), 0, + "Non-image deck keeps the text scaffold", + ) diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index a10d20c..38092a1 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -130,6 +130,47 @@ class GameboardViewTest(TestCase): empty_positions = {e.get("data-position") for e in empties} self.assertEqual(empty_positions, {"cover", "crown"}) + def test_my_sea_applet_slot_renders_image_when_deck_has_card_images(self): + """Sprint A.7 — when the drawn card belongs to an image-equipped deck + (Minchiate today), the .my-sea-slot--filled carries `.my-sea-slot--image` + + renders an child instead of the text scaffold. + Shares the contour-stroke + depth shadow SCSS w. my_sign + my-sea + central sig + my-sign-applet via the comma-list selector in + `_card-deck.scss`.""" + from apps.epic.models import personal_sig_cards, TarotCard, DeckVariant + from apps.gameboard.models import MySeaDraw + sig_pile = personal_sig_cards(self.user) + self.user.significator = sig_pile[0] + self.user.save() + minchiate = DeckVariant.objects.get(slug="minchiate-fiorentine-1860-1890") + # Use a Minchiate card for the slot draw (doesn't need to be the sig + # — the sig is from Earthman; the drawn card is a separate concept). + card = TarotCard.objects.filter( + deck_variant=minchiate, slug="il-matto", + ).first() + MySeaDraw.objects.create( + user=self.user, + spread="situation-action-outcome", + hand=[{"position": "lay", "card_id": card.id, + "reversed": False, "polarity": "gravity"}], + significator_id=self.user.significator_id, + ) + response = self.client.get("/gameboard/") + parsed = lxml.html.fromstring(response.content) + [filled] = parsed.cssselect("#id_applet_my_sea .my-sea-slot--filled") + self.assertIn("my-sea-slot--image", filled.get("class", "")) + self.assertEqual(filled.get("data-arcana-key"), "MAJOR") + [img] = filled.cssselect("img.sig-stage-card-img") + self.assertIn( + "minchiate-fiorentine-1860-1890-trumps-00-il-matto.png", + img.get("src", ""), + ) + # Text scaffold absent in image mode. + self.assertEqual( + len(filled.cssselect(".fan-card-corner")), 0, + "Image-mode slot must not render the text scaffold", + ) + def test_my_sea_applet_renders_slots_even_when_user_significator_cleared(self): """Regression (user bug 2026-05-25 PM): deleting User.significator (via my-sign DEL) must NOT blank the My Sea applet on the gameboard diff --git a/src/static_src/scss/_billboard.scss b/src/static_src/scss/_billboard.scss index 8d9aa2d..a47b1f2 100644 --- a/src/static_src/scss/_billboard.scss +++ b/src/static_src/scss/_billboard.scss @@ -485,6 +485,19 @@ body.page-billposts { // `_card-deck.scss:1002-1019` for levity, :1039-1057 for gravity). background: rgba(var(--priUser), 1); border: 0.12rem solid rgba(var(--secUser), 0.6); + + // Sprint A.6 — image-mode override. `_card-deck.scss` imports before + // `_billboard.scss`, so the shared `.my-sign-applet-card--image` rule + // there gets out-cascaded by the base bg/border above (same specificity, + // later declaration wins). Re-state the transparency here AFTER the + // base. The contour stroke + depth shadow on the still come + // from the shared `_card-deck.scss` rule, which only loses on `bg` + + // `border` properties — not on the filter chain. + &.my-sign-applet-card--image { + background: transparent; + border: 0; + position: relative; // anchor for the absolute FLIP btn + } color: rgba(var(--terUser), 1); padding: 0.35rem; position: relative; @@ -568,6 +581,24 @@ body.page-billposts { } } + // Sprint A.6 — FLIP btn for non-polarized image-equipped decks in the + // applet. Nested INSIDE the .my-sign-applet-card.--image (which has + // position: relative) so absolute positioning anchors to the card bounds. + // Hidden during the rotateY animation via the [data-flipping] hook on + // the parent card — same pattern as the my_sign page (`_card-deck.scss:889`) + // and the tarot-fan view (`_card-deck.scss:459`). + .my-sign-applet-card .my-sign-applet-flip-btn { + position: absolute; + z-index: 10; + bottom: 0.6rem; + left: 0.6rem; + margin: 0; + } + .my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn { + opacity: 0; + pointer-events: none; + } + // Stat block — mirrors the stage card's footprint (same 5:8 aspect + // height) so the pair reads as a balanced 2-tile composition centred // in the applet aperture. Styling cribbed from `.sig-stat-block` in diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index ef618e6..3387ca8 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -663,7 +663,9 @@ html:has(.sig-backdrop) { // border around the bounding box). Color is arcana-driven: `--quiUser` (cream) // for minor + middle, `--terUser` (gold) for major per // [[project-image-based-deck-face-rendering]]'s Q2 lock. -.sig-stage-card.sig-stage-card--image { +.sig-stage-card.sig-stage-card--image, +.my-sign-applet-card.my-sign-applet-card--image, +.my-sea-slot.my-sea-slot--image { --img-stroke-color: rgba(var(--quiUser), 1); background: transparent; border: 0; diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss index cba5742..a39c095 100644 --- a/src/static_src/scss/_gameboard.scss +++ b/src/static_src/scss/_gameboard.scss @@ -423,6 +423,7 @@ body.page-gameboard { font-weight: 600; opacity: 1; color: rgba(var(--seciUser), 1); + text-shadow: 0 0 0.25rem rgba(var(--priUser), 1); text-align: center; pointer-events: none; white-space: nowrap; @@ -792,6 +793,21 @@ body.page-gameboard { } .my-sea-slot--filled.my-sea-slot--reversed { transform: rotate(180deg); } + // Sprint A.7 — image-mode slot override. `_card-deck.scss` imports + // after `_gameboard.scss`, but the polarity rules above (`.my-sea-slot-- + // filled.my-sea-slot--gravity` / `--levity`) are nested inside + // `#id_applet_my_sea` (specificity 1,2,0) and beat the top-level shared + // `.my-sea-slot.my-sea-slot--image` rule (0,2,0) on bg + border + color. + // Re-state the transparency here at matching nested specificity so the + // PNG card-back is unobstructed. Filter-chain / contour-stroke / depth + // shadow on `.sig-stage-card-img` still come from the shared rule (no + // collision — different selector target). + .my-sea-slot--filled.my-sea-slot--image { + background: transparent; + border: 0; + padding: 0; + } + // Empty slot — matches the my_sea.html picker's empty `.sea-card- // slot` style (`_card-deck.scss:1299-1303`): 0.15rem DASHED border in // --terUser at full opacity, --duoUser fill. Same width + dash @@ -816,13 +832,14 @@ body.page-gameboard { .my-sea-slot-label { position: relative; z-index: 2; - margin-top: -0.15rem; + margin-top: -0.05rem; padding: 0 0.2rem; font-size: 0.65rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(var(--secUser), 0.85); + text-shadow: 0 0 0.25rem rgba(var(--priUser), 1); text-align: center; white-space: nowrap; line-height: 1.1; diff --git a/src/templates/apps/billboard/_partials/_applet-my-sign.html b/src/templates/apps/billboard/_partials/_applet-my-sign.html index 5c25e57..fd36022 100644 --- a/src/templates/apps/billboard/_partials/_applet-my-sign.html +++ b/src/templates/apps/billboard/_partials/_applet-my-sign.html @@ -14,37 +14,59 @@ {# `_billboard.scss`. `significator_reversed` is the POLARITY #} {# axis (True ↔ levity), so the saved sig is always upright #} {# in its polarity — no `.stage-card--reversed` rotation. #} -
-
- {{ card.corner_rank }} - {% if card.suit_icon %}{% endif %} -
-
- {# `request.user.sig_face` is the rendering payload from #} - {# `TarotCard.applet_face()` — mirrors `populateCard` in #} - {# `stage-card.js:135-144`: #} - {# • Polarity-split (cards 48-49, trumps 19-21): #} - {# single-line title, qualifier blank. #} - {# • Major + qualifier: title carries a trailing #} - {# comma + qualifier renders BELOW. #} - {# • Non-Major (middle court, Schizo / Nomad w. no #} - {# qualifier): qualifier renders ABOVE the title. #} - {% with face=request.user.sig_face %} - {% if face.qualifier_first %} -

{{ face.qualifier }}

-

{{ face.title }}

- {% else %} -

{{ face.title }}

-

{{ face.qualifier }}

+
+ {% if card.deck_variant.has_card_images %} + {# Sprint A.6 — image-mode render mirrors my_sign.html's #} + {# .sig-stage-card--image treatment. Shares the SCSS rule #} + {# (comma-list selector) so the contour stroke + tray-card #} + {# silhouette black depth shadow + arcana stroke-color #} + {# come for free. #} + {{ card.name }} + {% if not card.deck_variant.is_polarized %} + {# Non-polarized image deck: FLIP btn shows the deck back #} + {# image (same behavior as my_sign.html main page). Both #} + {# the back-img + flip-btn nest INSIDE the card so the #} + {# absolute-positioned FLIP btn anchors to the card's #} + {# bounds (card is position: relative in --image mode). #} + + {% endif %} - {% endwith %} -

{{ card.get_arcana_display }}

-
-
- {{ card.corner_rank }} - {% if card.suit_icon %}{% endif %} -
+ {% else %} +
+ {{ card.corner_rank }} + {% if card.suit_icon %}{% endif %} +
+
+ {# `request.user.sig_face` is the rendering payload from #} + {# `TarotCard.applet_face()` — mirrors `populateCard` in #} + {# `stage-card.js:135-144`: #} + {# • Polarity-split (cards 48-49, trumps 19-21): #} + {# single-line title, qualifier blank. #} + {# • Major + qualifier: title carries a trailing #} + {# comma + qualifier renders BELOW. #} + {# • Non-Major (middle court, Schizo / Nomad w. no #} + {# qualifier): qualifier renders ABOVE the title. #} + {% with face=request.user.sig_face %} + {% if face.qualifier_first %} +

{{ face.qualifier }}

+

{{ face.title }}

+ {% else %} +

{{ face.title }}

+

{{ face.qualifier }}

+ {% endif %} + {% endwith %} +

{{ card.get_arcana_display }}

+
+
+ {{ card.corner_rank }} + {% if card.suit_icon %}{% endif %} +
+ {% endif %}
{# Stat block — same shape as my_sign.html's `.sig-stat-block` #} {# (Emanation face label + keyword list) but no SPIN/FYI btns #} @@ -59,6 +81,45 @@ {% endfor %}
+ {# Sprint A.6 — applet FLIP btn handler. Mirrors my_sign.html's #} + {# `_flipToBackAnimated()` shape (rotateY 0→90→0 over 500ms, class #} + {# toggle at halfway, `data-flipping` attr for SCSS to hide the #} + {# btn). Self-contained inline script — no shared module needed #} + {# since the applet is the only consumer outside the main page #} + {# (which has its own copy). Script wrapped inside the sig-present #} + {# branch AND inside `{% with card %}` scope so `card` references #} + {# resolve + the JS selector strings don't leak into the no-sig #} + {# DOM (which would trip substring-matching tests). #} + {% if card.deck_variant.has_card_images and not card.deck_variant.is_polarized %} + + {% endif %} {% endwith %} {% else %}

No sign chosen yet.

diff --git a/src/templates/apps/gameboard/_partials/_applet-my-sea.html b/src/templates/apps/gameboard/_partials/_applet-my-sea.html index e635894..5218909 100644 --- a/src/templates/apps/gameboard/_partials/_applet-my-sea.html +++ b/src/templates/apps/gameboard/_partials/_applet-my-sea.html @@ -27,38 +27,48 @@ {# the wrap so it sits BELOW the slot box (user-spec #} {# 2026-05-23: same position as the my_sea.html picker's #} {# `.sea-pos-label`). #} -
-
- {{ slot.card.corner_rank }} - {% if slot.card.suit_icon %}{% endif %} -
-
- {# `slot.face` is the rendering payload from `TarotCard. #} - {# applet_face()` — mirrors `populateCard` in #} - {# `stage-card.js`: #} - {# • Polarity-split (cards 19-21, 48-49): single-line #} - {# title, qualifier blank. #} - {# • Pattern B Major (2-5, 10-15, 22-35, 41): swapped #} - {# reversal name + polarity qualifier carried. #} - {# • Pattern B' Major (16-18): swapped reversal name, #} - {# no qualifier on reversal. #} - {# • Non-Major: qualifier ABOVE the title. #} - {# Empty `.fan-card-qualifier` is hidden by `:empty` CSS. #} - {% if slot.face.qualifier_first %} -

{{ slot.face.qualifier }}

-

{{ slot.face.title }}

- {% else %} -

{{ slot.face.title }}

-

{{ slot.face.qualifier }}

- {% endif %} -

{{ slot.card.get_arcana_display }}

-
-
- {{ slot.card.corner_rank }} - {% if slot.card.suit_icon %}{% endif %} -
+ data-card-id="{{ slot.card.id }}" + data-arcana-key="{{ slot.card.arcana }}"> + {% if slot.card.deck_variant.has_card_images %} + {# Sprint A.7 — image-mode slot render. Shares the #} + {# `.sig-stage-card-img` SCSS rule via the #} + {# `.my-sea-slot--image` comma-list addition in #} + {# `_card-deck.scss`. Contour stroke + depth shadow #} + {# scale w. the slot's smaller dimensions. #} + {{ slot.card.name }} + {% else %} +
+ {{ slot.card.corner_rank }} + {% if slot.card.suit_icon %}{% endif %} +
+
+ {# `slot.face` is the rendering payload from `TarotCard. #} + {# applet_face()` — mirrors `populateCard` in #} + {# `stage-card.js`: #} + {# • Polarity-split (cards 19-21, 48-49): single-line #} + {# title, qualifier blank. #} + {# • Pattern B Major (2-5, 10-15, 22-35, 41): swapped #} + {# reversal name + polarity qualifier carried. #} + {# • Pattern B' Major (16-18): swapped reversal name, #} + {# no qualifier on reversal. #} + {# • Non-Major: qualifier ABOVE the title. #} + {# Empty `.fan-card-qualifier` is hidden by `:empty` CSS. #} + {% if slot.face.qualifier_first %} +

{{ slot.face.qualifier }}

+

{{ slot.face.title }}

+ {% else %} +

{{ slot.face.title }}

+

{{ slot.face.qualifier }}

+ {% endif %} +

{{ slot.card.get_arcana_display }}

+
+
+ {{ slot.card.corner_rank }} + {% if slot.card.suit_icon %}{% endif %} +
+ {% endif %}
{{ slot.label }}