diff --git a/src/apps/epic/migrations/0015_card49_polarity_reversal_titles.py b/src/apps/epic/migrations/0015_card49_polarity_reversal_titles.py new file mode 100644 index 0000000..0aff797 --- /dev/null +++ b/src/apps/epic/migrations/0015_card49_polarity_reversal_titles.py @@ -0,0 +1,60 @@ +"""Populate card 49's polarity-split reversal titles. + +The Earthman deck's last two cards (48–49) carry distinct titles per polarity +(stored in `levity_emanation` / `gravity_emanation` / `levity_reversal` / +`gravity_reversal`) rather than a shared title + qualifier. + +Card 48 had its full set seeded in migration 0004: + levity: Father Sky → reversal: The Storm + gravity: Mother Sea → reversal: The Flood + +Card 49 had only emanations seeded; this migration fills the reversals: + levity: The Effulgent Mould of Man → reversal: The Vibrational Mould of Man + gravity: The Devouring Eagle → reversal: The All-Bestowing Eagle + +The "qualifier" (Effulgent / Vibrational / Devouring / All-Bestowing) is baked +into the title between "The" and the title-proper rather than rendered as a +separate qualifier slot — the per-polarity title strings are stored verbatim. +""" +from django.db import migrations + + +def populate_card49_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="MAJOR", number=49, + ).update( + levity_reversal="The Vibrational Mould of Man", + gravity_reversal="The All-Bestowing Eagle", + ) + + +def clear_card49_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="MAJOR", number=49, + ).update( + levity_reversal="", + gravity_reversal="", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("epic", "0014_rename_reversal_to_reversal_qualifier"), + ] + + operations = [ + migrations.RunPython(populate_card49_reversals, reverse_code=clear_card49_reversals), + ] diff --git a/src/apps/epic/models.py b/src/apps/epic/models.py index d87be2d..2892506 100644 --- a/src/apps/epic/models.py +++ b/src/apps/epic/models.py @@ -291,21 +291,22 @@ class TarotCard(models.Model): def emanation_for(self, polarity): """Return the upright title for a given polarity ('levity' or 'gravity'). - Falls back to name for cards without a polarity split.""" + Falls back to name_title (group prefix stripped) for cards without a + polarity split.""" if polarity == 'levity' and self.levity_emanation: return self.levity_emanation if polarity == 'gravity' and self.gravity_emanation: return self.gravity_emanation - return self.name + return self.name_title def reversal_for(self, polarity): """Return the reversed title for a given polarity. - Falls back to reversal (blank = same as emanation_for).""" + Falls back to reversal_qualifier (blank = same as emanation_for).""" if polarity == 'levity' and self.levity_reversal: return self.levity_reversal if polarity == 'gravity' and self.gravity_reversal: return self.gravity_reversal - return self.reversal or self.emanation_for(polarity) + return self.reversal_qualifier or self.emanation_for(polarity) @property def name_group(self): diff --git a/src/apps/epic/static/apps/epic/stage-card.js b/src/apps/epic/static/apps/epic/stage-card.js index 0d3a143..37c5f38 100644 --- a/src/apps/epic/static/apps/epic/stage-card.js +++ b/src/apps/epic/static/apps/epic/stage-card.js @@ -31,6 +31,13 @@ var StageCard = (function () { levity_qualifier: el.dataset.levityQualifier || '', gravity_qualifier: el.dataset.gravityQualifier || '', reversal_qualifier: el.dataset.reversalQualifier || '', + // Polarity-split title overrides — non-blank for cards 48-49 only, + // where each polarity (and within each polarity, each axis state) + // has a fully distinct title rather than a shared name + qualifier. + levity_emanation: el.dataset.levityEmanation || '', + gravity_emanation: el.dataset.gravityEmanation || '', + levity_reversal: el.dataset.levityReversal || '', + gravity_reversal: el.dataset.gravityReversal || '', }; } @@ -47,11 +54,17 @@ var StageCard = (function () { // falls back to the current polarity's qualifier when blank (6F behavior). function populateCard(stageCard, card, polarity) { if (!stageCard) return; - var isLevity = polarity === 'levity'; + var isLevity = polarity === 'levity'; var qualifier = isLevity ? (card.levity_qualifier || '') : (card.gravity_qualifier || ''); - var isMajor = _isMajor(card); - var title = card.name_title || ''; + var isMajor = _isMajor(card); + var title = card.name_title || ''; var reversalQualifier = card.reversal_qualifier || ''; + // Polarity-split overrides (cards 48-49). When set, the full title + // string already incorporates whatever qualifier the card carries + // ("The Effulgent Mould of Man" etc.) — render as a single line and + // leave the upright qualifier slots empty. + var emanationOverride = isLevity ? (card.levity_emanation || '') : (card.gravity_emanation || ''); + var reversalOverride = isLevity ? (card.levity_reversal || '') : (card.gravity_reversal || ''); stageCard.querySelectorAll('.fan-corner-rank').forEach(function (el) { el.textContent = card.corner_rank || ''; @@ -66,27 +79,38 @@ var StageCard = (function () { }); var nameGroupEl = stageCard.querySelector('.fan-card-name-group'); - if (nameGroupEl) nameGroupEl.textContent = card.name_group || ''; + if (nameGroupEl) nameGroupEl.textContent = emanationOverride ? '' : (card.name_group || ''); var arcanaEl = stageCard.querySelector('.fan-card-arcana'); if (arcanaEl) arcanaEl.textContent = isMajor ? 'Major Arcana' : 'Middle Arcana'; var nameEl = stageCard.querySelector('.fan-card-name'); - if (nameEl) nameEl.textContent = isMajor ? title + ',' : title; - var qAbove = stageCard.querySelector('.sig-qualifier-above'); - if (qAbove) qAbove.textContent = isMajor ? '' : qualifier; var qBelow = stageCard.querySelector('.sig-qualifier-below'); - if (qBelow) qBelow.textContent = isMajor ? qualifier : ''; - // Reversal face — three cases: - // Major: title (with comma) in qualifier slot, qualifier in name slot + if (emanationOverride) { + // Cards 48-49 — single-line title, no qualifier slots. + if (nameEl) nameEl.textContent = emanationOverride; + if (qAbove) qAbove.textContent = ''; + if (qBelow) qBelow.textContent = ''; + } else { + if (nameEl) nameEl.textContent = isMajor ? title + ',' : title; + if (qAbove) qAbove.textContent = isMajor ? '' : qualifier; + if (qBelow) qBelow.textContent = isMajor ? qualifier : ''; + } + + // Reversal face — four cases: + // Polarity-split: full reversal title in qualifier slot (top-after-spin), name slot empty + // Major: title (with comma) in qualifier slot, qualifier in name slot // Non-major + reversal_qual: reversal_qualifier in qualifier slot, title in name slot // Non-major no reversal_qual: fall back to current polarity's qualifier var rQual = stageCard.querySelector('.fan-card-reversal-qualifier'); var rName = stageCard.querySelector('.fan-card-reversal-name'); if (rQual && rName) { - if (isMajor) { + if (reversalOverride) { + rQual.textContent = reversalOverride; + rName.textContent = ''; + } else if (isMajor) { rQual.textContent = title + ','; rName.textContent = qualifier; } else if (reversalQualifier) { diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index dae14b9..5ce5fad 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -424,15 +424,17 @@ line-height: 1; background: none; border: none; - color: rgba(var(--secUser), 0.6); + text-shadow: 0 0 1px rgba(0, 0, 0, 1); + color: rgba(var(--terUser), 0.6); cursor: pointer; padding: 1rem; transition: color 0.15s; pointer-events: auto; + outline: none; + box-shadow: none; - &:hover { color: rgba(var(--secUser), 1); } + &:hover { color: rgba(var(--ninUser), 1); } // Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav - &:focus:not(:focus-visible) { outline: none; box-shadow: none; } &--prev { left: 1rem; } &--next { right: 1rem; } } @@ -1392,6 +1394,12 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6); } .sea-stage-content { + // Card width drives the stage-card AND the stat block (which reuses + // --sig-card-w via the shared stat-block-shared mixin's calc rules). + // Override per breakpoint below to keep the stage from blowing up on small + // screens. + --sig-card-w: 180px; + position: relative; z-index: 1; display: flex; @@ -1401,6 +1409,15 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6); padding: 1.5rem; } +// Sea-stage mobile breakpoints — mirror the fan modal's portrait/landscape +// trigger thresholds so behavior across staging surfaces stays consistent. +@media (orientation: portrait) and (max-width: 480px) { + .sea-stage-content { --sig-card-w: 130px; } +} +@media (orientation: landscape) and (max-height: 500px) { + .sea-stage-content { --sig-card-w: 130px; } +} + // Stage card — size matches sig-select stage (--sig-card-w driven by inline style) .sea-stage-card { flex-shrink: 0; diff --git a/src/templates/apps/gameboard/_partials/_sea_overlay.html b/src/templates/apps/gameboard/_partials/_sea_overlay.html index 764d17e..5f230b2 100644 --- a/src/templates/apps/gameboard/_partials/_sea_overlay.html +++ b/src/templates/apps/gameboard/_partials/_sea_overlay.html @@ -134,7 +134,7 @@ {# ── Sea stage — big card viewer ─────────────────────────────────────────── #}