Sea Select: refactor to scroll-snap options→cross (mirror Sky Select), drop the modal — TDD

The Gaussian spread modal couldn't hang off the burger #id_sea_btn anymore (that
button now also opens the felt). Mirror Sky Select's form→wheel scroll-snap
instead: the felt starts with the spread OPTIONS on the --duoUser felt; clicking
OK confirms the spread → the options shunt DOWN and the spread CROSS takes page 1
(scroll down to find the options again). No modal, no corner NVM.

- `_sea_overlay.html` restructured into `.sea-options-col` (the .sea-select
  combobox + mini preview + OK .btn-confirm + AUTO DRAW + DEL — NO deck stacks)
  and `.sea-cross-col` (the real .my-sea-cross + the Gravity/Levity deck stacks +
  the portaled stage). `#id_sea_overlay` is a `display:contents` passthrough so
  the two cols are the scroll-snap sections.
- OK (`#id_sea_confirm_spread`) → `_chooseSpread()`: adds `sea-spread-chosen` to
  the felt → SCSS engages `scroll-snap-type:y mandatory`, the cross-col gets
  `order:-1` (page 1), options shunt to page 2; locks the combobox; eases the
  scroller to the cross. AUTO DRAW also confirms first. A reload of an in-progress
  sea renders `sea-spread-chosen` (cross revealed) server-side.
- SCSS (`_sky.scss`): the sea felt is now a column scroller; `.sea-cross-col`
  `display:none` pre-confirm; the `sea-spread-chosen` scroll-snap block mirrors
  `body.sky-saved`. The options `.sea-form-col` goes transparent/content-sized
  (blends onto the felt, not the modal's --priUser card).
- Sea sub-btn: no longer activated by openSea; it's the POST-COMPLETION reopen
  affordance (cascade activates it + `sea_btn_active = hand_complete` ctx flag),
  an active click → `window.openSeaFelt()` (review the saved spread), like the
  sky btn. Removed the sea_btn open-modal IIFE + the corner NVM.
- IT: options-on-felt (combobox + OK + AUTO DRAW + DEL + preview) w. NO modal /
  NVM. 952 epic+gameboard ITs + Jasmine + PickSeaAsyncTransitionTest(3) green.

my_sea.html keeps its modal (untouched) — the gameroom intentionally diverges.

[[project-character-creation-spec]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-07 23:53:43 -04:00
parent cf84fdc992
commit edc9a49f06
4 changed files with 254 additions and 232 deletions

View File

@@ -1,158 +1,148 @@
{% load static %}
{% comment %}
DRAW SEA — Sea Select felt + Gaussian spread modal, unified with my_sea.html
(2026-06-07). Replaces the legacy dark single-modal. TWO surfaces, mirroring
my_sea:
• Sea Select FELT (.sea-page--room) — a --duoUser fill of the hex pane; the
real Celtic-Cross spread deals here + the deck stacks. Opened by
#id_pick_sea_btn (html.sea-open); the room gear's NVM returns to the hex.
• Gaussian spread MODAL (#id_sea_spread_modal) — the .sea-select combobox
(the two Celtic Cross 6-card options ONLY, per user-spec 2026-06-07) +
AUTO DRAW + DEL + a miniaturized shape preview + a corner #id_sea_cancel
NVM (closes back to the felt). Opened via the burger #id_sea_btn.
Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
DRAW SEA — Sea Select felt (2026-06-07), mirroring Sky Select's form→wheel
scroll-snap. The felt starts with the spread OPTIONS (.sea-options-col, page 1):
the .sea-select combobox (the two Celtic Cross spreads only) + a mini shape
preview + OK + AUTO DRAW + DEL. Clicking OK confirms the spread → the options
shunt DOWN and the spread CROSS (.sea-cross-col, w. the Gravity/Levity deck
stacks) takes page 1 via scroll-snap (scroll down to find the options again).
NO modal, NO corner NVM. The burger #id_sea_btn is the post-completion REOPEN
affordance (active only once the spread is complete, like the sky btn) — NOT
active during drawing. Each draw persists onto the seat's Character.celtic_cross
via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
{% endcomment %}
<div class="sea-page sea-page--room" id="id_sea_page">
<div class="my-sea-picker{% if hand_complete %} my-sea-picker--locked{% endif %}" id="id_sea_overlay"
<div class="sea-page sea-page--room{% if saved_by_position %} sea-spread-chosen{% endif %}" id="id_sea_page">
<div id="id_sea_overlay" class="sea-overlay-content"
data-sea-deck-url="{% url 'epic:sea_deck' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}"
data-sea-save-url="{% url 'epic:sea_save' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}"
data-sea-delete-url="{% url 'epic:sea_delete' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}"
data-sea-user-polarity="{{ user_polarity }}">
{# ── Felt cross — the REAL deal target (.my-sea-cross). Sig pins core; the #}
{# six CC positions render w. labels + saved-hand pre-fill. The default #}
{# spread is the polarity-matched Celtic Cross (or the saved one). #}
<div class="sea-cards-col">
<div class="sea-cross my-sea-cross" data-spread="{{ sea_default_spread }}">
<div class="sea-crucifix-cell sea-pos-crown">
<span class="sea-pos-label" data-position="crown"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="crown" saved=saved_by_position.crown crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-leave">
<span class="sea-pos-label" data-position="leave"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="leave" saved=saved_by_position.leave crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-core">
{# Center significator — supply the card-FACE image when the sig's #}
{# deck has one (mirrors my_sea); else fall through to the corner- #}
{# rank + suit-icon text thumbnail (the tray sig stays text-only). #}
{# my_tray_sig's deck_variant is the card's OWN deck (the Sig #}
{# Select pick comes from the Role Select contributed deck via #}
{# _room_deck_variant), so this is the right image source. #}
<div class="sig-stage-card sea-sig-card{% if my_tray_sig.deck_variant.has_card_images %} sig-stage-card--image{% endif %}" style="--sig-card-w: 4rem"{% if my_tray_sig %} data-card-id="{{ my_tray_sig.id }}" data-arcana-key="{{ my_tray_sig.arcana }}"{% endif %}>
{% if my_tray_sig %}
{% if my_tray_sig.deck_variant.has_card_images %}
<img class="sig-stage-card-img" src="{{ my_tray_sig.image_url }}" alt="{{ my_tray_sig.name }}">
{% else %}
<span class="fan-corner-rank">{{ my_tray_sig.corner_rank }}</span>
{% if my_tray_sig.suit_icon %}<i class="fa-solid {{ my_tray_sig.suit_icon }}"></i>{% endif %}
{% endif %}
{% endif %}
</div>
<div class="sea-pos-cover">
<span class="sea-pos-label" data-position="cover"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="cover" saved=saved_by_position.cover crossing=False %}
</div>
<div class="sea-pos-cross">
<span class="sea-pos-label" data-position="cross"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="cross" saved=saved_by_position.cross crossing=True %}
</div>
</div>
<div class="sea-crucifix-cell sea-pos-loom">
<span class="sea-pos-label" data-position="loom"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="loom" saved=saved_by_position.loom crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-lay">
<span class="sea-pos-label" data-position="lay"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="lay" saved=saved_by_position.lay crossing=False %}
</div>
</div>
</div>
{# ── Deck stacks — pinned bottom-right of the felt, so FLIP stays usable #}
{# while the spread modal is closed. ALWAYS two stacks (Gravity + Levity), #}
{# unlike my_sea / Sig Select: in the room's Sea Select phase the gamer may #}
{# draw from EITHER populated half (sea_deck returns both), even a CARTE #}
{# user whose monodeck is presented across the two polarity stacks #}
{# (user-spec 2026-06-07). So the polarization-conditional shared partial #}
{# is intentionally NOT used here. #}
<div class="my-sea-stacks-wrap">
<div class="sea-stacks">
<span class="sea-stacks-label">DECKS</span>
<div class="sea-deck-stack sea-deck-stack--gravity">
<div class="sea-stack-face">
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Gravity</span>
</div>
<div class="sea-deck-stack sea-deck-stack--levity">
<div class="sea-stack-face">
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
</div>
</div>
{# ── Gaussian spread modal — opens on the burger #id_sea_btn. #}
<div id="id_sea_spread_modal" class="my-sea-spread-modal" hidden>
<div class="my-sea-spread-modal__backdrop"></div>
<div class="my-sea-spread-modal__panel">
{# Corner NVM (top-right) — closes the modal back to the felt. #}
<button type="button" id="id_sea_cancel" class="btn btn-cancel btn-sm sea-modal-nvm">NVM</button>
{# Miniaturized spread preview — shape only, never dealt to. #}
{% include "apps/gameboard/_partials/_sea_spread_preview.html" with preview_spread=sea_default_spread sig=my_tray_sig %}
<div class="sea-form-col">
<div class="sea-form-main">
<div class="sea-field">
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
<p class="sea-reversal-hint">{{ stack_reversal_pct|default:25 }}% reversals</p>
{# Two Celtic Cross 6-card spreads only (user-spec 2026-06-07). #}
<input type="hidden" id="id_sea_spread" name="spread" autocomplete="off"
value="{{ sea_default_spread }}">
<div class="sea-select"
data-combobox
data-combobox-target="id_sea_spread"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="id_sea_spread_label"
tabindex="0">
<span class="sea-select-current">{% if sea_default_spread == "escape-velocity" %}Celtic Cross, Escape Velocity{% else %}Celtic Cross, Waite-Smith{% endif %}</span>
<span class="sea-select-arrow" aria-hidden="true"></span>
<ul class="sea-select-list" role="listbox">
{% if user_polarity == "levity" %}
<li role="option" data-value="waite-smith" aria-selected="{% if sea_default_spread == 'escape-velocity' %}false{% else %}true{% endif %}">Celtic Cross, Waite-Smith</li>
<li role="option" data-value="escape-velocity" aria-selected="{% if sea_default_spread == 'escape-velocity' %}true{% else %}false{% endif %}">Celtic Cross, Escape Velocity</li>
{% else %}
<li role="option" data-value="escape-velocity" aria-selected="{% if sea_default_spread == 'waite-smith' %}false{% else %}true{% endif %}">Celtic Cross, Escape Velocity</li>
<li role="option" data-value="waite-smith" aria-selected="{% if sea_default_spread == 'waite-smith' %}true{% else %}false{% endif %}">Celtic Cross, Waite-Smith</li>
{% endif %}
</ul>
</div>
{# ── OPTIONS section — page 1 initially; shunts to page 2 on OK. The select #}
{# form + preview + the OK / AUTO DRAW / DEL actions. NO deck stacks here. #}
<div class="sea-options-col">
<div class="sea-form-col">
<div class="sea-form-main">
<div class="sea-field">
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
<p class="sea-reversal-hint">{{ stack_reversal_pct|default:25 }}% reversals</p>
{# Two Celtic Cross 6-card spreads only (user-spec 2026-06-07). #}
<input type="hidden" id="id_sea_spread" name="spread" autocomplete="off"
value="{{ sea_default_spread }}">
<div class="sea-select"
data-combobox
data-combobox-target="id_sea_spread"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="id_sea_spread_label"
tabindex="0">
<span class="sea-select-current">{% if sea_default_spread == "escape-velocity" %}Celtic Cross, Escape Velocity{% else %}Celtic Cross, Waite-Smith{% endif %}</span>
<span class="sea-select-arrow" aria-hidden="true"></span>
<ul class="sea-select-list" role="listbox">
{% if user_polarity == "levity" %}
<li role="option" data-value="waite-smith" aria-selected="{% if sea_default_spread == 'escape-velocity' %}false{% else %}true{% endif %}">Celtic Cross, Waite-Smith</li>
<li role="option" data-value="escape-velocity" aria-selected="{% if sea_default_spread == 'escape-velocity' %}true{% else %}false{% endif %}">Celtic Cross, Escape Velocity</li>
{% else %}
<li role="option" data-value="escape-velocity" aria-selected="{% if sea_default_spread == 'waite-smith' %}false{% else %}true{% endif %}">Celtic Cross, Escape Velocity</li>
<li role="option" data-value="waite-smith" aria-selected="{% if sea_default_spread == 'waite-smith' %}true{% else %}false{% endif %}">Celtic Cross, Waite-Smith</li>
{% endif %}
</ul>
</div>
</div>
<div class="sea-form-actions">
{# AUTO DRAW (mid-draw) — commits the remaining hand in one POST + #}
{# animates placement onto the felt. Disabled once complete (the #}
{# room has no GATE VIEW yet — character creation ends at the #}
{# Voronoi map, roadmap step 21). #}
<button type="button"
id="id_sea_action_btn"
class="btn btn-primary{% if hand_complete %} btn-disabled{% endif %}"
data-state="{% if hand_complete %}complete{% else %}auto-draw{% endif %}">AUTO<br>DRAW</button>
<button type="button"
id="id_sea_del"
class="btn btn-danger{% if not saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}DEL{% else %}&times;{% endif %}</button>
{# Miniaturized spread preview — shape only, never dealt to. Shows #}
{# the chosen spread's shape before OK reveals the real cross. #}
{% include "apps/gameboard/_partials/_sea_spread_preview.html" with preview_spread=sea_default_spread sig=my_tray_sig %}
</div>
<div class="sea-form-actions">
{# OK confirms the spread → shunts the options down + reveals the #}
{# cross (a regular .btn-confirm, NOT a big .btn-primary). #}
<button type="button" id="id_sea_confirm_spread" class="btn btn-confirm">OK</button>
{# AUTO DRAW commits the remaining hand in one POST + animates onto #}
{# the cross (confirms the spread first if not yet). DEL clears. #}
<button type="button"
id="id_sea_action_btn"
class="btn btn-primary{% if hand_complete %} btn-disabled{% endif %}"
data-state="{% if hand_complete %}complete{% else %}auto-draw{% endif %}">AUTO<br>DRAW</button>
<button type="button"
id="id_sea_del"
class="btn btn-danger{% if not saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}DEL{% else %}&times;{% endif %}</button>
</div>
</div>
</div>
{# ── CROSS section — hidden until OK; then order:-1 takes page 1. The REAL #}
{# deal target (.my-sea-cross) + the Gravity/Levity deck stacks (FLIP). #}
<div class="sea-cross-col">
<div class="sea-cards-col">
<div class="sea-cross my-sea-cross" data-spread="{{ sea_default_spread }}">
<div class="sea-crucifix-cell sea-pos-crown">
<span class="sea-pos-label" data-position="crown"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="crown" saved=saved_by_position.crown crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-leave">
<span class="sea-pos-label" data-position="leave"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="leave" saved=saved_by_position.leave crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-core">
{# Center significator — supply the card-FACE image when the sig's #}
{# deck has one (mirrors my_sea); else the corner-rank + suit-icon #}
{# text thumbnail. my_tray_sig.deck_variant is the card's OWN deck #}
{# (Sig Select picks from the Role Select contributed deck). #}
<div class="sig-stage-card sea-sig-card{% if my_tray_sig.deck_variant.has_card_images %} sig-stage-card--image{% endif %}" style="--sig-card-w: 4rem"{% if my_tray_sig %} data-card-id="{{ my_tray_sig.id }}" data-arcana-key="{{ my_tray_sig.arcana }}"{% endif %}>
{% if my_tray_sig %}
{% if my_tray_sig.deck_variant.has_card_images %}
<img class="sig-stage-card-img" src="{{ my_tray_sig.image_url }}" alt="{{ my_tray_sig.name }}">
{% else %}
<span class="fan-corner-rank">{{ my_tray_sig.corner_rank }}</span>
{% if my_tray_sig.suit_icon %}<i class="fa-solid {{ my_tray_sig.suit_icon }}"></i>{% endif %}
{% endif %}
{% endif %}
</div>
<div class="sea-pos-cover">
<span class="sea-pos-label" data-position="cover"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="cover" saved=saved_by_position.cover crossing=False %}
</div>
<div class="sea-pos-cross">
<span class="sea-pos-label" data-position="cross"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="cross" saved=saved_by_position.cross crossing=True %}
</div>
</div>
<div class="sea-crucifix-cell sea-pos-loom">
<span class="sea-pos-label" data-position="loom"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="loom" saved=saved_by_position.loom crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-lay">
<span class="sea-pos-label" data-position="lay"></span>
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="lay" saved=saved_by_position.lay crossing=False %}
</div>
</div>
</div>
{# ALWAYS two stacks (Gravity + Levity) — the gamer draws from EITHER #}
{# populated half (sea_deck returns both), even a CARTE monodeck. #}
<div class="my-sea-stacks-wrap">
<div class="sea-stacks">
<span class="sea-stacks-label">DECKS</span>
<div class="sea-deck-stack sea-deck-stack--gravity">
<div class="sea-stack-face">
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Gravity</span>
</div>
<div class="sea-deck-stack sea-deck-stack--levity">
<div class="sea-stack-face">
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
</div>
</div>
</div>
{# Sea stage — portaled big-card viewer (shared w. my_sea). #}
{# Sea stage — portaled big-card viewer (position:fixed, escapes the snap). #}
{% include "apps/gameboard/_partials/_sea_stage.html" %}
</div>
</div>
@@ -166,7 +156,6 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
if (!page || !overlay) return;
// ── Per-spread draw order + labels — the two Celtic Cross variants only.
// WS / EV share the 6 positions but differ in draw order + diagonal labels.
var DRAW_ORDER = {
'waite-smith': ['cover', 'cross', 'crown', 'lay', 'loom', 'leave'],
'escape-velocity': ['cover', 'cross', 'lay', 'leave', 'crown', 'loom'],
@@ -181,6 +170,7 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
var preview = overlay.querySelector('.sea-cross--preview');
var actionBtn = overlay.querySelector('#id_sea_action_btn');
var delBtn = overlay.querySelector('#id_sea_del');
var okBtn = overlay.querySelector('#id_sea_confirm_spread');
var seaSelect = overlay.querySelector('[data-combobox][data-combobox-target="id_sea_spread"]');
if (!hidden || !cross) return;
@@ -206,7 +196,6 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
.catch(function () {});
}
// ── FLIP affordance (CSS-driven via .sea-deck-stack--active).
function _hideOk() {
if (_activeStack) { _activeStack.classList.remove('sea-deck-stack--active'); _activeStack = null; }
}
@@ -215,16 +204,43 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
function _lockSpread() {
if (seaSelect) { seaSelect.classList.add('sea-select--locked'); seaSelect.setAttribute('aria-disabled', 'true'); }
}
function _unlockSpread() {
if (seaSelect) { seaSelect.classList.remove('sea-select--locked'); seaSelect.removeAttribute('aria-disabled'); }
// ── Confirm the chosen spread → shunt the options DOWN + reveal the cross.
// Mirrors Sky Select's form→wheel: the felt page gets `sea-spread-chosen` → the
// SCSS engages the scroll-snap (the cross takes page 1 via order:-1, the options
// shunt to page 2); lock the combobox; ease the scroller up to the cross.
// Idempotent. Triggered by OK and (defensively) by AUTO DRAW.
var _spreadChosen = page.classList.contains('sea-spread-chosen');
function _chooseSpread() {
if (_spreadChosen) return;
_spreadChosen = true;
page.classList.add('sea-spread-chosen');
_lockSpread();
_scrollToCross();
}
function _scrollToCross() {
// Pin to the options (now page 2) so the cross slides in from above, then ease
// up to the cross (page 1) — mirrors the sky felt's form-shunt reveal.
var opts = overlay.querySelector('.sea-options-col');
if (opts) page.scrollTop = opts.offsetTop;
if (page.scrollTop === 0) return;
var start = page.scrollTop, t0 = null, DUR = 320;
function step(now) {
if (t0 === null) t0 = now;
var t = Math.min(1, (now - t0) / DUR);
var u = 1 - t, e = 1 - u * u * u;
page.scrollTop = Math.max(0, start * (1 - e));
if (t < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
if (okBtn) okBtn.addEventListener('click', _chooseSpread);
function _setHasDrawn(on) {
if (delBtn) { delBtn.classList.toggle('btn-disabled', !on); delBtn.innerHTML = on ? 'DEL' : '×'; }
}
function _setComplete(on, live) {
_locked = on;
overlay.classList.toggle('my-sea-picker--locked', on);
overlay.querySelectorAll('.sea-deck-stack .sea-stack-ok').forEach(function (btn) {
btn.classList.toggle('btn-disabled', on);
btn.innerHTML = on ? '×' : 'FLIP';
@@ -234,17 +250,13 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
actionBtn.classList.toggle('btn-disabled', on);
}
_hideOk();
// Live completion (manual FLIP of the 6th card / AUTO DRAW finishing) → run
// the post-completion cascade. NOT on init (a reload of an already-complete
// sea lands on the SEED MAP hex server-side, no animation).
if (on && live) _startSeaCascade();
}
// ── Post-completion cascade (mirrors CAST SKY's _startSaveCascade) ──────────
// The 6-card spread completes → linger ~3s on the felt → the felt eases OUT
// (revealing the table-hex) → DRAW SEA gives way to SEED MAP + the sea glow
// fires (burger → sea_btn handoff) → +3s → SEED MAP eases IN. Same shape as
// the CAST SKY → sky-btn glow → DRAW SEA sequence (user-spec 2026-06-07).
// 6 cards down → linger ~3s → the felt eases OUT (revealing the table-hex) →
// DRAW SEA gives way to SEED MAP + the sea glow fires (burger → sea_btn) → +3s
// → SEED MAP eases IN. Same shape as CAST SKY → sky-btn glow → DRAW SEA.
var _SEA_CASCADE_LINGER = 3000, _SEA_FELT_FADE = 500, _SEED_DELAY = 3000;
var _cascadeRun = false;
function _startSeaCascade() {
@@ -253,16 +265,16 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
setTimeout(_cascadeFeltOut, _SEA_CASCADE_LINGER);
}
function _cascadeFeltOut() {
page.classList.add('sea-page--cascade-out'); // CSS fades the felt out
page.classList.add('sea-page--cascade-out');
setTimeout(function () {
document.documentElement.classList.remove('sea-open');
page.classList.remove('sea-page--cascade-out');
_restorePhaseBtns();
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
// DRAW SEA is stale → ease it out; keep the sea_btn active (reopen/review)
// + start the sea glow handoff on the burger, concurrent w. the hex reveal.
var seaPhaseBtn = document.getElementById('id_pick_sea_btn');
if (seaPhaseBtn) seaPhaseBtn.classList.add('hex-phase-btn--out');
// The sea btn becomes the reopen/review affordance (active only NOW — the
// post-completion cue, like the sky btn) + the sea glow fires on the burger.
var seaBtn = document.getElementById('id_sea_btn');
if (seaBtn) seaBtn.classList.add('active');
var burgerBtn = document.getElementById('id_burger_btn');
@@ -272,7 +284,7 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
}
function _cascadeSeedMapIn() {
var seedBtn = document.getElementById('id_seed_map_btn');
if (seedBtn) seedBtn.classList.remove('hex-phase-btn--out'); // eases in
if (seedBtn) seedBtn.classList.remove('hex-phase-btn--out');
}
function _collectHandFromDom() {
@@ -292,7 +304,6 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
return hand;
}
// ── Persist — upsert the full current hand onto the seat's Character.
function _postLock(hand) {
if (!hand.length) return Promise.resolve(null);
return fetch(SEA_SAVE_URL, {
@@ -335,8 +346,9 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
});
overlay.addEventListener('click', _hideOk);
// ── AUTO DRAW — commit remaining hand in ONE POST, then animate placement.
// ── AUTO DRAW — confirm the spread (reveal the cross) then commit + animate.
function _autoDraw() {
_chooseSpread(); // ensure the cross is revealed before dealing onto it
var order = _currentOrder();
var remaining = order.length - _filled;
if (remaining <= 0) return;
@@ -361,8 +373,6 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
function placeNext() {
if (idx >= autoEntries.length) { _setComplete(true, true); return; } // live → cascade
var e = autoEntries[idx++];
// Gameroom Sea Select always has BOTH polarity stacks (no single-stack
// fallback — that's a my_sea monodeck concern, not here).
var stack = overlay.querySelector('.sea-deck-stack--' + (e.isLevity ? 'levity' : 'gravity'));
if (stack) _showOk(stack);
setTimeout(function () {
@@ -404,7 +414,7 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
});
}
// ── Spread switch → re-layout the felt cross + the preview + labels.
// ── Spread switch (pre-OK) → re-layout the felt cross + the preview + labels.
function syncLabels(spread) {
var labels = POSITION_LABELS[spread] || {};
cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
@@ -419,10 +429,8 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
hidden.addEventListener('change', sync);
// ── Open / close the felt (DRAW SEA ⇄ gear NVM nav). On open: disable the
// burger Text + Sky sub-btns (Text's swipe machine would drive into the
// scroll-locked reelhouse; Sky would reopen the wheel mid-draw), light the
// sea_btn, mark html.sea-entered (mutes the sky-saved glow henceforth), and
// start the burger glow handoff. Server baseline restored on close.
// burger Text + Sky sub-btns. The sea btn is NOT activated here — it's the
// post-completion reopen affordance (see the cascade + the reopen binding).
var _disabledBtns = [];
function _disablePhaseBtns() {
_disabledBtns = [];
@@ -437,14 +445,8 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
}
function openSea() {
document.documentElement.classList.add('sea-open');
// Once DRAW SEA is engaged the sky-saved glow stays muted (user-spec
// 2026-06-07, conditions 2 & 3). The SEA glow-machine does NOT fire here —
// mirroring Sky Select, it holds until the hand is complete (see the
// completion-glow IIFE below), so drawing stays quiet.
document.documentElement.classList.add('sea-entered');
_disablePhaseBtns();
var seaBtn = document.getElementById('id_sea_btn');
if (seaBtn) seaBtn.classList.add('active');
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
}
function closeSea() {
@@ -456,13 +458,20 @@ 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);
// ── Sea sub-btn = post-completion REOPEN (active only once the spread is
// complete, like the sky btn). An ACTIVE click re-shows the felt to review the
// saved spread (window.openSeaFelt); inactive clicks fall to burger-btn.js's
// --priRd flash.
var seaSubBtn = document.getElementById('id_sea_btn');
if (seaSubBtn) {
seaSubBtn.addEventListener('click', function () {
if (!seaSubBtn.classList.contains('active')) return;
openSea();
});
}
// 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.
// stay clickable to RE-OPEN the stage after a refresh.
function _seedSavedHand() {
if (!window.SeaDeal || !window.SeaDeal.seedHand) return;
var byId = {};
@@ -480,14 +489,10 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
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.
// ── Init — bind SeaDeal, seed the deck, then re-seed the saved hand.
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
// across reloads, not just after a live DRAW SEA click.
if (_filled > 0) document.documentElement.classList.add('sea-entered');
if (_filled >= _currentOrder().length) { _setComplete(true); _lockSpread(); _setHasDrawn(true); }
else if (_filled > 0) { _lockSpread(); _setHasDrawn(true); }
@@ -495,58 +500,17 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
}());
</script>
{# ── Spread modal open/close — opens on the burger #id_sea_btn (active while #}
{# the felt is up). Closes on the corner NVM (#id_sea_cancel), the Gaussian #}
{# backdrop, Escape, or a guard-OK (so AUTO DRAW / DEL run against a closed #}
{# modal). Mirrors my_sea's Phase-2 IIFE. #}
<script>
(function () {
var seaBtn = document.getElementById('id_sea_btn');
var modal = document.getElementById('id_sea_spread_modal');
if (!seaBtn || !modal) return;
var backdrop = modal.querySelector('.my-sea-spread-modal__backdrop');
var cancelBtn = modal.querySelector('#id_sea_cancel');
function openModal() { modal.removeAttribute('hidden'); }
function closeModal() { modal.setAttribute('hidden', ''); }
seaBtn.addEventListener('click', function () {
// Inactive sub-btn click is the delegated --priRd flash (burger-btn.js);
// only open the modal when the Sea sub-btn is active (felt is up).
if (!seaBtn.classList.contains('active')) return;
openModal();
});
if (backdrop) backdrop.addEventListener('click', closeModal);
if (cancelBtn) cancelBtn.addEventListener('click', closeModal); // corner NVM → felt
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !modal.hasAttribute('hidden')) closeModal();
});
// Modal stays up through the AUTO DRAW / DEL guard portal (so it can anchor
// against the visible btn); close on guard-OK so the draw / delete proceeds
// against an already-closed modal (the spread animates onto the felt).
document.addEventListener('click', function (e) {
if (!e.target.closest('#id_guard_portal .guard-yes')) return;
if (!modal.hasAttribute('hidden')) closeModal();
});
}());
</script>
{# ── Sea glow handoff — the SEA glow-machine does NOT fire while drawing. The #}
{# post-completion CASCADE (in the picker IIFE above) STARTS it on the burger #}
{# as the felt eases out (DRAW SEA → SEED MAP), mirroring CAST SKY's burger → #}
{# sky-btn cue. This IIFE only carries the handoff: burger → sea_btn → #}
{# .sea-select → end, so the user is led to review their sea. User-spec #}
{# 2026-06-07. #}
{# .sea-select → end, so the user is led to reopen + review their sea. #}
<script>
(function () {
var burgerBtn = document.getElementById('id_burger_btn');
var seaBtn = document.getElementById('id_sea_btn');
var modal = document.getElementById('id_sea_spread_modal');
if (!burgerBtn || !seaBtn || !modal) return;
var seaSelect = modal.querySelector('.sea-select');
if (!seaSelect) return;
var seaSelect = document.querySelector('#id_sea_overlay .sea-select');
if (!burgerBtn || !seaBtn || !seaSelect) return;
var glowDone = false;
function endGlow() {
@@ -556,8 +520,6 @@ Each draw persists onto the seat's Character.celtic_cross via epic:sea_save.
seaSelect.classList.remove('glow-handoff');
}
// Handoff on clicks: burger → sea_btn → .sea-select → end. (The cascade adds
// `glow-handoff` to the burger to begin the sequence.)
burgerBtn.addEventListener('click', function () {
if (glowDone || !burgerBtn.classList.contains('glow-handoff')) return;
burgerBtn.classList.remove('glow-handoff');