@@ -1,158 +1,148 @@
{% load static %}
{% comment %}
DRAW SEA — Sea Select felt + Gaussian spread modal, unified with my_sea.htm l
(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→whee l
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 sp read 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 %}× {% 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 %}× {% 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 < / l i>
< 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 = "butt on"
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 %}× {% 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-acti ons ">
{# 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 %}× {% 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 %}× {% 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 %}× {% 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' ) ;