diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py
index b6a493d..8ad4a72 100644
--- a/src/apps/epic/tests/integrated/test_views.py
+++ b/src/apps/epic/tests/integrated/test_views.py
@@ -4162,6 +4162,19 @@ class PickSeaUnifiedFeltTest(TestCase):
content = self.client.get(self.url).content.decode()
self.assertIn("room-menu-sea", content)
+ def test_sea_stage_renders_back_img_for_image_deck(self):
+ """The sea-stage FLIP reveals the card back. The back-img now renders for
+ ANY image-equipped deck WITH a back image (polarized or not) — dropping
+ the old `not is_polarized` gate — so the Gravity/Levity draw can show a
+ polarity-tinted back (user-spec 2026-06-07)."""
+ minch = DeckVariant.objects.get(slug="minchiate-fiorentine-1860-1890")
+ founder = self.gamers[0]
+ founder.unlocked_decks.add(minch)
+ founder.equipped_deck = minch
+ founder.save(update_fields=["equipped_deck"])
+ content = self.client.get(self.url).content.decode()
+ self.assertIn("sig-stage-card-back-img", content)
+
def test_always_two_deck_stacks_gravity_and_levity(self):
"""Unlike my_sea / Sig Select, the room Sea Select ALWAYS shows BOTH the
Gravity + Levity stacks — the gamer draws from either populated half
diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss
index 9c7deca..dd737a2 100644
--- a/src/static_src/scss/_card-deck.scss
+++ b/src/static_src/scss/_card-deck.scss
@@ -2328,6 +2328,36 @@ $_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .f
);
}
+// ── FLIP-to-back polarity tint + border (user-spec 2026-06-07) ────────────────
+// The card-back art is identical for both polarities, so the FLIP reveal would
+// be ambiguous. Distinguish the two stacks: a 0.3-alpha fill OVERLAY (the same
+// deck fill colors) + a polarity border on the flipped card.
+// gravity → --quiUser tint / --quaUser border
+// levity → --terUser tint / --ninUser border
+// Scoped to `.is-flipped-to-back` on the sea stage only, so my_sign / applet
+// stages are untouched. The `&.sig-stage-card--image` chain (0,4,0) out-cascades
+// the image-mode `border:0` / transparent rules above so the border + tint show.
+.sea-stage-card.is-flipped-to-back {
+ position: relative;
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ border-radius: 0.4rem;
+ }
+}
+.sea-stage--gravity .sea-stage-card.is-flipped-to-back,
+.sea-stage--gravity .sea-stage-card.sig-stage-card--image.is-flipped-to-back {
+ border: 0.18rem solid rgba(var(--quaUser), 1);
+ &::after { background: rgba(var(--quiUser), 0.3); }
+}
+.sea-stage--levity .sea-stage-card.is-flipped-to-back,
+.sea-stage--levity .sea-stage-card.sig-stage-card--image.is-flipped-to-back {
+ border: 0.18rem solid rgba(var(--ninUser), 1);
+ &::after { background: rgba(var(--terUser), 0.3); }
+}
+
// Sea stat block — reuses sig-select stat-block sizing, scoped to sea-stage.
// `background` left blank here; the `.sea-stage--gravity` / `.sea-stage--
// levity` parent rules below set the polarity-aware bg (sig convention,
diff --git a/src/templates/apps/gameboard/_partials/_sea_overlay.html b/src/templates/apps/gameboard/_partials/_sea_overlay.html
index 6e12b1d..7b30de1 100644
--- a/src/templates/apps/gameboard/_partials/_sea_overlay.html
+++ b/src/templates/apps/gameboard/_partials/_sea_overlay.html
@@ -407,8 +407,34 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
var pickSeaBtn = document.getElementById('id_pick_sea_btn');
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
- // ── Init — seed deck, restore saved-hand state, draw labels.
- _fetchDeck();
+ // Re-seed SeaDeal's `_seaHand` from the server-rendered saved slots so they
+ // stay clickable to RE-OPEN the stage after a refresh. Without this, SeaDeal's
+ // in-memory `_seaHand` is only populated by openStage/register during the live
+ // session → after a reload the overlay click handler short-circuits on
+ // `if (!_seaHand[pos]) return` and the saved slots silently no-op (user-reported
+ // 2026-06-07; same fix my_sea carries). The card payloads come from the deck
+ // fetch (looked up by `data-card-id`); `reversed`/polarity are DOM-sourced.
+ function _seedSavedHand() {
+ if (!window.SeaDeal || !window.SeaDeal.seedHand) return;
+ var byId = {};
+ _levityPile.concat(_gravityPile).forEach(function (c) { byId[c.id] = c; });
+ var seed = {};
+ cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').forEach(function (slot) {
+ var posName = slot.dataset.posKey;
+ var card = byId[parseInt(slot.dataset.cardId, 10)];
+ if (!posName || !card) return;
+ var slotCard = {};
+ for (var k in card) if (Object.prototype.hasOwnProperty.call(card, k)) slotCard[k] = card[k];
+ slotCard.reversed = slot.classList.contains('sea-card-slot--reversed');
+ seed[posName] = { card: slotCard, isLevity: slot.classList.contains('sea-card-slot--levity') };
+ });
+ SeaDeal.seedHand(seed);
+ }
+
+ // ── Init — bind SeaDeal to the (possibly injected) overlay, seed the deck,
+ // then re-seed the saved hand once the deck fetch resolves.
+ if (window.SeaDeal && window.SeaDeal.reinit) SeaDeal.reinit();
+ _fetchDeck().then(_seedSavedHand);
_filled = cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').length;
// Already-drawn sea hand → "in Sea Select or beyond" (user-spec 2026-06-07,
// condition 3): mark sea-entered on load so the sky-saved glow stays muted
@@ -417,10 +443,6 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
if (_filled >= _currentOrder().length) { _setComplete(true); _lockSpread(); _setHasDrawn(true); }
else if (_filled > 0) { _lockSpread(); _setHasDrawn(true); }
syncLabels(hidden.value);
-
- // Re-seed SeaDeal's _seaHand from server-rendered saved slots so they stay
- // clickable to re-open the stage. (Card payloads come from the deck fetch.)
- if (window.SeaDeal && window.SeaDeal.reinit) SeaDeal.reinit();
}());
diff --git a/src/templates/apps/gameboard/_partials/_sea_stage.html b/src/templates/apps/gameboard/_partials/_sea_stage.html
index 001ac69..e7c246d 100644
--- a/src/templates/apps/gameboard/_partials/_sea_stage.html
+++ b/src/templates/apps/gameboard/_partials/_sea_stage.html
@@ -42,19 +42,19 @@
{% comment %}
- Polish-6 — back-img + FLIP btn. The back-img mirrors the
- my_sign.html / _applet-my-sign.html pattern: only renders
- for non-polarized image-equipped decks (since back-img is
- meaningless otherwise; src derives from user's equipped
- deck). FLIP btn renders UNCONDITIONALLY per user-spec "allow
- the FLIP btn everywhere"; sea.js's click handler no-ops
- when no back-img sibling exists. Multi-user gameroom is a
- known limitation here — the back-img src is the room
- viewer's deck-back, not the drawing gamer's, which is wrong
- when different gamers' decks have different backs. Parked
- for a future multi-user polish pass.
+ back-img + FLIP btn. FLIP reveals the card back. Renders for ANY
+ image-equipped deck that has a back image — INCLUDING polarized decks
+ (user-spec 2026-06-07): the Sea Select phase tints the back per the
+ drawn card's polarity (`.sea-stage--gravity` / `--levity`, see
+ `_card-deck.scss`), so the same back-art reads distinctly for the two
+ stacks (was previously gated to non-polarized decks, where it never
+ rendered for the room's Gravity/Levity draw). The FLIP btn renders
+ unconditionally; sea.js's handler no-ops when no back-img sibling
+ exists (text decks). Multi-user gameroom limitation: the src is the
+ VIEWER's deck-back — correct for the sea (each gamer draws their own),
+ parked for the general multi-user case.
{% endcomment %}
- {% if request.user.is_authenticated and request.user.equipped_deck.has_card_images and not request.user.equipped_deck.is_polarized %}
+ {% if request.user.is_authenticated and request.user.equipped_deck.has_card_images and request.user.equipped_deck.back_image_url %}
{% endif %}