diff --git a/src/apps/epic/migrations/0007_populate_middle_arcana_reversals.py b/src/apps/epic/migrations/0007_populate_middle_arcana_reversals.py
new file mode 100644
index 0000000..26a3d26
--- /dev/null
+++ b/src/apps/epic/migrations/0007_populate_middle_arcana_reversals.py
@@ -0,0 +1,73 @@
+"""Populate TarotCard.reversal for Earthman Middle Arcana court cards.
+
+Each suit has a fixed reversal qualifier that replaces the polarity qualifier
+(Elevated/Graven) when the card is spun to its reversed face:
+ Brands → Seething Grails → Gloomy Blades → Nervous Crowns → Vacant
+
+Also clears the incorrectly inherited reversal on The Schizo (card 1), which
+mistakenly carried 'Territoriality' from The Occultist (card 2).
+"""
+from django.db import migrations
+
+SUIT_REVERSAL_QUALIFIER = {
+ "BRANDS": "Seething",
+ "GRAILS": "Gloomy",
+ "BLADES": "Nervous",
+ "CROWNS": "Vacant",
+}
+
+RANK_NAMES = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
+
+
+def populate_reversals(apps, schema_editor):
+ TarotCard = apps.get_model("epic", "TarotCard")
+ DeckVariant = apps.get_model("epic", "DeckVariant")
+
+ try:
+ earthman = DeckVariant.objects.get(slug="earthman")
+ except DeckVariant.DoesNotExist:
+ return
+
+ # Middle Arcana court cards
+ for suit, qualifier in SUIT_REVERSAL_QUALIFIER.items():
+ TarotCard.objects.filter(
+ deck_variant=earthman,
+ arcana="MIDDLE",
+ suit=suit,
+ number__in=list(RANK_NAMES.keys()),
+ ).update(reversal=qualifier)
+
+ # Clear The Schizo's incorrectly inherited reversal (belongs to The Occultist)
+ TarotCard.objects.filter(
+ deck_variant=earthman,
+ arcana="MAJOR",
+ number=1,
+ ).update(reversal="")
+
+
+def clear_reversals(apps, schema_editor):
+ TarotCard = apps.get_model("epic", "TarotCard")
+ DeckVariant = apps.get_model("epic", "DeckVariant")
+ try:
+ earthman = DeckVariant.objects.get(slug="earthman")
+ except DeckVariant.DoesNotExist:
+ return
+ TarotCard.objects.filter(
+ deck_variant=earthman, arcana="MIDDLE",
+ suit__in=list(SUIT_REVERSAL_QUALIFIER.keys()),
+ number__in=list(RANK_NAMES.keys()),
+ ).update(reversal="")
+ TarotCard.objects.filter(deck_variant=earthman, arcana="MAJOR", number=1).update(
+ reversal="Territoriality"
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("epic", "0006_add_deck_variant_to_tableseat"),
+ ]
+
+ operations = [
+ migrations.RunPython(populate_reversals, reverse_code=clear_reversals),
+ ]
diff --git a/src/apps/epic/static/apps/epic/sig-select.js b/src/apps/epic/static/apps/epic/sig-select.js
index 232b05c..ef277c3 100644
--- a/src/apps/epic/static/apps/epic/sig-select.js
+++ b/src/apps/epic/static/apps/epic/sig-select.js
@@ -132,9 +132,22 @@ var SigSelect = (function () {
stageCard.querySelector('.sig-qualifier-above').textContent = isMajor ? '' : qualifier;
stageCard.querySelector('.sig-qualifier-below').textContent = isMajor ? qualifier : '';
- // Reversed face — same qualifier, polarity-resolved reversal title
- stageCard.querySelector('.fan-card-reversal-qualifier').textContent = qualifier;
- stageCard.querySelector('.fan-card-reversal-name').textContent = cardEl.dataset.reversal || '';
+ // Reversed face.
+ // - Major arcana: polarity qualifier + reversal concept name
+ // - Non-major w. reversal: suit qualifier word replaces polarity qualifier;
+ // card name (title) stays the same — two separate lines
+ // - Non-major w/o reversal: fall back to mirroring the polarity qualifier
+ var reversal = cardEl.dataset.reversal || '';
+ if (isMajor) {
+ stageCard.querySelector('.fan-card-reversal-qualifier').textContent = qualifier;
+ stageCard.querySelector('.fan-card-reversal-name').textContent = reversal;
+ } else if (reversal) {
+ stageCard.querySelector('.fan-card-reversal-qualifier').textContent = reversal;
+ stageCard.querySelector('.fan-card-reversal-name').textContent = title;
+ } else {
+ stageCard.querySelector('.fan-card-reversal-qualifier').textContent = qualifier;
+ stageCard.querySelector('.fan-card-reversal-name').textContent = '';
+ }
// Populate stat block keyword faces and reset to upright
statBlock.classList.remove('is-reversed');
diff --git a/src/static/tests/SigSelectSpec.js b/src/static/tests/SigSelectSpec.js
index 15f1aef..cdfd2ec 100644
--- a/src/static/tests/SigSelectSpec.js
+++ b/src/static/tests/SigSelectSpec.js
@@ -22,8 +22,8 @@ describe("SigSelect", () => {
-
+
@@ -64,7 +64,7 @@ describe("SigSelect", () => {
data-articulations="[]"
data-levity-qualifier="Elevated"
data-gravity-qualifier="Graven"
- data-reversal="Territoriality">
+ data-reversal="">
K
@@ -483,12 +483,14 @@ describe("SigSelect", () => {
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
});
- it("updateStage() populates fan-card-reversal-name from data-reversal", () => {
+ it("non-major with data-reversal: reversal-qualifier = suit word, reversal-name = card name", () => {
makeFixture();
- card.dataset.reversal = "Territoriality";
+ card.dataset.reversal = "Nervous";
hover();
+ // "Nervous" goes into qualifier slot (own line); upright name reused in name slot
+ expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
- .toBe("Territoriality");
+ .toBe(card.dataset.nameTitle);
});
it("updateStage() populates fan-card-reversal-qualifier with levity qualifier", () => {
@@ -505,6 +507,24 @@ describe("SigSelect", () => {
.toBe("Graven");
});
+ it("non-major with data-reversal: suit qualifier on own line, upright name repeated below", () => {
+ makeFixture({ polarity: "levity", userRole: "PC" });
+ card.dataset.reversal = "Vacant";
+ hover();
+ expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
+ expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
+ .toBe(card.dataset.nameTitle);
+ });
+
+ it("major arcana with data-reversal: polarity qualifier still shown alongside reversal name", () => {
+ makeFixture({ polarity: "levity", userRole: "PC" });
+ card.dataset.arcana = "Major Arcana";
+ card.dataset.reversal = "Territoriality";
+ hover();
+ expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Elevated");
+ expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Territoriality");
+ });
+
it("hovering a card without data-reversal clears the reversal name", () => {
makeFixture();
card.dataset.reversal = "Territoriality";
diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss
index 09fcfb3..2d95063 100644
--- a/src/static_src/scss/_card-deck.scss
+++ b/src/static_src/scss/_card-deck.scss
@@ -224,22 +224,23 @@ html:has(.sig-backdrop) {
padding: 0.25rem 0.15rem;
gap: 0.2rem;
+ .fan-card-face-upright { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
+ .fan-card-face-reversal { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding-top: 0.1rem; }
.fan-card-name-group { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.6; }
+ // Upright qualifier + name share sizing/weight/color with their reversed counterparts
.sig-qualifier-above,
- .sig-qualifier-below { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
- .fan-card-name { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
+ .sig-qualifier-below,
+ .fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
+ .fan-card-name,
+ .fan-card-reversal-name { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
.fan-card-arcana { font-size: calc(var(--sig-card-w, 120px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
.fan-card-correspondence{ display: none; } // Minchiate equivalence shown in game-kit only
- // Reversed face — counter-rotated so they read forward when card is spun
+ // Reversed face elements — pre-rotated so they read forward after card spins
.fan-card-reversal-qualifier,
.fan-card-reversal-name {
transform: rotate(180deg);
opacity: 0.25;
- transition: opacity 0.2s;
- font-weight: 600;
}
- .fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 120px) * 0.093); }
- .fan-card-reversal-name { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.2; }
}
&.stage-card--reversed {
diff --git a/src/static_src/tests/SigSelectSpec.js b/src/static_src/tests/SigSelectSpec.js
index 15f1aef..cdfd2ec 100644
--- a/src/static_src/tests/SigSelectSpec.js
+++ b/src/static_src/tests/SigSelectSpec.js
@@ -22,8 +22,8 @@ describe("SigSelect", () => {
-
+
@@ -64,7 +64,7 @@ describe("SigSelect", () => {
data-articulations="[]"
data-levity-qualifier="Elevated"
data-gravity-qualifier="Graven"
- data-reversal="Territoriality">
+ data-reversal="">
K
@@ -483,12 +483,14 @@ describe("SigSelect", () => {
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
});
- it("updateStage() populates fan-card-reversal-name from data-reversal", () => {
+ it("non-major with data-reversal: reversal-qualifier = suit word, reversal-name = card name", () => {
makeFixture();
- card.dataset.reversal = "Territoriality";
+ card.dataset.reversal = "Nervous";
hover();
+ // "Nervous" goes into qualifier slot (own line); upright name reused in name slot
+ expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
- .toBe("Territoriality");
+ .toBe(card.dataset.nameTitle);
});
it("updateStage() populates fan-card-reversal-qualifier with levity qualifier", () => {
@@ -505,6 +507,24 @@ describe("SigSelect", () => {
.toBe("Graven");
});
+ it("non-major with data-reversal: suit qualifier on own line, upright name repeated below", () => {
+ makeFixture({ polarity: "levity", userRole: "PC" });
+ card.dataset.reversal = "Vacant";
+ hover();
+ expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
+ expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
+ .toBe(card.dataset.nameTitle);
+ });
+
+ it("major arcana with data-reversal: polarity qualifier still shown alongside reversal name", () => {
+ makeFixture({ polarity: "levity", userRole: "PC" });
+ card.dataset.arcana = "Major Arcana";
+ card.dataset.reversal = "Territoriality";
+ hover();
+ expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Elevated");
+ expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Territoriality");
+ });
+
it("hovering a card without data-reversal clears the reversal name", () => {
makeFixture();
card.dataset.reversal = "Territoriality";
diff --git a/src/templates/apps/gameboard/_partials/_sig_select_overlay.html b/src/templates/apps/gameboard/_partials/_sig_select_overlay.html
index a632548..f100a2f 100644
--- a/src/templates/apps/gameboard/_partials/_sig_select_overlay.html
+++ b/src/templates/apps/gameboard/_partials/_sig_select_overlay.html
@@ -23,14 +23,18 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
-
-
-
-
+
{# not shown in sig-select — game-kit only #}
-
-
+