Sea Select: rebuild as a felt + Gaussian spread modal, unify w. my_sea — TDD

Hollow out the gameroom DRAW SEA dark modal into the two surfaces my_sea
uses: a --duoUser felt (.sea-page--room) filling the hex pane where the
Celtic Cross deals, + a Gaussian spread modal (#id_sea_spread_modal: the
.sea-select combobox w. the two Celtic Cross 6-card opts ONLY, AUTO DRAW/DEL,
a mini preview, + a corner #id_sea_cancel NVM). Opened by DRAW SEA
(html.sea-open); the room gear's NVM (room-menu-sea) returns to the hex;
#id_text_btn + #id_sky_btn go inert while the felt is open.

- persist: epic:sea_save / sea_delete upsert the seat's Character.celtic_cross
  (none of my_sea's MySeaDraw quota/Brief machinery); room ctx adds
  saved_by_position + saved_sea_spread + sea_default_spread + hand_complete so
  a reload re-renders the filled cross. celtic_cross field already existed (no
  migration)
- mini spread preview (_sea_spread_preview.html) in BOTH the gameroom + my_sea
  modals — shape only, NEVER dealt to: SeaDeal scopes its slot queries to
  .sea-cross:not(.sea-cross--preview)
- always TWO deck stacks (Gravity + Levity) in the room Sea Select — the gamer
  draws from either populated half (sea_deck split), even a CARTE monodeck;
  unlike my_sea / Sig Select's polarization collapse
- glow coordination: the sky-saved glow is muted in the sky/sea phases
  (html.sky-open / sea-open / sea-entered); sea glow color --priYl -> --priId
  (distinct from sky's --priTk); the sea glow-machine fires at hand-COMPLETION
  (mirrors Sky Select), not during drawing
- guard copy "Auto deal cards?" -> "Auto Draw cards?" (match the AUTO DRAW btn)
- fix: drop the stale `html.sea-open #id_aperture_fill { opacity:1 }` — it
  painted the opaque z-90 fill over the z-5 felt so the spread flashed then
  vanished (same trap as the CAST SKY felt); removed the dead .sea-backdrop /
  .sea-overlay / .sea-modal-* SCSS
- tests: epic PickSeaPersistTest (7) + PickSeaUnifiedFeltTest (6) ITs; SeaDeal
  preview-scoping + BurgerSpec sky-glow-mute Jasmine specs; my_sea sig-card
  ITs scoped to .my-sea-cross (the preview adds a 2nd .sea-sig-card)

[[feedback-felt-aperture-fill-covers-felt]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-07 21:42:24 -04:00
parent 4322e1fc17
commit c037e876e2
21 changed files with 1100 additions and 293 deletions

View File

@@ -1,84 +1,107 @@
{% load static %}
{# DRAW SEA overlay — Celtic Cross spread entry #}
{# Included in room.html when table_status == "SKY_SELECT" and sky_confirmed #}
{# Layout is the reverse of CAST SKY: cards left (transparent), form right #}
<div class="sea-backdrop"></div>
<div class="sea-overlay" id="id_sea_overlay"
{% 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.
{% 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"
data-sea-deck-url="{% url 'epic:sea_deck' room.id %}"
data-sea-save-url="{% url 'epic:sea_save' room.id %}"
data-sea-delete-url="{% url 'epic:sea_delete' room.id %}"
data-sea-user-polarity="{{ user_polarity }}">
<div class="sea-modal-wrap">
<div class="sea-modal">
<header class="sea-modal-header">
<h2>SEA <span>SELECT</span></h2>
<p>Draw +6 cards to describe your character's influences and seed the map.</p>
</header>
<div class="sea-modal-body">
{# ── Cards column (transparent) ───────────────────────────── #}
<div class="sea-cards-col">
<div class="sea-cross">
{# Crown — CC pos 3 / EV pos 5 #}
<div class="sea-crucifix-cell sea-pos-crown">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
{# Behind (past) — CC pos 6 / EV pos 4 #}
<div class="sea-crucifix-cell sea-pos-leave">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
{# Center — Significator (always placed) + Cover + Cross overlaid #}
<div class="sea-crucifix-cell sea-pos-core">
<div class="sig-stage-card sea-sig-card" style="--sig-card-w: 4rem">
{% if my_tray_sig %}
<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 %}
</div>
{# Cover — CC/EV pos 1, stacked face-up on Sig #}
<div class="sea-pos-cover">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
{# Cross — CC/EV pos 2, rotated 90° on Cover #}
<div class="sea-pos-cross">
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
</div>
</div>
{# Before (future) — CC pos 5 / EV pos 6 #}
<div class="sea-crucifix-cell sea-pos-loom">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
{# Beneath (root) — CC pos 4 / EV pos 3 #}
<div class="sea-crucifix-cell sea-pos-lay">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
{# ── 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">
<div class="sig-stage-card sea-sig-card" style="--sig-card-w: 4rem">
{% if my_tray_sig %}
<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 %}
</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 %}
{# ── Form column (priUser / opaque) ───────────────────────── #}
<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>
{% comment %}
Reversal-rate hint — `stack_reversal_pct` flows from
apps.epic.utils.stack_reversal_probability via the
view. Currently a module default; placeholder UI for
a forthcoming per-user setting.
{% endcomment %}
<p class="sea-reversal-hint">{{ stack_reversal_pct|default:25 }}% reversals</p>
{% comment %}
Custom combobox — native <select> dropdowns ignore most CSS on
Firefox/Chrome (OS-rendered list); this gives full styling control.
combobox.js wires up the keyboard nav, click-outside-to-close, and
writes the chosen value to the hidden <input id="id_sea_spread"> so
sea.js's existing `spreadSel.value` read still works.
{% endcomment %}
<input type="hidden" id="id_sea_spread" name="spread"
value="{% if user_polarity == 'levity' %}waite-smith{% else %}escape-velocity{% endif %}">
{# 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"
@@ -87,176 +110,411 @@
aria-haspopup="listbox"
aria-labelledby="id_sea_spread_label"
tabindex="0">
<span class="sea-select-current">{% if user_polarity == "levity" %}Celtic Cross, Waite-Smith{% else %}Celtic Cross, Escape Velocity{% endif %}</span>
<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="true">Celtic Cross, Waite-Smith</li>
<li role="option" data-value="escape-velocity" aria-selected="false">Celtic Cross, Escape Velocity</li>
<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="true">Celtic Cross, Escape Velocity</li>
<li role="option" data-value="waite-smith" aria-selected="false">Celtic Cross, Waite-Smith</li>
<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>
{# Two face-down deck piles — tap to proffer OK #}
<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" type="button">FLIP</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" type="button">FLIP</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
</div>
</div>
<div class="sea-form-actions">
<button type="button" id="id_sea_lock_hand" class="btn btn-primary" disabled>
LOCK HAND
</button>
<button type="button" id="id_sea_del" class="btn btn-danger">
DEL
</button>
{# 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>
</div>
</div>
</div>
</div>
</div>{# /.sea-modal-body #}
</div>{# /.sea-modal #}
<button type="button" id="id_sea_cancel" class="btn btn-cancel btn-sm">NVM</button>
</div>{# /.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. #}
{# Sea stage — portaled big-card viewer (shared w. my_sea). #}
{% include "apps/gameboard/_partials/_sea_stage.html" %}
</div>{# /.sea-overlay #}
</div>
</div>
<script>
(function () {
'use strict';
const overlay = document.getElementById('id_sea_overlay');
const cancelBtn = document.getElementById('id_sea_cancel');
var page = document.getElementById('id_sea_page');
var overlay = document.getElementById('id_sea_overlay');
if (!page || !overlay) return;
function openSea() {
document.documentElement.classList.add('sea-open');
}
function closeSea() {
document.documentElement.classList.remove('sea-open');
}
const pickSeaBtn = document.getElementById('id_pick_sea_btn');
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
cancelBtn.addEventListener('click', closeSea);
// ── Deck draw ──────────────────────────────────────────────────────────────
const SEA_DECK_URL = overlay.dataset.seaDeckUrl;
const SPREAD_ORDER = {
'waite-smith': ['.sea-pos-cover', '.sea-pos-cross', '.sea-pos-crown', '.sea-pos-lay', '.sea-pos-loom', '.sea-pos-leave'],
'escape-velocity': ['.sea-pos-cover', '.sea-pos-cross', '.sea-pos-lay', '.sea-pos-leave', '.sea-pos-crown', '.sea-pos-loom'],
// ── 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'],
};
var POSITION_LABELS = {
'waite-smith': { crown: 'Crown', leave: 'Behind', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Beneath' },
'escape-velocity': { crown: 'Crown', leave: 'Leave', cover: 'Cover', cross: 'Cross', loom: 'Loom', lay: 'Lay' },
};
let levityPile = [], gravityPile = [];
let _filled = 0;
let _activeStack = null;
var hidden = overlay.querySelector('#id_sea_spread');
var cross = overlay.querySelector('.my-sea-cross');
var preview = overlay.querySelector('.sea-cross--preview');
var actionBtn = overlay.querySelector('#id_sea_action_btn');
var delBtn = overlay.querySelector('#id_sea_del');
var seaSelect = overlay.querySelector('[data-combobox][data-combobox-target="id_sea_spread"]');
if (!hidden || !cross) return;
const spreadSel = overlay.querySelector('#id_sea_spread');
const lockBtn = overlay.querySelector('#id_sea_lock_hand');
const delBtn = overlay.querySelector('#id_sea_del');
var SEA_DECK_URL = overlay.dataset.seaDeckUrl;
var SEA_SAVE_URL = overlay.dataset.seaSaveUrl;
var SEA_DELETE_URL = overlay.dataset.seaDeleteUrl;
function _spreadKey() {
return spreadSel ? spreadSel.value : 'waite-smith';
}
function _nextPosSelector() {
const order = SPREAD_ORDER[_spreadKey()] || SPREAD_ORDER['waite-smith'];
return order[_filled] || null;
}
function _hideOk() {
if (_activeStack) {
const ok = _activeStack.querySelector('.sea-stack-ok');
if (ok) ok.style.display = 'none';
_activeStack.classList.remove('sea-deck-stack--active');
_activeStack = null;
}
}
function _showOk(stack) {
_hideOk();
_activeStack = stack;
stack.classList.add('sea-deck-stack--active');
const ok = stack.querySelector('.sea-stack-ok');
if (ok) ok.style.display = '';
}
function _reset() {
_filled = 0;
_hideOk();
overlay.querySelectorAll('.sea-card-slot').forEach(s => {
s.classList.remove('sea-card-slot--filled', 'sea-card-slot--visible', 'sea-card-slot--focused', 'sea-card-slot--levity', 'sea-card-slot--gravity');
s.classList.add('sea-card-slot--empty');
s.innerHTML = '';
delete s.dataset.cardId;
delete s.dataset.posKey;
});
if (lockBtn) lockBtn.disabled = true;
if (window.SeaDeal) SeaDeal.resetHand();
_fetchDeck();
var _levityPile = [], _gravityPile = [];
var _filled = 0;
var _activeStack = null;
var _locked = false;
function _csrf() {
var m = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
return m ? decodeURIComponent(m[1]) : '';
}
function _currentOrder() { return DRAW_ORDER[hidden.value] || DRAW_ORDER['waite-smith']; }
function _fetchDeck() {
fetch(SEA_DECK_URL, { credentials: 'same-origin' })
.then(r => r.json())
.then(data => { levityPile = data.levity || []; gravityPile = data.gravity || []; })
.catch(() => {});
return fetch(SEA_DECK_URL, { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) { _levityPile = data.levity || []; _gravityPile = data.gravity || []; })
.catch(function () {});
}
overlay.querySelectorAll('.sea-deck-stack').forEach(stack => {
stack.addEventListener('click', e => {
e.stopPropagation();
_activeStack === stack ? _hideOk() : _showOk(stack);
// ── FLIP affordance (CSS-driven via .sea-deck-stack--active).
function _hideOk() {
if (_activeStack) { _activeStack.classList.remove('sea-deck-stack--active'); _activeStack = null; }
}
function _showOk(stack) { _hideOk(); _activeStack = stack; stack.classList.add('sea-deck-stack--active'); }
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'); }
}
function _setHasDrawn(on) {
if (delBtn) { delBtn.classList.toggle('btn-disabled', !on); delBtn.innerHTML = on ? 'DEL' : '×'; }
}
function _setComplete(on) {
_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';
});
const ok = stack.querySelector('.sea-stack-ok');
if (actionBtn) {
actionBtn.dataset.state = on ? 'complete' : 'auto-draw';
actionBtn.classList.toggle('btn-disabled', on);
}
_hideOk();
}
function _collectHandFromDom() {
var byPos = {};
cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').forEach(function (slot) {
var pos = slot.dataset.posKey || '';
if (!pos) return;
byPos[pos] = {
position: pos,
card_id: parseInt(slot.dataset.cardId, 10),
reversed: /sea-card-slot--reversed\b/.test(slot.className),
polarity: /sea-card-slot--levity\b/.test(slot.className) ? 'levity' : 'gravity',
};
});
var hand = [];
_currentOrder().forEach(function (pos) { if (byPos[pos]) hand.push(byPos[pos]); });
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, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': _csrf() },
body: JSON.stringify({ spread: hidden.value, hand: hand }),
}).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
}
// ── Deck-stack FLIP → deposit one card onto the felt cross.
overlay.querySelectorAll('.sea-deck-stack').forEach(function (stack) {
stack.addEventListener('click', function (e) {
e.stopPropagation();
if (_activeStack === stack) _hideOk(); else _showOk(stack);
});
var ok = stack.querySelector('.sea-stack-ok');
if (ok) {
ok.style.display = 'none';
ok.addEventListener('click', e => {
ok.addEventListener('click', function (e) {
e.stopPropagation();
const isLevity = stack.classList.contains('sea-deck-stack--levity');
const pile = isLevity ? levityPile : gravityPile;
const card = pile.length ? pile.shift() : null;
const pos = _nextPosSelector();
if (card && pos) {
if (_locked) { _hideOk(); return; }
var isLevity = stack.classList.contains('sea-deck-stack--levity');
var pile = isLevity ? _levityPile : _gravityPile;
if (!pile.length) { pile = isLevity ? _gravityPile : _levityPile; isLevity = !isLevity; }
var card = pile.length ? pile.shift() : null;
var order = _currentOrder();
var posName = order[_filled];
if (card && posName) {
if (window.SeaDeal && window.SeaDeal.openStage) {
SeaDeal.openStage(card, '.sea-pos-' + posName, isLevity);
}
_filled++;
if (lockBtn) lockBtn.disabled = (_filled < 6);
if (window.SeaDeal) SeaDeal.openStage(card, pos, isLevity);
if (_filled === 1) { _lockSpread(); _setHasDrawn(true); }
_postLock(_collectHandFromDom());
if (_filled >= order.length) _setComplete(true);
}
_hideOk();
});
}
});
overlay.addEventListener('click', _hideOk);
if (delBtn) delBtn.addEventListener('click', _reset);
// ── AUTO DRAW — commit remaining hand in ONE POST, then animate placement.
function _autoDraw() {
var order = _currentOrder();
var remaining = order.length - _filled;
if (remaining <= 0) return;
var autoEntries = [];
for (var i = 0; i < remaining; i++) {
var isLevity = false;
var pile = _gravityPile;
if (_levityPile.length && (!_gravityPile.length || Math.round(Math.random()))) { isLevity = true; pile = _levityPile; }
if (!pile.length) { isLevity = !isLevity; pile = isLevity ? _levityPile : _gravityPile; }
if (!pile.length) break;
autoEntries.push({ card: pile.shift(), posName: order[_filled + i], isLevity: isLevity });
}
var fullHand = _collectHandFromDom();
autoEntries.forEach(function (e) {
fullHand.push({ position: e.posName, card_id: e.card.id, reversed: !!e.card.reversed, polarity: e.isLevity ? 'levity' : 'gravity' });
});
_postLock(fullHand).then(function (body) {
if (!body || !body.ok) return;
if (_filled === 0) _lockSpread();
_setHasDrawn(true);
var idx = 0;
function placeNext() {
if (idx >= autoEntries.length) { _setComplete(true); return; }
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 () {
if (window.SeaDeal && window.SeaDeal.register) {
SeaDeal.register(e.card, '.sea-pos-' + e.posName, e.isLevity);
}
var sl = cross.querySelector('.sea-pos-' + e.posName + ' .sea-card-slot');
if (sl) sl.classList.add('sea-card-slot--visible');
_filled++;
_hideOk();
setTimeout(placeNext, 250);
}, 350);
}
placeNext();
});
}
// ── DEL — guard → clear the seat's celtic_cross → reload.
if (delBtn) {
delBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (delBtn.classList.contains('btn-disabled')) return;
if (!window.showGuard) return;
window.showGuard(delBtn, 'Are you sure?', function () {
fetch(SEA_DELETE_URL, {
method: 'POST', credentials: 'same-origin', headers: { 'X-CSRFToken': _csrf() },
}).then(function (r) { if (r.ok) window.location.reload(); });
});
});
}
// ── AUTO DRAW btn → guard portal.
if (actionBtn) {
actionBtn.addEventListener('click', function (e) {
e.preventDefault();
if (actionBtn.dataset.state !== 'auto-draw') return;
if (!window.showGuard) return;
window.showGuard(actionBtn, 'Auto Draw cards?', _autoDraw);
});
}
// ── Spread switch → re-layout the felt cross + the preview + labels.
function syncLabels(spread) {
var labels = POSITION_LABELS[spread] || {};
cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
el.textContent = labels[el.dataset.position] || '';
});
}
function sync() {
cross.setAttribute('data-spread', hidden.value);
if (preview) preview.setAttribute('data-spread', hidden.value);
syncLabels(hidden.value);
}
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.
var _disabledBtns = [];
function _disablePhaseBtns() {
_disabledBtns = [];
['id_text_btn', 'id_sky_btn'].forEach(function (id) {
var b = document.getElementById(id);
if (b && b.classList.contains('active')) { b.classList.remove('active'); _disabledBtns.push(b); }
});
}
function _restorePhaseBtns() {
_disabledBtns.forEach(function (b) { b.classList.add('active'); });
_disabledBtns = [];
}
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() {
document.documentElement.classList.remove('sea-open');
_restorePhaseBtns();
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
}
window.openSeaFelt = openSea;
var pickSeaBtn = document.getElementById('id_pick_sea_btn');
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
// ── Init — seed deck, restore saved-hand state, draw labels.
_fetchDeck();
if (window.SeaDeal) SeaDeal.reinit();
})();
_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); }
syncLabels(hidden.value);
// Re-seed SeaDeal's _seaHand from server-rendered saved slots so they stay
// clickable to re-open the stage. (Card payloads come from the deck fetch.)
if (window.SeaDeal && window.SeaDeal.reinit) SeaDeal.reinit();
}());
</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>
{# ── Completion glow cue (mirrors Sky Select) — the SEA glow-machine does NOT #}
{# fire while drawing. It starts only once ALL slots are drawn (the action btn #}
{# flips to data-state="complete", DRAW SEA gives way to SEED MAP), then hands #}
{# off burger → sea_btn → .sea-select so the user can review their sea. The #}
{# cue starts on the live completion TRANSITION (manual FLIP or AUTO DRAW), not #}
{# on a reload of an already-complete sea. User-spec 2026-06-07. #}
<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');
var actionBtn = document.getElementById('id_sea_action_btn');
if (!seaSelect || !actionBtn) return;
var started = false, glowDone = false;
function startGlow() {
if (started || glowDone) return;
started = true;
burgerBtn.classList.add('glow-handoff');
}
function endGlow() {
glowDone = true;
burgerBtn.classList.remove('glow-handoff');
seaBtn.classList.remove('glow-handoff');
seaSelect.classList.remove('glow-handoff');
}
// Handoff on clicks: burger → sea_btn → .sea-select → end.
burgerBtn.addEventListener('click', function () {
if (glowDone || !burgerBtn.classList.contains('glow-handoff')) return;
burgerBtn.classList.remove('glow-handoff');
seaBtn.classList.add('glow-handoff');
});
seaBtn.addEventListener('click', function () {
if (glowDone || !seaBtn.classList.contains('glow-handoff')) return;
seaBtn.classList.remove('glow-handoff');
seaSelect.classList.add('glow-handoff');
});
seaSelect.addEventListener('click', function () {
if (glowDone) return;
endGlow();
});
// Start the cue on the live completion transition (data-state → "complete").
var obs = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].attributeName !== 'data-state') continue;
if (actionBtn.dataset.state === 'complete') startGlow();
}
});
obs.observe(actionBtn, { attributes: true, attributeFilter: ['data-state'] });
}());
</script>