My Sea per-spread positions + draw-order JS config + position labels — Sprint 5 iter 3 follow-up — TDD

User-locked spec 2026-05-19: each three-card spread uses a DIFFERENT 3-position subset of the 6 surrounding positions, in its own draw order. Replaces the iter-3 binary `data-spread-shape="three-card|six-card"` model w. per-spread `data-spread="<value>"`. Closes iter 3 cleanly + scaffolds the draw-order data iter 4 will consume.

Position subsets (per spread):
  PPF → leave (1) · cover (2) · loom  (3)
  SAO → lay   (1) · cover (2) · crown (3)
  MBS → crown (1) · lay   (2) · loom  (3)
  DOS → loom  (1) · cross (2) · cover (3)
  Waite-Smith     → all 6 surrounding (cover · cross · crown · lay · loom · leave)
  Escape Velocity → all 6 surrounding (cover · cross · lay · leave · crown · loom)

All 6 cells continue to render in DOM unconditionally — `.my-sea-cross[data-spread="<value>"]` SCSS rules hide inactive positions per spread via `display: none`. Cover/cross live nested inside `.sea-pos-core` so their absolute-overlay positioning rules from `_card-deck.scss:1310-1331` carry over for free.

**Position labels** (re-appropriated `.sea-stack-name` typography per user) — `.sea-pos-label` inside each empty `.sea-card-slot--empty` carries the per-spread caption. Server-renders SAO's labels by default (lay=Situation, cover=Action, crown=Outcome); JS swaps labels via `POSITION_LABELS[spread]` lookup on combobox change. Inactive-for-spread positions render their span w. empty `textContent` so JS only has to set text, never toggle visibility. Celtic Cross variants share the gameroom's existing position vocabulary (Crown/Beneath/Cover/Cross/Before/Behind).

**DRAW_ORDER JS const** baked into the inline picker IIFE — array of position names per spread, ready for iter 4's deck-click-deposit logic to consume. Exposed via `window._mySeaDrawOrder` so iter-4 click handlers can `window._mySeaDrawOrder[currentSpread][nextSlotIdx]` to resolve the target position. No click handlers wired yet — iter 4 territory.

**Selenium trap caught**: the combobox click-twice-on-the-toggle bug — re-clicking the combobox while `aria-expanded='true'` closes the dropdown (combobox.js's toggle behavior). Test 3's spread-cycling iterates through 6 spreads, each needs the dropdown OPEN before clicking a new option; added a `_pick(value)` helper that checks `aria-expanded` first.

Files:
- `templates/apps/gameboard/my_sea.html` — `.my-sea-cross[data-spread]` w. server-rendered default; each empty slot wraps a `<span class="sea-pos-label" data-position="<name>">` (SAO labels seeded inline, others empty initially); inline IIFE adds `DRAW_ORDER` + `POSITION_LABELS` consts + `syncLabels()` that swaps captions on `change`.
- `static_src/scss/_gameboard.scss` — drops the `data-spread-shape="three-card"|"six-card"` rules; adds 4 per-spread visibility rules (PPF/SAO/MBS/DOS). Celtic Cross variants inherit the gameroom's full 3×3 grid w. no overrides. `.sea-pos-label` style mirrors `.sea-stack-name` from _card-deck.scss line 1557 (small-uppercase-letter-spaced-scaleY) sans the polarity color — these aren't deck identifiers, just spread-position captions.
- `apps/gameboard/tests/integrated/test_views.py` — IT `test_cross_carries_initial_three_card_spread_shape` renamed + retargeted to `data-spread="situation-action-outcome"`; new IT `test_template_renders_sao_position_labels_on_default` pins the seeded SAO labels + empty spans for inactive positions.
- `functional_tests/test_game_my_sea.py` — iter-2's `test_picker_hides_six_card_only_positions_by_default` renamed to `test_picker_renders_sao_default_position_subset` w. SAO-specific visibility expectations (lay/cover/crown visible; leave/loom/cross hidden). iter-3's `test_picking_celtic_cross_reveals_six_card_positions` rewritten + expanded to `test_picking_spread_swaps_data_spread_and_position_visibility` — cycles through all 6 spreads, asserts `data-spread` attribute + per-position `is_displayed()` for each. New `test_per_spread_position_labels_render_and_update` cycles through 5 spreads (SAO default + 4 switches) asserting captions match the spec.

Tests: 33/33 FT green across test_bill_my_sign + test_game_my_sea; 1049/1049 IT/UT green in 52s.

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-05-19 19:38:53 -04:00
parent fd5db951a7
commit f154d660bd
4 changed files with 270 additions and 93 deletions

View File

@@ -695,9 +695,27 @@ class MySeaSpreadFormTemplateTest(TestCase):
'data-value="situation-action-outcome" aria-selected="true"', html,
)
def test_cross_carries_initial_three_card_spread_shape(self):
def test_cross_carries_initial_data_spread_sao(self):
# `.my-sea-cross[data-spread]` is the per-spread visibility key;
# default-spread context value renders into the attribute. SCSS
# rules in _gameboard.scss hide the inactive positions per spread.
response = self.client.get(reverse("my_sea"))
self.assertContains(response, 'data-spread-shape="three-card"')
self.assertContains(response, 'data-spread="situation-action-outcome"')
def test_template_renders_sao_position_labels_on_default(self):
# Server-renders the SAO position labels into the empty drop-zone
# `.sea-pos-label` spans so the page is correct before JS boots.
# JS swaps labels on spread change.
response = self.client.get(reverse("my_sea"))
html = response.content.decode()
self.assertIn('data-position="lay">Situation</span>', html)
self.assertIn('data-position="cover">Action</span>', html)
self.assertIn('data-position="crown">Outcome</span>', html)
# Inactive-for-SAO positions render their span but w. empty
# textContent (JS fills them on spread switch).
self.assertIn('data-position="leave"></span>', html)
self.assertIn('data-position="loom"></span>', html)
self.assertIn('data-position="cross"></span>', html)
def test_form_col_renders_decks_lock_hand_del_and_reversal_hint(self):
response = self.client.get(reverse("my_sea"))

View File

@@ -399,24 +399,24 @@ class MySeaPickerPhaseTest(FunctionalTest):
# ── Test 3 ───────────────────────────────────────────────────────────────
def test_picker_hides_six_card_only_positions_by_default(self):
"""Crown / lay / cross are 6-card-spread-only positions; they
sit in the picker DOM (so iter 3's spread switch can reveal
them without re-rendering) but stay `display:none` while the
default 3-card spread (`data-spread-shape="three-card"`) is
active. Iter 3's SPREAD dropdown switches `data-spread-shape`
on change to reveal these positions for Celtic Cross variants."""
def test_picker_renders_sao_default_position_subset(self):
"""Default spread = Situation/Action/Outcome (SAO) → only lay
(Situation) + cover (Action) + crown (Outcome) visible from the
6 surrounding positions; leave / loom / cross hidden. All 6
cells render in DOM so spread-switching never re-mutates the
cross structure — per-spread visibility lives in SCSS via
`.my-sea-cross[data-spread="..."]` rules."""
picker = self._enter_picker_phase()
for hidden_pos in (".sea-pos-crown", ".sea-pos-lay", ".sea-pos-cross"):
with self.subTest(position=hidden_pos):
elements = picker.find_elements(By.CSS_SELECTOR, hidden_pos)
visible = {".sea-pos-lay", ".sea-pos-cover", ".sea-pos-crown"}
hidden = {".sea-pos-leave", ".sea-pos-loom", ".sea-pos-cross"}
for pos in visible | hidden:
with self.subTest(position=pos):
elements = picker.find_elements(By.CSS_SELECTOR, pos)
self.assertEqual(len(elements), 1, f"{pos} should render in DOM")
expected_visible = pos in visible
self.assertEqual(
len(elements), 1,
f"{hidden_pos} should render in DOM (gets shown on 6-card spread switch)",
)
self.assertFalse(
elements[0].is_displayed(),
f"{hidden_pos} should be hidden in default 3-card spread",
elements[0].is_displayed(), expected_visible,
f"{pos} visibility wrong for SAO default; expected {expected_visible}",
)
@@ -495,16 +495,13 @@ class MySeaSpreadFormTest(FunctionalTest):
def test_default_spread_is_situation_action_outcome(self):
"""Per the spec, `Situation, Action, Outcome` is the default
spread on landing — selected in the combobox + reflected in
the hidden `<input id="id_sea_spread">` initial value. The
combobox's `.sea-select-current` `<span>` carries the visible
label (rendered even when the dropdown is closed)."""
the hidden `<input id="id_sea_spread">` initial value + on
`.my-sea-cross[data-spread]`."""
picker = self._enter_picker_phase()
hidden = picker.find_element(By.CSS_SELECTOR, "#id_sea_spread")
self.assertEqual(
hidden.get_attribute("value"), "situation-action-outcome",
)
# Selected <li> is inside the closed dropdown — its `.text` is
# "" per Selenium's hidden-text convention; use textContent.
selected = picker.find_element(
By.CSS_SELECTOR, ".sea-select-list [role='option'][aria-selected='true']"
)
@@ -512,52 +509,132 @@ class MySeaSpreadFormTest(FunctionalTest):
selected.get_attribute("textContent").strip(),
"Situation, Action, Outcome",
)
# The visible current-label span is always rendered.
current = picker.find_element(By.CSS_SELECTOR, ".sea-select-current")
self.assertEqual(current.text.strip(), "Situation, Action, Outcome")
# And the cross is in three-card shape mode by default.
cross = picker.find_element(By.CSS_SELECTOR, ".my-sea-cross")
self.assertEqual(cross.get_attribute("data-spread-shape"), "three-card")
self.assertEqual(
cross.get_attribute("data-spread"), "situation-action-outcome",
)
# ── Test 3 ───────────────────────────────────────────────────────────────
def test_picking_celtic_cross_reveals_six_card_positions(self):
"""Clicking a 6-card spread option swaps `data-spread-shape`
to `six-card` on `.my-sea-cross` — crown / lay / cross become
visible. Three-card variants flip back to `three-card`."""
def test_picking_spread_swaps_data_spread_and_position_visibility(self):
"""Each spread reveals its own position subset (user-locked
2026-05-19):
PPF → leave + cover + loom visible
SAO → lay + cover + crown
MBS → crown + lay + loom
DOS → loom + cross + cover
CC variants → all 6 surrounding positions.
`.my-sea-cross[data-spread]` swaps on combobox change; SCSS
rules toggle the inactive positions to `display: none`."""
ALL_POSITIONS = {"crown", "leave", "cover", "cross", "loom", "lay"}
SPREAD_POSITIONS = {
"past-present-future": {"leave", "cover", "loom"},
"situation-action-outcome": {"lay", "cover", "crown"},
"mind-body-spirit": {"crown", "lay", "loom"},
"desire-obstacle-solution": {"loom", "cross", "cover"},
"waite-smith": ALL_POSITIONS,
"escape-velocity": ALL_POSITIONS,
}
picker = self._enter_picker_phase()
cross = picker.find_element(By.CSS_SELECTOR, ".my-sea-cross")
# Open the combobox + click Celtic Cross Waite-Smith
combo = picker.find_element(By.CSS_SELECTOR, "[data-combobox]")
combo.click()
waite_smith = picker.find_element(
By.CSS_SELECTOR,
".sea-select-list [role='option'][data-value='waite-smith']",
)
waite_smith.click()
self.wait_for(
lambda: self.assertEqual(
cross.get_attribute("data-spread-shape"), "six-card"
cross = picker.find_element(By.CSS_SELECTOR, ".my-sea-cross")
combo = picker.find_element(By.CSS_SELECTOR, "[data-combobox]")
def _pick(value):
# Combobox click outside an open dropdown opens it; click on
# an option inside selects + closes. Re-opening for each pick
# keeps the test deterministic.
if combo.get_attribute("aria-expanded") != "true":
combo.click()
opt = picker.find_element(
By.CSS_SELECTOR,
f".sea-select-list [role='option'][data-value='{value}']",
)
)
for pos in (".sea-pos-crown", ".sea-pos-lay", ".sea-pos-cross"):
with self.subTest(position=pos):
self.assertTrue(
picker.find_element(By.CSS_SELECTOR, pos).is_displayed(),
f"{pos} should be visible when 6-card spread selected",
opt.click()
for value, expected_visible in SPREAD_POSITIONS.items():
with self.subTest(spread=value):
_pick(value)
self.wait_for(
lambda v=value: self.assertEqual(
cross.get_attribute("data-spread"), v
)
)
# Switch back to a three-card spread → hidden again.
combo.click()
mbs = picker.find_element(
By.CSS_SELECTOR,
".sea-select-list [role='option'][data-value='mind-body-spirit']",
for pos in ALL_POSITIONS:
element = picker.find_element(
By.CSS_SELECTOR, f".sea-pos-{pos}"
)
should_show = pos in expected_visible
self.assertEqual(
element.is_displayed(), should_show,
f"spread={value} pos={pos}: expected is_displayed={should_show}",
)
# ── Test 4 ───────────────────────────────────────────────────────────────
def test_per_spread_position_labels_render_and_update(self):
"""Each visible empty slot carries a `.sea-pos-label` caption
whose text matches the spread's per-position label map (e.g.
SAO default: lay='Situation', cover='Action', crown='Outcome').
JS updates labels on spread change. Reappropriates the
GRAVITY/LEVITY (`.sea-stack-name`) caption styling."""
SPREAD_LABELS = {
"situation-action-outcome": {"lay": "Situation", "cover": "Action", "crown": "Outcome"},
"past-present-future": {"leave": "Past", "cover": "Present", "loom": "Future"},
"mind-body-spirit": {"crown": "Mind", "lay": "Body", "loom": "Spirit"},
"desire-obstacle-solution": {"loom": "Desire", "cross": "Obstacle","cover":"Solution"},
"waite-smith": {"crown": "Crown", "leave": "Beneath", "cover": "Cover",
"cross": "Cross", "loom": "Before", "lay": "Behind"},
}
picker = self._enter_picker_phase()
combo = picker.find_element(By.CSS_SELECTOR, "[data-combobox]")
def _pick(value):
if combo.get_attribute("aria-expanded") != "true":
combo.click()
picker.find_element(
By.CSS_SELECTOR,
f".sea-select-list [role='option'][data-value='{value}']",
).click()
# SAO default — assert labels via the server-rendered initial state.
for pos, expected_label in SPREAD_LABELS["situation-action-outcome"].items():
with self.subTest(spread="situation-action-outcome", position=pos):
label_el = picker.find_element(
By.CSS_SELECTOR,
f".sea-pos-label[data-position='{pos}']",
)
self.assertEqual(
label_el.get_attribute("textContent").strip(),
expected_label,
)
# Switch to each other spread + verify the labels update.
for spread, position_to_label in SPREAD_LABELS.items():
if spread == "situation-action-outcome":
continue
_pick(spread)
for pos, expected_label in position_to_label.items():
with self.subTest(spread=spread, position=pos):
label_el = self.wait_for(
lambda p=pos, lbl=expected_label: self._wait_label(p, lbl, picker)
)
self.assertEqual(
label_el.get_attribute("textContent").strip(),
expected_label,
)
def _wait_label(self, position, expected_label, picker):
el = picker.find_element(
By.CSS_SELECTOR, f".sea-pos-label[data-position='{position}']"
)
mbs.click()
self.wait_for(
lambda: self.assertEqual(
cross.get_attribute("data-spread-shape"), "three-card"
if el.get_attribute("textContent").strip() != expected_label:
raise AssertionError(
f"label@{position}: got "
f"{el.get_attribute('textContent')!r}, want {expected_label!r}"
)
)
return el
# ── Test 4 ───────────────────────────────────────────────────────────────

View File

@@ -285,25 +285,64 @@ body.page-gameboard {
gap: 1rem;
}
// .my-sea-cross renders all 6 surrounding positions (crown/lay/cross
// PLUS leave/loom/cover) so the SPREAD dropdown can toggle 3-card vs
// 6-card layout via a single data-attribute swap (no DOM mutation).
// Default `three-card` hides the 3 forsaken positions via `display:
// none`; `six-card` (Celtic Cross variants) renders all 7 cells.
// Cover always shows (it's part of both shapes — overlaid on sig).
.my-sea-cross[data-spread-shape="three-card"] {
grid-template-areas:
"leave core loom";
grid-template-rows: auto;
// .my-sea-cross renders all 6 surrounding positions (crown/leave/lay/
// loom + cover/cross overlaid on core) unconditionally. The SPREAD
// dropdown sets `data-spread="<name>"` on this element; per-spread
// rules below hide the positions each spread doesn't use. Inherits
// the 3×3 `grid-template-areas` from _card-deck.scss line 1189-1200
// so visible cells land in their canonical positions; hidden cells
// just leave their grid slots empty.
//
// Per-spread position subsets — user-locked 2026-05-19:
// PPF: leave (1) cover (2) loom (3) — horizontal middle row
// SAO: lay (1) cover (2) crown (3) — vertical center column
// MBS: crown (1) lay (2) loom (3) — T-shape (crown + lay vertical, loom right)
// DOS: loom (1) cross (2) cover (3) — sig-anchored cluster + loom
// CC variants: all 6 positions (Waite-Smith / Escape Velocity differ in DRAW ORDER only,
// not in position visibility).
.my-sea-cross[data-spread="past-present-future"] {
.sea-pos-crown,
.sea-pos-lay,
.sea-pos-cross,
.sea-pos-lay { display: none; }
}
.my-sea-cross[data-spread="situation-action-outcome"] {
.sea-pos-leave,
.sea-pos-loom,
.sea-pos-cross { display: none; }
}
.my-sea-cross[data-spread-shape="six-card"] {
// Inherits `.sea-cross`'s 3×3 template from _card-deck.scss line 1189-
// 1200; nothing to override layout-wise. All cells visible.
.my-sea-cross[data-spread="mind-body-spirit"] {
.sea-pos-leave,
.sea-pos-cover,
.sea-pos-cross { display: none; }
}
.my-sea-cross[data-spread="desire-obstacle-solution"] {
.sea-pos-leave,
.sea-pos-crown,
.sea-pos-lay { display: none; }
}
// Celtic Cross variants (waite-smith / escape-velocity) — all positions
// visible by default. No `display: none` overrides needed.
// Position-name caption inside each empty `.sea-card-slot--empty` —
// re-appropriates the GRAVITY/LEVITY `.sea-stack-name` typographic
// look (_card-deck.scss line 1557): small uppercase letter-spaced w.
// a subtle scaleY stretch, --terUser ink at 0.6 opacity. No polarity
// coloring — these are spread-position labels, not deck identifiers.
.sea-pos-label {
font-size: 0.65rem;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 600;
opacity: 0.6;
transform: scaleY(1.2);
color: rgba(var(--terUser), 1);
text-align: center;
pointer-events: none;
}
// Section dividers inside the SPREAD combobox — labels "3-card spreads"

View File

@@ -66,21 +66,27 @@
</div>
</div>
{# Picker phase — flexible spread layout. Sig pins .sea-pos- #}
{# core; the 6 surrounding positions (crown/leave/loom/lay/ #}
{# cover/cross) all render unconditionally so the SPREAD #}
{# dropdown can swap `.my-sea-cross[data-spread-shape]` #}
{# between `three-card` (default — hides crown/lay/cross via #}
{# SCSS) + `six-card` (Celtic Cross variants — shows all). #}
{# Hidden until FREE DRAW click swaps data-phase to `picker`. #}
{# Picker phase — per-spread flexible layout. Sig pins .sea- #}
{# pos-core; the 6 surrounding positions all render in DOM #}
{# so the SPREAD dropdown can swap `.my-sea-cross[data- #}
{# spread]` between the 4 three-card variants (each w. its #}
{# own 3-position subset + draw order) + the 2 six-card #}
{# Celtic Cross variants (all 6 surrounding positions). #}
{# Each empty slot carries a `.sea-pos-label` caption (re- #}
{# appropriated from the GRAVITY/LEVITY .sea-stack-name look) #}
{# that JS updates per spread. #}
<div class="my-sea-picker" style="display:none">
<div class="sea-cards-col">
<div class="sea-cross my-sea-cross" data-spread-shape="three-card">
<div class="sea-cross my-sea-cross" data-spread="{{ default_spread }}">
<div class="sea-crucifix-cell sea-pos-crown">
<div class="sea-card-slot sea-card-slot--empty"></div>
<div class="sea-card-slot sea-card-slot--empty">
<span class="sea-pos-label" data-position="crown">Outcome</span>
</div>
</div>
<div class="sea-crucifix-cell sea-pos-leave">
<div class="sea-card-slot sea-card-slot--empty"></div>
<div class="sea-card-slot sea-card-slot--empty">
<span class="sea-pos-label" data-position="leave"></span>
</div>
</div>
<div class="sea-crucifix-cell sea-pos-core">
<div class="sig-stage-card sea-sig-card"
@@ -89,17 +95,25 @@
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% endif %}
</div>
<div class="sea-pos-cover">
<div class="sea-card-slot sea-card-slot--empty"></div>
<div class="sea-card-slot sea-card-slot--empty">
<span class="sea-pos-label" data-position="cover">Action</span>
</div>
</div>
<div class="sea-pos-cross">
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing">
<span class="sea-pos-label" data-position="cross"></span>
</div>
</div>
</div>
<div class="sea-crucifix-cell sea-pos-loom">
<div class="sea-card-slot sea-card-slot--empty"></div>
<div class="sea-card-slot sea-card-slot--empty">
<span class="sea-pos-label" data-position="loom"></span>
</div>
</div>
<div class="sea-crucifix-cell sea-pos-lay">
<div class="sea-card-slot sea-card-slot--empty"></div>
<div class="sea-card-slot sea-card-slot--empty">
<span class="sea-pos-label" data-position="lay">Situation</span>
</div>
</div>
</div>
</div>
@@ -170,20 +184,49 @@
<script src="{% static 'apps/epic/combobox.js' %}"></script>
<script>
(function () {
// Spread → cross-shape mapping. Celtic Cross variants
// (Waite-Smith / Escape Velocity) get the 6-card shape;
// every other spread is 3-card (cover/leave/loom only).
var SIX_CARD_SPREADS = ['waite-smith', 'escape-velocity'];
// Per-spread draw order + position labels — locked in spec
// (user 2026-05-19). Each three-card spread uses a DIFFERENT
// 3-position subset of the 6 surrounding positions, in a
// specific order. The Celtic Cross variants share position
// labels (Crown/Beneath/Cover/Cross/Before/Behind — gameroom
// vocabulary) but differ in draw order.
//
// DRAW_ORDER feeds iter 4's deck-click-deposit logic; for
// iter 3 it's just baked-in metadata.
var DRAW_ORDER = {
'past-present-future': ['leave', 'cover', 'loom'],
'situation-action-outcome': ['lay', 'cover', 'crown'],
'mind-body-spirit': ['crown', 'lay', 'loom'],
'desire-obstacle-solution': ['loom', 'cross', 'cover'],
'waite-smith': ['cover', 'cross', 'crown', 'lay', 'loom', 'leave'],
'escape-velocity': ['cover', 'cross', 'lay', 'leave', 'crown', 'loom'],
};
var POSITION_LABELS = {
'past-present-future': { leave: 'Past', cover: 'Present', loom: 'Future' },
'situation-action-outcome': { lay: 'Situation', cover: 'Action', crown: 'Outcome' },
'mind-body-spirit': { crown: 'Mind', lay: 'Body', loom: 'Spirit' },
'desire-obstacle-solution': { loom: 'Desire', cross: 'Obstacle',cover: 'Solution' },
'waite-smith': { crown: 'Crown', leave: 'Beneath', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Behind' },
'escape-velocity': { crown: 'Crown', leave: 'Beneath', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Behind' },
};
var hidden = document.getElementById('id_sea_spread');
var cross = document.querySelector('.my-sea-cross');
if (!hidden || !cross) return;
function syncLabels(spread) {
var labels = POSITION_LABELS[spread] || {};
cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
var pos = el.dataset.position;
el.textContent = labels[pos] || '';
});
}
function sync() {
var shape = SIX_CARD_SPREADS.indexOf(hidden.value) >= 0
? 'six-card' : 'three-card';
cross.setAttribute('data-spread-shape', shape);
cross.setAttribute('data-spread', hidden.value);
syncLabels(hidden.value);
}
hidden.addEventListener('change', sync);
sync();
// Exposed for iter 4 (deck-click-deposit reads DRAW_ORDER).
window._mySeaDrawOrder = DRAW_ORDER;
}());
</script>