diff --git a/.woodpecker/_retry_failed.sh b/.woodpecker/_retry_failed.sh
index c357ffd..92b0abe 100644
--- a/.woodpecker/_retry_failed.sh
+++ b/.woodpecker/_retry_failed.sh
@@ -41,10 +41,13 @@ if [ "$FIRST" -eq 0 ]; then
fi
# Parse failure labels. Match both FAIL: and ERROR: lines; the dotted
-# path lives inside the trailing parens. `sort -u` dedupes if a single
-# test produces multiple lines (rare but possible).
+# path lives inside the FIRST parens. Parameterized subTest failures
+# append a SECOND `(param='...')` group, so anchor to the first group —
+# a greedy `^.*\(` would wrongly grab the subtest param and feed
+# `manage.py test` a bogus label (ModuleNotFoundError). `sort -u`
+# dedupes the repeated method label the subtests share.
FAILED=$(grep -E '^(FAIL|ERROR): ' "$LOG" \
- | sed -E 's/^.*\(([^)]+)\)[^()]*$/\1/' \
+ | sed -E 's/^[^(]*\(([^)]+)\).*/\1/' \
| sort -u \
| tr '\n' ' ')
diff --git a/src/functional_tests/test_game_my_sea.py b/src/functional_tests/test_game_my_sea.py
index 6951b91..0e55710 100644
--- a/src/functional_tests/test_game_my_sea.py
+++ b/src/functional_tests/test_game_my_sea.py
@@ -623,7 +623,11 @@ class MySeaSpreadFormTest(FunctionalTest):
By.CSS_SELECTOR,
f".sea-select-list [role='option'][data-value='{value}']",
)
- opt.click()
+ # JS-click (matches the _open_spread_modal idiom): the dropdown
+ # opens downward in landscape inside the centered, un-scrollable
+ # picker modal, so the LAST option can land below the fold where a
+ # native .click()'s auto-scroll fails (ElementNotInteractable).
+ self.browser.execute_script("arguments[0].click()", opt)
for value, expected_visible in SPREAD_POSITIONS.items():
with self.subTest(spread=value):
diff --git a/src/static/tests/SeaDealSpec.js b/src/static/tests/SeaDealSpec.js
index 7300b03..359c3e0 100644
--- a/src/static/tests/SeaDealSpec.js
+++ b/src/static/tests/SeaDealSpec.js
@@ -381,9 +381,10 @@ describe("SeaDeal", () => {
// ── Spread-preview scoping ──────────────────────────────────────────────── //
// The spread modal carries a miniaturized `.sea-cross--preview` INSIDE the
// same `#id_sea_overlay`. SeaDeal must deal ONLY onto the real cross — the
- // preview slots stay empty (user-spec 2026-06-07). Guards the trap where a
- // bare `overlay.querySelector('.sea-pos-cover')` could grab the preview's
- // slot. (Gameroom Sea Select felt rebuild.)
+ // preview slots stay empty (user-spec 2026-06-07). The shipped preview now
+ // namespaces its cells `.sea-prev-pos-*` so a bare `.sea-pos-*` never grabs
+ // it; SeaDeal also scopes via `.sea-cross:not(.sea-cross--preview)`, which
+ // this spec locks. (Gameroom Sea Select felt rebuild.)
describe("preview cross is never dealt to", () => {
beforeEach(() => {
makeFixture();
@@ -391,7 +392,7 @@ describe("SeaDeal", () => {
const preview = document.createElement("div");
preview.className = "sea-cross sea-cross--preview";
preview.innerHTML =
- '
' +
+ '
';
overlay.appendChild(preview);
SeaDeal._testInit(); // re-capture _cross with the preview present
@@ -402,7 +403,7 @@ describe("SeaDeal", () => {
const realSlot = overlay.querySelector(
".sea-cross:not(.sea-cross--preview) .sea-pos-cover .sea-card-slot");
const previewSlot = overlay.querySelector(
- ".sea-cross--preview .sea-pos-cover .sea-card-slot");
+ ".sea-cross--preview .sea-prev-pos-cover .sea-card-slot");
expect(realSlot.classList.contains("sea-card-slot--filled")).toBe(true);
expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false);
expect(previewSlot.classList.contains("sea-card-slot--empty")).toBe(true);
@@ -411,7 +412,7 @@ describe("SeaDeal", () => {
it("register() (AUTO DRAW path) also skips the preview slot", () => {
SeaDeal.register(CARD, ".sea-pos-cover", true);
const previewSlot = overlay.querySelector(
- ".sea-cross--preview .sea-pos-cover .sea-card-slot");
+ ".sea-cross--preview .sea-prev-pos-cover .sea-card-slot");
expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false);
});
});
diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss
index 30e910a..f09e111 100644
--- a/src/static_src/scss/_card-deck.scss
+++ b/src/static_src/scss/_card-deck.scss
@@ -1972,6 +1972,13 @@ $_sea-card-glow: 0 0 0.5rem 0.5rem rgba(var(--ninUser), 0.3), 0 0 0.4rem rgba(0,
border-radius: 0.3rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4);
+ // Bound the dropdown height + give it its own scroll so a long list
+ // (or a low trigger position in landscape) can never run off the
+ // bottom of the un-scrollable picker modal — the last option stays
+ // reachable instead of landing below the fold.
+ max-height: 16rem;
+ overflow-y: auto;
+
li[role="option"] {
padding: 0.4rem 0.5rem;
border-radius: 0.2rem;
diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss
index dda09b1..410d577 100644
--- a/src/static_src/scss/_gameboard.scss
+++ b/src/static_src/scss/_gameboard.scss
@@ -870,19 +870,54 @@ body.page-gameboard {
.sea-card-slot--crossing { width: 1.6rem; height: 2.6rem; }
.sea-sig-card { transform: scale(0.85); }
+ // The preview cells are namespaced `.sea-prev-pos-*` (renamed from
+ // `.sea-pos-*`) so a bare `.sea-pos-*` selector matches ONLY the live
+ // `.my-sea-cross` cells, not this preview. The live cross draws its
+ // Celtic-Cross geometry from bare `.sea-pos-*` rules in _card-deck.scss;
+ // the preview needs its own scoped copies of that geometry below.
+ .sea-prev-pos-crown { grid-area: crown; }
+ .sea-prev-pos-leave { grid-area: leave; }
+ .sea-prev-pos-core { grid-area: core; position: relative; }
+ .sea-prev-pos-loom { grid-area: loom; }
+ .sea-prev-pos-lay { grid-area: lay; }
+
+ // Cover + Cross absolutely overlaid on the center Sig card (mirrors
+ // _card-deck.scss's `.sea-pos-cover/.sea-pos-cross` overlay rules).
+ .sea-prev-pos-cover,
+ .sea-prev-pos-cross {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+ }
+ .sea-prev-pos-cover { z-index: 3; }
+ .sea-prev-pos-cross { z-index: 4; }
+ .sea-prev-pos-cross .sea-card-slot { transform: rotate(90deg); }
+
+ // Empty Cover/Cross slots stay transparent + dotted so the Sig card
+ // reads through (matches the live cross's empty-overlay styling).
+ .sea-prev-pos-cover .sea-card-slot--empty,
+ .sea-prev-pos-cross .sea-card-slot--empty {
+ background-color: transparent;
+ border-color: rgba(var(--terUser), 0.25);
+ box-shadow: none;
+ }
+
// Per-spread position subsets — mirror the .my-sea-cross hide rules so the
// preview shape matches the live cross for the 3-card spreads.
&[data-spread="past-present-future"] {
- .sea-pos-crown, .sea-pos-cross, .sea-pos-lay { display: none; }
+ .sea-prev-pos-crown, .sea-prev-pos-cross, .sea-prev-pos-lay { display: none; }
}
&[data-spread="situation-action-outcome"] {
- .sea-pos-leave, .sea-pos-loom, .sea-pos-cross { display: none; }
+ .sea-prev-pos-leave, .sea-prev-pos-loom, .sea-prev-pos-cross { display: none; }
}
&[data-spread="mind-body-spirit"] {
- .sea-pos-leave, .sea-pos-cover, .sea-pos-cross { display: none; }
+ .sea-prev-pos-leave, .sea-prev-pos-cover, .sea-prev-pos-cross { display: none; }
}
&[data-spread="desire-obstacle-solution"] {
- .sea-pos-leave, .sea-pos-cover, .sea-pos-lay { display: none; }
+ .sea-prev-pos-leave, .sea-prev-pos-cover, .sea-prev-pos-lay { display: none; }
}
// Celtic Cross variants (waite-smith / escape-velocity) — all 6 visible.
}
diff --git a/src/static_src/tests/SeaDealSpec.js b/src/static_src/tests/SeaDealSpec.js
index 7300b03..359c3e0 100644
--- a/src/static_src/tests/SeaDealSpec.js
+++ b/src/static_src/tests/SeaDealSpec.js
@@ -381,9 +381,10 @@ describe("SeaDeal", () => {
// ── Spread-preview scoping ──────────────────────────────────────────────── //
// The spread modal carries a miniaturized `.sea-cross--preview` INSIDE the
// same `#id_sea_overlay`. SeaDeal must deal ONLY onto the real cross — the
- // preview slots stay empty (user-spec 2026-06-07). Guards the trap where a
- // bare `overlay.querySelector('.sea-pos-cover')` could grab the preview's
- // slot. (Gameroom Sea Select felt rebuild.)
+ // preview slots stay empty (user-spec 2026-06-07). The shipped preview now
+ // namespaces its cells `.sea-prev-pos-*` so a bare `.sea-pos-*` never grabs
+ // it; SeaDeal also scopes via `.sea-cross:not(.sea-cross--preview)`, which
+ // this spec locks. (Gameroom Sea Select felt rebuild.)
describe("preview cross is never dealt to", () => {
beforeEach(() => {
makeFixture();
@@ -391,7 +392,7 @@ describe("SeaDeal", () => {
const preview = document.createElement("div");
preview.className = "sea-cross sea-cross--preview";
preview.innerHTML =
- '
' +
+ '
';
overlay.appendChild(preview);
SeaDeal._testInit(); // re-capture _cross with the preview present
@@ -402,7 +403,7 @@ describe("SeaDeal", () => {
const realSlot = overlay.querySelector(
".sea-cross:not(.sea-cross--preview) .sea-pos-cover .sea-card-slot");
const previewSlot = overlay.querySelector(
- ".sea-cross--preview .sea-pos-cover .sea-card-slot");
+ ".sea-cross--preview .sea-prev-pos-cover .sea-card-slot");
expect(realSlot.classList.contains("sea-card-slot--filled")).toBe(true);
expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false);
expect(previewSlot.classList.contains("sea-card-slot--empty")).toBe(true);
@@ -411,7 +412,7 @@ describe("SeaDeal", () => {
it("register() (AUTO DRAW path) also skips the preview slot", () => {
SeaDeal.register(CARD, ".sea-pos-cover", true);
const previewSlot = overlay.querySelector(
- ".sea-cross--preview .sea-pos-cover .sea-card-slot");
+ ".sea-cross--preview .sea-prev-pos-cover .sea-card-slot");
expect(previewSlot.classList.contains("sea-card-slot--filled")).toBe(false);
});
});
diff --git a/src/templates/apps/gameboard/_partials/_sea_spread_preview.html b/src/templates/apps/gameboard/_partials/_sea_spread_preview.html
index c39f3fd..d2ff876 100644
--- a/src/templates/apps/gameboard/_partials/_sea_spread_preview.html
+++ b/src/templates/apps/gameboard/_partials/_sea_spread_preview.html
@@ -7,6 +7,11 @@ NEVER receive dealt cards (SeaDeal scopes its slot queries to
shape still reads. Shared by the gameroom Sea Select modal AND my_sea's spread
modal so the two locations are identical.
+The cell classes are namespaced `.sea-prev-pos-*` (NOT `.sea-pos-*`) so a bare
+`.sea-pos-*` selector matches ONLY the live `.my-sea-cross` cells, never this
+preview. The preview's Celtic-Cross geometry is supplied by scoped copies under
+`.sea-cross--preview` in _gameboard.scss.
+
Args:
preview_spread — slug to seed `data-spread` (drives the per-spread show/hide).
sig — the significator card for the center pip (corner_rank +
@@ -14,27 +19,27 @@ Args:
{% endcomment %}
-
+
-
+
-
+
{% if sig %}{{ sig.corner_rank }}{% if sig.suit_icon %}{% endif %}{% endif %}
-