From b6e93b9d64476579117e5c0e3e7e9860b2556021 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 19 May 2026 22:02:27 -0400 Subject: [PATCH] =?UTF-8?q?My=20Sea=20iter-4a=20follow-up=20batch:=20modal?= =?UTF-8?q?=20port=20+=20draw=20polish=20+=20label=20positioning=20+=20Maj?= =?UTF-8?q?or=20Arcana=20fix=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-driven bug-squash + UX-polish cycle on top of iter 4a (ca2a62f). All 14 fixes ship behind the same iter-4a banner since they close the substage's UX gaps without expanding scope to iter 4b's persistence layer. SeaDeal modal port — extracted apps/gameboard/_partials/_sea_stage.html shared by gameroom + my-sea; aliased .my-sea-picker w. id=id_sea_overlay so SeaDeal.init() finds it; FLIP click → SeaDeal.openStage delegation instead of bare _fillSlot. Fixes the user-reported 'thumbnail disappears' bug — slot was landing at opacity 0 (.--filled w.o .--visible) because SeaDeal's _hideStage (which adds --visible on modal dismiss) was never running. 3 new FTs cover the modal flow. Spread lock + DEL reshuffle — _lockSpread/_unlockSpread toggle .sea-select--locked class on the combobox; first deposit locks, _resetHand unlocks. _reshuffleDeck Fisher-Yates over combined piles + re-rolls 25% reversal axis on DEL so successive DELs don't re-deal the same hand. Verified Claudezilla: 3 DEL cycles produced distinct lay cards (150 → 114 → 155). Cover/cross empty slots — subtle dotted outline (transparent bg + 0.25 alpha border) w. --duoUser mask reveal on hover/touch. Per the user spec; rule lives in _card-deck.scss (shared between gameroom + my-sea). Plus matching label-opacity (0.25 idle → 0.6 hover) via CSS :hover ancestor propagation. DOS spec — Solution moved from cover → crown per user correction. DRAW_ORDER ['loom', 'cross', 'crown']; POSITION_LABELS {loom: Desire, cross: Obstacle, crown: Solution}; SCSS hide list flipped from [leave, crown, lay] → [leave, cover, lay]; FT/IT assertions updated. SAO → DOS soft-reload bug — Firefox autofill on hidden input restored the previous-session DOS value, tripping combobox.js's change-event guard. Fix: autocomplete=off + force-sync hidden.value from server-rendered aria-selected option in init. Captured as feedback_firefox_autofill_hidden_inputs (generalizable trap). .sea-pos-label outside .sea-card-slot — moved label to be a sibling of the slot in the cell, so SeaDeal innerHTML clobber on draw doesn't erase it. Per-position absolute positioning touching slot borders: crown/cover above (translate -50%, 0.1rem, scaleY 1.2); lay/cross below (translate -50%, -0.1rem, scaleY 1.2); leave left, CCW (writing-mode vertical-rl + rotate 180deg + scaleX 1.2); loom right, CW (writing-mode vertical-rl + scaleX 1.2). scaleX for rotated labels (not scaleY) — perpendicular to text-flow is the visible-width direction after rotation. .my-sea-cross gap bumped to 1.75rem for label clearance. Escape Velocity label swaps — POSITION_LABELS for escape-velocity: {crown: Crown, leave: Lay, cover: Cover, cross: Cross, loom: Loom, lay: Leave}. Replaces the Waite-Smith Behind/Beneath/Before per user spec. SPREAD dropdown portal — .my-sea-form-col .sea-form-main { overflow: visible } + .sea-select-list { z-index: 1000 } so the dropdown extends past the form-main scroll area + sits above the picker stacking ints. Gameroom .sea-form-main still scrolls (only my-sea opts out). Major Arcana polarity-split rendering — added 9 missing _card_dict keys to my-sea's _my_sea_deck_data to match gameroom epic.views.sea_deck's contract: levity_emanation, gravity_emanation, levity_reversal, gravity_reversal, italic_word, keywords_upright, keywords_reversed, energies, operations. Without these StageCard.populateCard falls through to plain name_title for trumps 19-21 + cards 48-49. Iter 4b cleanup candidate: extract apps.epic.utils.card_dict() to DRY the now-identical helpers. Tests deferred — user explicitly belayed FT runs during the bug-fix substage. Iter 4b will re-establish a green sweep before its commit lands. Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/apps/gameboard/views.py | 15 ++ src/functional_tests/test_game_my_sea.py | 117 +++++++++++- src/static_src/scss/_card-deck.scss | 25 ++- src/static_src/scss/_gameboard.scss | 138 +++++++++++++- .../gameboard/_partials/_sea_overlay.html | 47 +---- .../apps/gameboard/_partials/_sea_stage.html | 52 +++++ src/templates/apps/gameboard/my_sea.html | 177 ++++++++++++++---- 7 files changed, 473 insertions(+), 98 deletions(-) create mode 100644 src/templates/apps/gameboard/_partials/_sea_stage.html diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index 987c7ca..33b50fe 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -249,6 +249,21 @@ def _my_sea_deck_data(user): "levity_qualifier": c.levity_qualifier, "gravity_qualifier": c.gravity_qualifier, "reversal_qualifier": c.reversal_qualifier, + # Polarity-split full-title overrides — required for Major + # Arcana (Earthman trumps 19-21 + cards 48-49) to render + # their per-polarity emanation/reversal names on the stage + # card. Without these StageCard.populateCard falls back to + # the plain `name_title` w. no qualifier. Mirrors the + # gameroom `epic.views.sea_deck` JSON shape exactly. + "levity_emanation": c.levity_emanation, + "gravity_emanation": c.gravity_emanation, + "levity_reversal": c.levity_reversal, + "gravity_reversal": c.gravity_reversal, + "italic_word": c.italic_word, + "keywords_upright": c.keywords_upright, + "keywords_reversed": c.keywords_reversed, + "energies": c.energies, + "operations": c.operations, "reversed": random.random() < reversal_prob, } diff --git a/src/functional_tests/test_game_my_sea.py b/src/functional_tests/test_game_my_sea.py index 8d9ed1c..6aa9105 100644 --- a/src/functional_tests/test_game_my_sea.py +++ b/src/functional_tests/test_game_my_sea.py @@ -533,7 +533,7 @@ class MySeaSpreadFormTest(FunctionalTest): "past-present-future": {"leave", "cover", "loom"}, "situation-action-outcome": {"lay", "cover", "crown"}, "mind-body-spirit": {"crown", "lay", "loom"}, - "desire-obstacle-solution": {"loom", "cross", "cover"}, + "desire-obstacle-solution": {"loom", "cross", "crown"}, "waite-smith": ALL_POSITIONS, "escape-velocity": ALL_POSITIONS, } @@ -583,7 +583,7 @@ class MySeaSpreadFormTest(FunctionalTest): "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"}, + "desire-obstacle-solution": {"loom": "Desire", "cross": "Obstacle","crown":"Solution"}, "waite-smith": {"crown": "Crown", "leave": "Beneath", "cover": "Cover", "cross": "Cross", "loom": "Before", "lay": "Behind"}, } @@ -670,9 +670,10 @@ class MySeaCardDrawTest(FunctionalTest): ) ) - def _draw_one(self, picker, polarity): - """Click a polarity swatch + the FLIP btn that appears → - deposits a card. `polarity` is `'levity'` or `'gravity'`.""" + def _draw_open_modal(self, picker, polarity): + """Click a polarity swatch + the FLIP btn that appears → opens + the SeaDeal stage modal. Returns the stage element so callers + can assert on it before dismissing.""" stack = picker.find_element( By.CSS_SELECTOR, f".sea-deck-stack--{polarity}" ) @@ -680,9 +681,45 @@ class MySeaCardDrawTest(FunctionalTest): flip = self.wait_for( lambda: stack.find_element(By.CSS_SELECTOR, ".sea-stack-ok") ) - # FLIP btn becomes visible after the stack click; wait for it. self.wait_for(lambda: self.assertTrue(flip.is_displayed())) flip.click() + # SeaDeal.openStage shows #id_sea_stage. Wait for the modal. + return self.wait_for( + lambda: self._stage_visible() + ) + + def _stage_visible(self): + stage = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stage") + if not stage.is_displayed(): + raise AssertionError("sea-stage not visible after FLIP click") + return stage + + def _dismiss_modal(self): + """Click the stage backdrop → SeaDeal._hideStage → modal hides + + slot gains `.--visible` (thumbnail fades in). + + Uses `execute_script` to dispatch the click rather than a native + Selenium `.click()` — `.sea-stage-content` overlays the backdrop + visually (centered card + stat block), so Selenium reports + ElementClickInterceptedException for a direct click. This is + the documented Selenium-limitation exception per the TDD skill; + the actual backdrop-click → close behaviour is Jasmine-tested + in [[SeaDealSpec.js]] / "Backdrop click closes the stage".""" + self.browser.execute_script( + "document.querySelector('#id_sea_stage .sea-stage-backdrop').click();" + ) + self.wait_for( + lambda: self.assertFalse( + self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stage").is_displayed() + ) + ) + + def _draw_one(self, picker, polarity): + """Full single-draw cycle: open modal + dismiss it. Used by FTs + that need to deposit multiple cards in sequence (the stage + backdrop blocks subsequent deck-stack clicks).""" + self._draw_open_modal(picker, polarity) + self._dismiss_modal() # ── Test 1 ─────────────────────────────────────────────────────────────── @@ -890,3 +927,71 @@ class MySeaCardDrawTest(FunctionalTest): hint = picker.find_element(By.CSS_SELECTOR, ".sea-reversal-hint") self.assertIn("25", hint.text) self.assertIn("reversal", hint.text.lower()) + + # ── Test (modal bug fix) ──────────────────────────────────────────────── + + def test_flip_click_opens_portaled_stage_modal(self): + """Bug fix (2026-05-19): the user-reported missing modal. After + clicking the deck stack + the FLIP btn that appears, SeaDeal. + openStage should fire — showing `#id_sea_stage` (position-fixed + full-viewport portal) above everything else. Before the fix the + slot got filled directly at opacity 0 → 'thumbnail summarily + disappears'. Now: modal opens; slot stays at `--filled` but + `--visible` is NOT added yet (waits for backdrop dismiss).""" + picker = self._enter_picker_phase() + stage = self._draw_open_modal(picker, "levity") + # Stage card carries the drawn card's data — non-empty corner rank. + rank = stage.find_element( + By.CSS_SELECTOR, ".sea-stage-card .fan-card-corner--tl .fan-corner-rank" + ) + self.assertTrue(rank.text.strip(), "stage card should display the drawn card's corner rank") + # Slot in the cross is in `.--filled` state but the thumbnail is + # invisible until the modal dismisses (the bug we're guarding). + slot = picker.find_element( + By.CSS_SELECTOR, ".sea-pos-lay .sea-card-slot.sea-card-slot--filled" + ) + self.assertNotIn( + "sea-card-slot--visible", slot.get_attribute("class"), + "slot should still be in pre-reveal opacity-0 state while modal is open", + ) + + # ── Test (modal bug fix, dismiss reveal) ─────────────────────────────── + + def test_backdrop_click_dismisses_modal_and_reveals_thumbnail(self): + """Bug fix part 2: clicking the `.sea-stage-backdrop` closes the + modal AND adds `.sea-card-slot--visible` to the deposited slot, + making the thumbnail fade in. Confirms the user-reported 'card + appears where the slot was' behavior post-dismiss.""" + picker = self._enter_picker_phase() + self._draw_open_modal(picker, "levity") + self._dismiss_modal() + slot = picker.find_element( + By.CSS_SELECTOR, ".sea-pos-lay .sea-card-slot.sea-card-slot--filled" + ) + self.assertIn( + "sea-card-slot--visible", slot.get_attribute("class"), + "post-dismiss, the slot should fade in via `.--visible`", + ) + + # ── Test (modal bug fix, stat block populates) ───────────────────────── + + def test_modal_stage_renders_stat_block_dom_contract(self): + """SeaDeal._populate populates the stat-block keyword `
    `s + via `#id_sea_stat_upright` / `#id_sea_stat_reversed`. The DOM + contract — these IDs exist inside the stage — is what this FT + pins; the actual stat content (keyword text, qualifier render) + is exercised by [[SeaDealSpec.js]]. Earthman seed cards in the + iter-4a FT pile carry empty keyword arrays so we can't assert + text content here without enriching the seed.""" + picker = self._enter_picker_phase() + self._draw_open_modal(picker, "levity") + # Stat-block UL elements exist inside the visible stage. + upright = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stat_upright") + reversed_ul = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stat_reversed") + self.assertIsNotNone(upright) + self.assertIsNotNone(reversed_ul) + # The sea stat block is inside the visible stage modal. + stat_block = self.browser.find_element( + By.CSS_SELECTOR, "#id_sea_stage .sea-stat-block" + ) + self.assertIsNotNone(stat_block) diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index e31ca4a..80413a3 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -1324,9 +1324,30 @@ $sea-card-h: 6.5rem; .sea-pos-cover { z-index: 3; } // above sig (z-index: 2) .sea-pos-cross { z-index: 4; } // above cover -// Empty Cover/Cross slots are invisible — they reveal only once a card is deposited +// Empty Cover/Cross slots — subtle dotted outline (no fill) so the +// underlying Sig card shows through. Hovering/touching reveals the +// full --duoUser mask, opaquing the slot + obscuring the Sig behind. +// Border + label dim to 0.25 alpha default; bounce to full on hover. +// The filled-slot hover behavior (opacity 0.3/0.15 → 1) at lines 1300- +// 1301 is untouched — this only restyles the EMPTY state. .sea-pos-cover .sea-card-slot--empty, -.sea-pos-cross .sea-card-slot--empty { opacity: 0; pointer-events: none; } +.sea-pos-cross .sea-card-slot--empty { + background-color: transparent; + border-color: rgba(var(--terUser), 0.25); + box-shadow: none; + pointer-events: auto; + transition: background-color 0.15s ease, border-color 0.15s ease; + + .sea-pos-label { opacity: 0.25; } +} + +.sea-pos-cover .sea-card-slot--empty:hover, +.sea-pos-cross .sea-card-slot--empty:hover { + background-color: rgba(var(--duoUser), 1); + border-color: rgba(var(--terUser), 1); + + .sea-pos-label { opacity: 0.6; } +} .sea-pos-cross .sea-card-slot { transform: rotate(90deg); } diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss index c722640..3b71cec 100644 --- a/src/static_src/scss/_gameboard.scss +++ b/src/static_src/scss/_gameboard.scss @@ -297,10 +297,19 @@ body.page-gameboard { // 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 +// DOS: loom (1) cross (2) crown (3) — loom right · cross overlay · crown above // CC variants: all 6 positions (Waite-Smith / Escape Velocity differ in DRAW ORDER only, // not in position visibility). +// Bump grid gap on my-sea (gameroom .sea-cross stays at 0.5rem since +// gameroom slots have no per-position labels). The vertical leave/loom +// labels need ~1.5rem of horizontal clearance from adjacent cells, and +// the horizontal crown/cover/lay/cross labels need ~1rem of vertical +// clearance so they don't overlap into the next row. +.my-sea-cross { + gap: 1rem !important; +} + .my-sea-cross[data-spread="past-present-future"] { .sea-pos-crown, .sea-pos-cross, @@ -321,28 +330,106 @@ body.page-gameboard { .my-sea-cross[data-spread="desire-obstacle-solution"] { .sea-pos-leave, - .sea-pos-crown, + .sea-pos-cover, .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. +// Position-name caption — 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. +// +// Labels live OUTSIDE the .sea-card-slot (sibling, inside the crucifix +// cell or the cover/cross wrapper) so they survive SeaDeal._fillSlot's +// `slot.innerHTML = …` clobber on draw. Each label is absolute- +// positioned to nearly touch the slot's nearest border per the user- +// locked spec: +// crown / cover — above top border +// lay / cross — below bottom border +// leave — left of left border, rotated 90° CCW +// loom — right of right border, rotated 90° CW .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); + opacity: 1; + color: rgba(var(--seciUser), 1); text-align: center; pointer-events: none; + white-space: nowrap; + position: absolute; + z-index: 2; +} + +// Cells need `position: relative` so absolute label children anchor +// to them. `.sea-pos-core` already has `position: relative` per the +// existing rule in _card-deck.scss line 1311; the other crucifix +// cells need it added. +.my-sea-cross .sea-crucifix-cell { position: relative; } + +// Above top border — overlaps slot's top edge by 0.1rem (per the +// `.sea-stack-name` "tuck under" treatment in _card-deck.scss:1564). +.sea-pos-crown > .sea-pos-label, +.sea-pos-cover > .sea-pos-label { + bottom: 100%; + left: 50%; + transform: translate(-50%, 0.1rem) scaleY(1.2); +} + +// Cover + cross labels dim w. their slots — they sit on top of the +// sig card so a vivid label would compete w. the sig at idle. Default +// 0.25 opacity matches the slot's faint dotted-outline at idle; the +// parent's :hover state (propagated up when the inside `.sea-card- +// slot:hover` fires per CSS hover-ancestor rules) boosts to the +// `.sea-pos-label` baseline 0.6, matching the slot's `--duoUser` mask +// reveal. +.sea-pos-cover > .sea-pos-label, +.sea-pos-cross > .sea-pos-label { + opacity: 0.5; + transition: opacity 0.15s ease; +} + +.sea-pos-cover:hover > .sea-pos-label, +.sea-pos-cross:hover > .sea-pos-label { + opacity: 1; +} + +// Below bottom border — same `0.1rem` overlap but downward. +.sea-pos-lay > .sea-pos-label, +.sea-pos-cross > .sea-pos-label { + top: 100%; + left: 50%; + transform: translate(-50%, -0.1rem) scaleY(1.2); +} + +// Left of left border, rotated 90° CCW — text reads bottom-to-top. +// `writing-mode: vertical-rl` puts text top-to-bottom (CW); a 180° +// rotation flips it to read bottom-to-top (CCW), satisfying the user- +// locked "Leave: counterclockwise" spec. +// +// `scaleX(1.2)` (instead of the horizontal labels' scaleY) widens the +// character column (perpendicular to text-flow) — for vertical-rl +// labels, that's the visible "width" the user noticed had been lost +// at this angle. Without it, the rotated labels look squat. +.sea-pos-leave > .sea-pos-label { + right: 100%; + top: 50%; + writing-mode: vertical-rl; + transform: translate(0.1rem, -50%) rotate(180deg) scaleX(1.2); +} + +// Right of right border, rotated 90° CW — text reads top-to-bottom. +// Native `writing-mode: vertical-rl` direction; no extra rotation. +.sea-pos-loom > .sea-pos-label { + left: 100%; + top: 50%; + writing-mode: vertical-rl; + transform: translate(-0.1rem, -50%) scaleX(1.2); } // Section dividers inside the SPREAD combobox — labels "3-card spreads" @@ -370,6 +457,26 @@ body.page-gameboard { .my-sea-form-col { flex: 0 0 16rem; max-width: 16rem; + + // Portal the SPREAD dropdown out of `.sea-form-main`'s overflow + // clip — by default the gameroom's `.sea-form-main { overflow-y: + // auto }` (from _card-deck.scss:1424) keeps the modal contents + // scrollable, but for my-sea's much shorter form the dropdown gets + // clipped instead of overlaying the LOCK HAND / DEL btns below. + // Setting overflow visible here lets the absolute-positioned + // `.sea-select-list` extend past the form area + sit "above + // everything else" via its existing z-index: 100. + .sea-form-main { + overflow: visible; + } + + // Bump the dropdown z-index well above the picker's stacking ints + // (cover z:3, cross z:4, modal stage z:9999 only opens on draw + // anyway). 1000 sits above any in-page layer the user might be + // interacting w. when they open the SPREAD picker. + .sea-select-list { + z-index: 1000; + } } // LOCK HAND post-commit visual-lock: dim everything that mutates the @@ -385,3 +492,14 @@ body.page-gameboard { cursor: default; } } + +// SPREAD combobox lock — applied after the first deposit so the user +// can't switch spread mid-draw + scramble the in-progress hand's +// position-to-card mapping. DEL releases the lock by removing this +// class. Same `pointer-events: none` treatment as `.btn-disabled` per +// [[feedback_btn_disabled_pointer_events]]. +.sea-select.sea-select--locked { + pointer-events: none; + opacity: 0.5; + cursor: default; +} diff --git a/src/templates/apps/gameboard/_partials/_sea_overlay.html b/src/templates/apps/gameboard/_partials/_sea_overlay.html index d2879a9..988d5e3 100644 --- a/src/templates/apps/gameboard/_partials/_sea_overlay.html +++ b/src/templates/apps/gameboard/_partials/_sea_overlay.html @@ -139,50 +139,9 @@ {# /.sea-modal-wrap #} {# ── Sea stage — big card viewer ─────────────────────────────────────────── #} - + {# Extracted to a shared partial so the my-sea picker (Sprint 5 iter 4-bugs) #} + {# reuses the same DOM contract that SeaDeal binds to. #} + {% include "apps/gameboard/_partials/_sea_stage.html" %} {# /.sea-overlay #} diff --git a/src/templates/apps/gameboard/_partials/_sea_stage.html b/src/templates/apps/gameboard/_partials/_sea_stage.html new file mode 100644 index 0000000..a576020 --- /dev/null +++ b/src/templates/apps/gameboard/_partials/_sea_stage.html @@ -0,0 +1,52 @@ +{# Sea stage — full-viewport portaled modal (`position: fixed; inset: 0` #} +{# per _card-deck.scss:1615) that opens above the picker / overlay when #} +{# `SeaDeal.openStage(card, posSelector, isLevity)` fires. Hosts the #} +{# full card face + stat block + SPIN / FYI controls; click backdrop to #} +{# dismiss + reveal the deposited card thumbnail in its slot. #} +{# #} +{# Shared by the gameroom SEA SELECT phase and the my-sea picker — same #} +{# HTML, same SeaDeal module bindings; only the parent overlay differs. #} + diff --git a/src/templates/apps/gameboard/my_sea.html b/src/templates/apps/gameboard/my_sea.html index 2bb8ffd..5080d4d 100644 --- a/src/templates/apps/gameboard/my_sea.html +++ b/src/templates/apps/gameboard/my_sea.html @@ -75,18 +75,28 @@ {# Each empty slot carries a `.sea-pos-label` caption (re- #} {# appropriated from the GRAVITY/LEVITY .sea-stack-name look) #} {# that JS updates per spread. #} -