Sig Select qualifier on the stat-block — green the 3 theme FTs
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

The qualifier (Elevated/Enlightened/Graven) now renders on the frozen stat-block
stat-face, not just the hover-only text card-face (which image-mode decks hide).
populateStatExtras takes opts.polarity and fills new .stat-face-qualifier--above
/--below slots, mirroring the card-face placement: non-major above the title,
major below it (title gets a trailing comma). Qualifier shares the title's style
per request ("same style as 'Jack of Crowns' below it").

The 3 theme FTs were asserting the qualifier on hover, but the stat-block is
display:none until a card is OK'd (.sig-stage--frozen) — only the card preview
shows on hover. Repointed them to select→OK→freeze, then read the stat-block.
Threaded polarity through the sig + my_sign callers; added 4 Jasmine specs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-03 14:28:45 -04:00
parent 544ce978d5
commit 9a00f96fe5
8 changed files with 208 additions and 45 deletions

View File

@@ -102,7 +102,7 @@ var SigSelect = (function () {
// (unified w. my_sign 2026-06-03) — previously the sig stat-block was a // (unified w. my_sign 2026-06-03) — previously the sig stat-block was a
// reduced label-only copy, so this call was absent and those fields // reduced label-only copy, so this call was absent and those fields
// stayed blank. // stayed blank.
StageCard.populateStatExtras(statBlock, card); StageCard.populateStatExtras(statBlock, card, { polarity: userPolarity });
stageCard.style.display = ''; stageCard.style.display = '';
stage.classList.add('sig-stage--active'); stage.classList.add('sig-stage--active');

View File

@@ -287,8 +287,38 @@ var StageCard = (function () {
var rawTitle = card.name_title || card.name || ''; var rawTitle = card.name_title || card.name || '';
var title = rawTitle.split(',')[0].trim(); var title = rawTitle.split(',')[0].trim();
var arcana = _arcanaDisplay(card); var arcana = _arcanaDisplay(card);
var isMajor = _isMajor(card);
// Polarity qualifier (Elevated/Enlightened/Graven) on the stat-block —
// the always-visible home now that image-mode decks hide the text card
// face. Mirrors populateCard's above/below placement: non-major above
// the title, major below it (title carries a trailing comma so the
// qualifier reads as a subtitle). Only filled when a polarity is
// supplied (sig / my_sign / sea); the kit fan omits it. Cards 48-49 +
// trumps 19-21 carry a polarity-split emanation override whose title
// already incorporates the qualifier inline → no separate slot.
var polarity = opts.polarity || '';
var qualifier = polarity === 'levity' ? (card.levity_qualifier || '')
: polarity === 'gravity' ? (card.gravity_qualifier || '')
: '';
var emanationOverride = polarity === 'levity' ? (card.levity_emanation || '')
: polarity === 'gravity' ? (card.gravity_emanation || '')
: '';
if (emanationOverride) {
title = emanationOverride;
qualifier = '';
}
var qAbove = qualifier && !isMajor ? qualifier : '';
var qBelow = qualifier && isMajor ? qualifier : '';
statBlock.querySelectorAll('.stat-face-title').forEach(function (el) { statBlock.querySelectorAll('.stat-face-title').forEach(function (el) {
el.textContent = title; el.textContent = qBelow ? title + ',' : title;
});
statBlock.querySelectorAll('.stat-face-qualifier--above').forEach(function (el) {
el.textContent = qAbove;
});
statBlock.querySelectorAll('.stat-face-qualifier--below').forEach(function (el) {
el.textContent = qBelow;
}); });
statBlock.querySelectorAll('.stat-face-arcana').forEach(function (el) { statBlock.querySelectorAll('.stat-face-arcana').forEach(function (el) {
el.textContent = arcana; el.textContent = arcana;

View File

@@ -1,5 +1,4 @@
import os import os
from unittest import skip
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.test import tag from django.test import tag
@@ -256,71 +255,80 @@ class SigSelectThemeTest(FunctionalTest):
ActionChains(self.browser).move_to_element(card).perform() ActionChains(self.browser).move_to_element(card).perform()
return card return card
def _select_card_and_freeze(self, css):
"""Click a card → OK → wait for the stage to freeze. The stat-block is
`display:none` until `.sig-stage--frozen` (only the card preview shows
on hover); OK'ing a card reveals it. The qualifier lives on the frozen
stat-block now (the hover-only text card-face is going away as decks
gain images), so assertions need the frozen state."""
card = self.browser.find_element(By.CSS_SELECTOR, css)
card.click()
ok = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-focused .sig-ok-btn")
)
ok.click()
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-stage--frozen"))
# ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── # # ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── #
@skip( # Qualifier asserts target the frozen stat-block (.stat-face--upright), not
"Qualifier (Elevated/Enlightened/Graven) now belongs on the always-" # the text card-face: image-mode decks hide .fan-card-face, and the qualifier
"visible stat-block, not the card face: the Earthman deck renders in " # is moving to the always-present stat-block. It mirrors the card-face
"image-mode (has_card_images defaults True) so the text card-face is " # above/below placement — non-major puts the qualifier ABOVE the title, major
"display:none. Unskip + repoint to the stat-block next." # BELOW it. See stage-card.js populateStatExtras + _stat_face.html.
) UPRIGHT_QUAL_ABOVE = ".sig-stat-block .stat-face--upright .stat-face-qualifier--above"
UPRIGHT_QUAL_BELOW = ".sig-stat-block .stat-face--upright .stat-face-qualifier--below"
def test_levity_non_major_card_shows_elevated_above(self): def test_levity_non_major_card_shows_elevated_above(self):
"""Hovering a non-major card in the levity overlay shows 'Elevated' in """Selecting (OK) a non-major card shows 'Elevated' in the frozen
qualifier-above and nothing in qualifier-below.""" stat-block qualifier-above and nothing in qualifier-below."""
room = self._setup_sig_room() room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io") # PC = levity self.create_pre_authenticated_session("founder@test.io") # PC = levity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Middle Arcana"]') self._select_card_and_freeze('.sig-card[data-arcana="Middle Arcana"]')
above = self.wait_for( self.wait_for(lambda: self.assertEqual(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_ABOVE).text,
) "Elevated",
self.assertEqual(above.text, "Elevated") ))
below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below") below = self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_BELOW)
self.assertEqual(below.text, "") self.assertEqual(below.text, "")
@skip(
"Qualifier now belongs on the always-visible stat-block, not the card "
"face (image-mode hides .fan-card-face). Unskip + repoint next."
)
def test_levity_major_card_shows_enlightened_below(self): def test_levity_major_card_shows_enlightened_below(self):
"""Hovering a major arcana card in the levity overlay shows 'Enlightened' in """Selecting (OK) a major arcana card shows 'Enlightened' in the frozen
qualifier-below and nothing in qualifier-above.""" stat-block qualifier-below and nothing in qualifier-above."""
room = self._setup_sig_room() room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io") # PC = levity self.create_pre_authenticated_session("founder@test.io") # PC = levity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Major Arcana"]') self._select_card_and_freeze('.sig-card[data-arcana="Major Arcana"]')
below = self.wait_for( self.wait_for(lambda: self.assertEqual(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below") self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_BELOW).text,
) "Enlightened",
self.assertEqual(below.text, "Enlightened") ))
above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") above = self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_ABOVE)
self.assertEqual(above.text, "") self.assertEqual(above.text, "")
# ── ST2: Gravity (Graven) qualifier ───────────────────────────────────── # # ── ST2: Gravity (Graven) qualifier ───────────────────────────────────── #
@skip(
"Qualifier now belongs on the always-visible stat-block, not the card "
"face (image-mode hides .fan-card-face). Unskip + repoint next."
)
def test_gravity_non_major_card_shows_graven_above(self): def test_gravity_non_major_card_shows_graven_above(self):
"""EC (bud) sees the gravity overlay; hovering a non-major card shows 'Graven'.""" """EC (bud) sees the gravity overlay; selecting (OK) a non-major card
shows 'Graven' in the frozen stat-block qualifier-above."""
room = self._setup_sig_room() room = self._setup_sig_room()
self.create_pre_authenticated_session("bud@test.io") # EC = gravity self.create_pre_authenticated_session("bud@test.io") # EC = gravity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay")) self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Middle Arcana"]') self._select_card_and_freeze('.sig-card[data-arcana="Middle Arcana"]')
above = self.wait_for( self.wait_for(lambda: self.assertEqual(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_ABOVE).text,
) "Graven",
self.assertEqual(above.text, "Graven") ))
# ── ST3: Correspondence not shown ─────────────────────────────────────── # # ── ST3: Correspondence not shown ─────────────────────────────────────── #

View File

@@ -34,10 +34,18 @@ describe("SigSelect", () => {
<button class="btn btn-info fyi-btn" type="button">FYI</button> <button class="btn btn-info fyi-btn" type="button">FYI</button>
<div class="stat-face stat-face--upright"> <div class="stat-face stat-face--upright">
<p class="stat-face-label">Emanation</p> <p class="stat-face-label">Emanation</p>
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
<p class="stat-face-title"></p>
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
<p class="stat-face-arcana"></p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul> <ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div> </div>
<div class="stat-face stat-face--reversed"> <div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversal</p> <p class="stat-face-label">Reversal</p>
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
<p class="stat-face-title"></p>
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
<p class="stat-face-arcana"></p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul> <ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div> </div>
<button class="btn btn-nav-left fyi-prev" type="button">&#9664;</button> <button class="btn btn-nav-left fyi-prev" type="button">&#9664;</button>
@@ -742,6 +750,51 @@ describe("SigSelect", () => {
}); });
}); });
// ── Polarity theming — stat-block qualifier ────────────────────────────── //
//
// The qualifier's always-visible home: image-mode decks hide the text card
// face, so the stat-block (.stat-face--upright) carries Elevated/Enlightened/
// Graven now. Mirrors the card-face above/below placement (non-major above
// the title, major below it w. a trailing-comma title).
describe("polarity theming — stat-block qualifier", () => {
const UPRIGHT = ".stat-face--upright";
it("levity non-major puts qualifier ABOVE the stat-block title, below empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' }); // fixture card = Minor Arcana
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("Elevated");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-title").textContent).toBe("King of Pentacles");
});
it("levity major puts qualifier BELOW the stat-block title, above empty, title gets comma", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("Elevated");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-title").textContent).toBe("King of Pentacles,");
});
it("gravity non-major puts 'Graven' ABOVE the stat-block title", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("Graven");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("");
});
it("re-hovering a different arcana re-places the stat-block qualifier slot", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); // non-major → above
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); // major → below
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("Elevated");
});
});
// ── WAIT NVM glow pulse ────────────────────────────────────────────────────── // // ── WAIT NVM glow pulse ────────────────────────────────────────────────────── //
describe("WAIT NVM glow pulse", () => { describe("WAIT NVM glow pulse", () => {

View File

@@ -176,7 +176,12 @@
// the rest are --quiUser. Let's change the latter to match the // the rest are --quiUser. Let's change the latter to match the
// former" — applet's `.stat-face-title` was already --quaUser; // former" — applet's `.stat-face-title` was already --quaUser;
// shared mixin now matches so all 4 stat-block surfaces unify). // shared mixin now matches so all 4 stat-block surfaces unify).
.stat-face-title { // Title + polarity qualifier share one style (user-spec 2026-06-03 —
// "'Elevated' needs to be the same style as 'Jack of Crowns' below it").
// Qualifier is the subtitle hugging the title (above for non-major, below
// for major); both pick up the arcana-keyed color override below.
.stat-face-title,
.stat-face-qualifier {
font-size: calc(var(--sig-card-w, 120px) * 0.105); font-size: calc(var(--sig-card-w, 120px) * 0.105);
font-weight: 700; font-weight: 700;
line-height: 1.15; line-height: 1.15;
@@ -184,7 +189,8 @@
text-wrap: balance; text-wrap: balance;
color: rgba(var(--quaUser), 1); color: rgba(var(--quaUser), 1);
} }
[data-arcana-key="MAJOR"] .stat-face-title { [data-arcana-key="MAJOR"] .stat-face-title,
[data-arcana-key="MAJOR"] .stat-face-qualifier {
color: rgba(var(--terUser), 1); color: rgba(var(--terUser), 1);
} }
@@ -195,10 +201,15 @@
opacity: 0.6; opacity: 0.6;
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07); margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
} }
// `:empty` rule hides title + arcana when stage-card.js hasn't populated // Qualifier hugs the title — tighten the gap on the side that touches it
// them yet (rest state) — prevents zero-height paragraphs from inflating // (below the above-slot, above the below-slot) so the two read as one unit.
// the stat block vertical layout. .stat-face-qualifier--above { margin-bottom: calc(var(--sig-card-w, 120px) * 0.01); }
.stat-face-qualifier--below { margin-top: calc(var(--sig-card-w, 120px) * -0.01); }
// `:empty` rule hides title + arcana + qualifier slots when stage-card.js
// hasn't populated them yet (rest state) — prevents zero-height paragraphs
// from inflating the stat block vertical layout.
.stat-face-title:empty, .stat-face-title:empty,
.stat-face-qualifier:empty,
.stat-face-arcana:empty { display: none; } .stat-face-arcana:empty { display: none; }
.stat-keywords { .stat-keywords {

View File

@@ -34,10 +34,18 @@ describe("SigSelect", () => {
<button class="btn btn-info fyi-btn" type="button">FYI</button> <button class="btn btn-info fyi-btn" type="button">FYI</button>
<div class="stat-face stat-face--upright"> <div class="stat-face stat-face--upright">
<p class="stat-face-label">Emanation</p> <p class="stat-face-label">Emanation</p>
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
<p class="stat-face-title"></p>
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
<p class="stat-face-arcana"></p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul> <ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div> </div>
<div class="stat-face stat-face--reversed"> <div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversal</p> <p class="stat-face-label">Reversal</p>
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
<p class="stat-face-title"></p>
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
<p class="stat-face-arcana"></p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul> <ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div> </div>
<button class="btn btn-nav-left fyi-prev" type="button">&#9664;</button> <button class="btn btn-nav-left fyi-prev" type="button">&#9664;</button>
@@ -742,6 +750,51 @@ describe("SigSelect", () => {
}); });
}); });
// ── Polarity theming — stat-block qualifier ────────────────────────────── //
//
// The qualifier's always-visible home: image-mode decks hide the text card
// face, so the stat-block (.stat-face--upright) carries Elevated/Enlightened/
// Graven now. Mirrors the card-face above/below placement (non-major above
// the title, major below it w. a trailing-comma title).
describe("polarity theming — stat-block qualifier", () => {
const UPRIGHT = ".stat-face--upright";
it("levity non-major puts qualifier ABOVE the stat-block title, below empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' }); // fixture card = Minor Arcana
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("Elevated");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-title").textContent).toBe("King of Pentacles");
});
it("levity major puts qualifier BELOW the stat-block title, above empty, title gets comma", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("Elevated");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-title").textContent).toBe("King of Pentacles,");
});
it("gravity non-major puts 'Graven' ABOVE the stat-block title", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("Graven");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("");
});
it("re-hovering a different arcana re-places the stat-block qualifier slot", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); // non-major → above
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); // major → below
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("");
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("Elevated");
});
});
// ── WAIT NVM glow pulse ────────────────────────────────────────────────────── // // ── WAIT NVM glow pulse ────────────────────────────────────────────────────── //
describe("WAIT NVM glow pulse", () => { describe("WAIT NVM glow pulse", () => {

View File

@@ -238,7 +238,7 @@ this billboard surface re-brands to "Sign".
StageCard.populateCard(stageCard, _currentCard, _polarity()); StageCard.populateCard(stageCard, _currentCard, _polarity());
StageCard.populateKeywords(statBlock, StageCard.populateKeywords(statBlock,
_currentCard.keywords_upright, _currentCard.keywords_reversed); _currentCard.keywords_upright, _currentCard.keywords_reversed);
StageCard.populateStatExtras(statBlock, _currentCard); StageCard.populateStatExtras(statBlock, _currentCard, { polarity: _polarity() });
_fyiData = StageCard.buildInfoData(_currentCard); _fyiData = StageCard.buildInfoData(_currentCard);
_fyiIdx = 0; _fyiIdx = 0;
if (fyiPanel) StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx); if (fyiPanel) StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);

View File

@@ -32,7 +32,15 @@ Args:
<p class="stat-face-label">{{ label_text }}</p> <p class="stat-face-label">{{ label_text }}</p>
</div> </div>
</div> </div>
{# Polarity qualifier (Elevated/Enlightened/Graven) — mirrors the card-face #}
{# above/below placement: non-major arcana puts it ABOVE the title, major #}
{# puts it BELOW (the title then carries a trailing comma). JS-only #}
{# (stage-card.js populateStatExtras w. opts.polarity); empty server-side #}
{# since the qualifier is polarity-dependent. The card-face copy is going #}
{# away as decks gain images, so the stat-block becomes its sole home. #}
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
<p class="stat-face-title">{% if card %}{{ card.name }}{% endif %}</p> <p class="stat-face-title">{% if card %}{{ card.name }}{% endif %}</p>
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
<p class="stat-face-arcana">{% if card %}{{ card.get_arcana_display }}{% endif %}</p> <p class="stat-face-arcana">{% if card %}{{ card.get_arcana_display }}{% endif %}</p>
<ul class="stat-keywords"{% if keywords_ul_id %} id="{{ keywords_ul_id }}"{% endif %}>{% if card %}{% for kw in card.keywords_upright %}<li>{{ kw }}</li>{% endfor %}{% endif %}</ul> <ul class="stat-keywords"{% if keywords_ul_id %} id="{{ keywords_ul_id }}"{% endif %}>{% if card %}{% for kw in card.keywords_upright %}<li>{{ kw }}</li>{% endfor %}{% endif %}</ul>
</div> </div>