My Sea iter-4a follow-up batch: modal port + draw polish + label positioning + Major Arcana fix — TDD

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 <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 22:02:27 -04:00
parent ca2a62fd84
commit b6e93b9d64
7 changed files with 473 additions and 98 deletions

View File

@@ -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;
}