PICK SEA styling: deck backs, card rank+icon display, fa-hand-dots Major Arcana — TDD

- migration 0010: icon='fa-hand-dots' for all Earthman Major Arcana number >= 2
  (Nomad/Schizo kept empty for distinct icons later)
- sea_deck view: switch from .values() to model instances; serializes corner_rank +
  suit_icon computed properties alongside DB fields
- sea overlay JS: _fillPos() renders <span class=fan-corner-rank> + <i fa-solid> HTML;
  tracks levity/gravity source via sea-card-slot--levity/gravity class; _reset() strips
  polarity classes; _showOk/_hideOk toggle sea-deck-stack--active
- template: gravity deck before levity; OK btn inside .sea-stack-face (absolute center);
  DECKS label (vertical-rl CCW) on stacks left; Gravity/Levity names under each pile
- _card-deck.scss: .sea-stacks-label (vertical-rl); .sea-stack-ok (absolute center on face);
  .sea-stack-name w. --quaUser/--terUser; glow on hover+:active+--active class —
  --ninUser for levity, --quaUser for gravity; sea-sig-card compact rank+icon display
- sea_partial view: ctx['room'] fix carried in from Sprint B

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-28 23:30:07 -04:00
parent 132e60864e
commit 6d75b9541f
4 changed files with 190 additions and 47 deletions

View File

@@ -0,0 +1,49 @@
"""Assign fa-hand-dots icon to all Earthman Major Arcana cards with number >= 2.
Cards 0 (The Nomad) and 1 (The Schizo) keep their existing icon value so they
can receive distinct icons later. All other Major Arcana groups (Popes, Implicit
Virtues, Elements, Realms, Explicit Virtues, Zodiac, Lunars, Planets, Inner Rings,
polarity-split finals) default to fa-hand-dots until per-group icons are assigned.
"""
from django.db import migrations
def assign_hand_dots(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__gte=2,
icon="",
).update(icon="fa-hand-dots")
def clear_hand_dots(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__gte=2,
icon="fa-hand-dots",
).update(icon="")
class Migration(migrations.Migration):
dependencies = [
("epic", "0009_schizo_card_ref_spans"),
]
operations = [
migrations.RunPython(assign_hand_dots, reverse_code=clear_hand_dots),
]

View File

@@ -1131,15 +1131,28 @@ def sea_deck(request, room_id):
.values_list('significator_id', flat=True)
)
def _card_dict(c):
return {
'id': c.id,
'name': c.name,
'arcana': c.arcana,
'suit': c.suit,
'number': c.number,
'corner_rank': c.corner_rank,
'suit_icon': c.suit_icon,
'levity_qualifier': c.levity_qualifier,
'gravity_qualifier': c.gravity_qualifier,
}
available = list(
TarotCard.objects.filter(deck_variant=deck)
.exclude(id__in=sig_ids)
.values('id', 'name', 'arcana', 'suit', 'number',
'levity_qualifier', 'gravity_qualifier')
TarotCard.objects.filter(deck_variant=deck).exclude(id__in=sig_ids)
)
_random.shuffle(available)
mid = len(available) // 2
return JsonResponse({'levity': available[:mid], 'gravity': available[mid:]})
return JsonResponse({
'levity': [_card_dict(c) for c in available[:mid]],
'gravity': [_card_dict(c) for c in available[mid:]],
})
@login_required

View File

@@ -801,14 +801,29 @@ $sea-card-h: 6.5rem;
.sea-card-slot--filled {
border-style: solid;
border-color: rgba(var(--secUser), 0.6);
flex-direction: column;
gap: 0.2rem;
.fan-corner-rank {
font-size: 1.15rem;
font-weight: 700;
line-height: 1;
}
i { font-size: 0.9rem; }
}
// Levity drawn card — standard polarity (priUser bg, secUser text)
.sea-card-slot--filled.sea-card-slot--levity {
background: rgba(var(--priUser), 1);
color: rgba(var(--terUser), 1);
font-size: 0.55rem;
font-weight: 600;
text-align: center;
padding: 0.2rem;
line-height: 1.2;
border-color: rgba(var(--secUser), 0.6);
color: rgba(var(--secUser), 0.85);
}
// Gravity drawn card — inverted polarity (secUser bg, priUser text)
.sea-card-slot--filled.sea-card-slot--gravity {
background: rgba(var(--secUser), 0.9);
border-color: rgba(var(--priUser), 0.4);
color: rgba(var(--priUser), 0.9);
}
// Cover + Cross — absolutely overlaid on the Sig card in .sea-pos-center
@@ -828,6 +843,23 @@ $sea-card-h: 6.5rem;
.sea-pos-cross .sea-card-slot { transform: rotate(90deg); }
// Sig card in center slot — compact rank + icon display
.sea-sig-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.2rem;
.fan-corner-rank {
font-size: 1.2rem;
font-weight: 700;
line-height: 1;
color: rgba(var(--secUser), 0.85);
}
i { font-size: 1rem; color: rgba(var(--secUser), 0.75); }
}
// .sig-stage-card is normally scoped inside .sig-stage — re-apply the card shell
// here so it renders correctly outside that context.
.sea-cross .sig-stage-card {
@@ -896,40 +928,86 @@ $sea-card-h: 6.5rem;
option { background: rgba(var(--priUser), 1); }
}
// Deck stacks — two face-down piles side by side
// Deck stacks — DECKS label + gravity + levity piles
.sea-stacks {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
gap: 0.75rem;
margin: 1rem 0;
}
.sea-stacks-label {
writing-mode: vertical-rl;
transform: rotate(180deg);
text-transform: uppercase;
font-size: 0.6rem;
letter-spacing: 0.15em;
opacity: 0.45;
white-space: nowrap;
flex-shrink: 0;
}
.sea-deck-stack {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
gap: 0.35rem;
cursor: pointer;
}
.sea-stack-face {
position: relative;
width: $sea-card-w;
height: $sea-card-h;
border-radius: 0.3rem;
background: rgba(var(--duoUser), 0.8);
border: 0.12rem solid rgba(var(--secUser), 0.5);
box-shadow: 0 2px 0 rgba(0,0,0,0.2), 0 4px 0 rgba(var(--duoUser),0.7), 0 5px 0 rgba(0,0,0,0.15);
border: 0.15rem solid;
display: flex;
align-items: center;
justify-content: center;
transition: box-shadow 0.15s;
.sea-deck-stack:hover & {
box-shadow: 0 2px 0 rgba(0,0,0,0.2), 0 4px 0 rgba(var(--duoUser),0.7), 0 5px 0 rgba(0,0,0,0.15),
0 0 0.5rem rgba(var(--terUser), 0.4);
}
}
.sea-deck-stack--levity .sea-stack-face { border-color: rgba(var(--terUser), 0.5); }
.sea-deck-stack--gravity .sea-stack-face { border-color: rgba(var(--quaUser), 0.5); }
.sea-stack-ok {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
}
.sea-stack-name {
font-size: 0.6rem;
letter-spacing: 0.06em;
text-transform: uppercase;
font-weight: 600;
}
.sea-deck-stack--gravity .sea-stack-name { color: rgba(var(--quaUser), 1); }
.sea-deck-stack--levity .sea-stack-name { color: rgba(var(--terUser), 1); }
// Deck backs — face-down pile colour identifies polarity
$_sea-shadow: 1px 2px 0 rgba(0,0,0,0.7), 0 4px 0 rgba(0,0,0,0.18), 2px 5px 5px rgba(0,0,0,0.5);
$_glow-levity: 0 0 0.8rem 0.15rem rgba(var(--ninUser), 0.6);
$_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
.sea-deck-stack--levity .sea-stack-face {
background: rgba(var(--terUser), 0.88);
border-color: rgba(var(--ninUser), 0.65);
box-shadow: $_sea-shadow;
}
.sea-deck-stack--gravity .sea-stack-face {
background: rgba(var(--quiUser), 0.88);
border-color: rgba(var(--quaUser), 0.65);
box-shadow: $_sea-shadow;
}
// Glow on hover, :active, and while OK is showing (--active class set by JS)
.sea-deck-stack--levity:hover .sea-stack-face,
.sea-deck-stack--levity:active .sea-stack-face,
.sea-deck-stack--levity.sea-deck-stack--active .sea-stack-face { box-shadow: $_sea-shadow, $_glow-levity; }
.sea-deck-stack--gravity:hover .sea-stack-face,
.sea-deck-stack--gravity:active .sea-stack-face,
.sea-deck-stack--gravity.sea-deck-stack--active .sea-stack-face { box-shadow: $_sea-shadow, $_glow-gravity; }
// Form action row — LOCK HAND + DEL side by side at the bottom
.sea-form-actions {

View File

@@ -30,17 +30,10 @@
</div>
{# Center — Significator (always placed) + Cover + Cross overlaid #}
<div class="sea-cross-cell sea-pos-center">
<div class="sig-stage-card" style="--sig-card-w: 4rem">
<div class="sig-stage-card sea-sig-card" style="--sig-card-w: 4rem">
{% if my_tray_sig %}
<div class="fan-card-face">
{% if my_tray_sig.arcana == "MAJOR" %}
<p class="fan-card-name">{{ my_tray_sig.name_title }}</p>
<p class="sig-qualifier-below">{% if user_polarity == "levity" %}{{ my_tray_sig.levity_qualifier }}{% else %}{{ my_tray_sig.gravity_qualifier }}{% endif %}</p>
{% else %}
<p class="sig-qualifier-above">{% if user_polarity == "levity" %}{{ my_tray_sig.levity_qualifier }}{% else %}{{ my_tray_sig.gravity_qualifier }}{% endif %}</p>
<p class="fan-card-name">{{ my_tray_sig.name_title }}</p>
{% endif %}
</div>
<span class="fan-corner-rank">{{ my_tray_sig.corner_rank }}</span>
{% if my_tray_sig.suit_icon %}<i class="fa-solid {{ my_tray_sig.suit_icon }}"></i>{% endif %}
{% endif %}
</div>
{# Cover — CC/EV pos 1, stacked face-up on Sig #}
@@ -82,14 +75,19 @@
{# Two face-down deck piles — tap to proffer OK #}
<div class="sea-stacks">
<div class="sea-deck-stack sea-deck-stack--levity">
<div class="sea-stack-face"></div>
<span class="sea-stacks-label">DECKS</span>
<div class="sea-deck-stack sea-deck-stack--gravity">
<div class="sea-stack-face">
<button class="btn btn-confirm sea-stack-ok" type="button">OK</button>
</div>
<div class="sea-deck-stack sea-deck-stack--gravity">
<div class="sea-stack-face"></div>
<span class="sea-stack-name">Gravity</span>
</div>
<div class="sea-deck-stack sea-deck-stack--levity">
<div class="sea-stack-face">
<button class="btn btn-confirm sea-stack-ok" type="button">OK</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
</div>
</div>
@@ -163,6 +161,7 @@
if (_activeStack) {
const ok = _activeStack.querySelector('.sea-stack-ok');
if (ok) ok.style.display = 'none';
_activeStack.classList.remove('sea-deck-stack--active');
_activeStack = null;
}
}
@@ -170,19 +169,23 @@
function _showOk(stack) {
_hideOk();
_activeStack = stack;
stack.classList.add('sea-deck-stack--active');
const ok = stack.querySelector('.sea-stack-ok');
if (ok) ok.style.display = '';
}
function _fillPos(sel, card) {
function _fillPos(sel, card, isLevity) {
const cell = overlay.querySelector(sel);
if (!cell) return;
const slot = cell.querySelector('.sea-card-slot');
if (!slot) return;
slot.classList.remove('sea-card-slot--empty');
slot.classList.add('sea-card-slot--filled');
slot.classList.add(isLevity ? 'sea-card-slot--levity' : 'sea-card-slot--gravity');
slot.dataset.cardId = String(card.id);
slot.textContent = card.name;
slot.innerHTML =
`<span class="fan-corner-rank">${card.corner_rank}</span>` +
(card.suit_icon ? `<i class="fa-solid ${card.suit_icon}"></i>` : '');
_filled++;
if (lockBtn) lockBtn.disabled = (_filled < 6);
}
@@ -191,9 +194,9 @@
_filled = 0;
_hideOk();
overlay.querySelectorAll('.sea-card-slot').forEach(s => {
s.classList.remove('sea-card-slot--filled');
s.classList.remove('sea-card-slot--filled', 'sea-card-slot--levity', 'sea-card-slot--gravity');
s.classList.add('sea-card-slot--empty');
s.textContent = '';
s.innerHTML = '';
delete s.dataset.cardId;
});
if (lockBtn) lockBtn.disabled = true;
@@ -221,7 +224,7 @@
const pile = isLevity ? levityPile : gravityPile;
const card = pile.length ? pile.shift() : null;
const pos = _nextPosSelector();
if (card && pos) _fillPos(pos, card);
if (card && pos) _fillPos(pos, card, isLevity);
_hideOk();
});
}