Files
python-tdd/src/static_src/scss/_card-deck.scss
Disco DeDisco b6e93b9d64 My Sea iter-4a follow-up batch: modal port + draw polish + label positioning + Major Arcana fix — TDD
User-driven bug-squash + UX-polish cycle on top of iter 4a (ca2a62f). All 14 fixes ship behind the same iter-4a banner since they close the substage's UX gaps without expanding scope to iter 4b's persistence layer.

SeaDeal modal port — extracted apps/gameboard/_partials/_sea_stage.html shared by gameroom + my-sea; aliased .my-sea-picker w. id=id_sea_overlay so SeaDeal.init() finds it; FLIP click → SeaDeal.openStage delegation instead of bare _fillSlot. Fixes the user-reported 'thumbnail disappears' bug — slot was landing at opacity 0 (.--filled w.o .--visible) because SeaDeal's _hideStage (which adds --visible on modal dismiss) was never running. 3 new FTs cover the modal flow.

Spread lock + DEL reshuffle — _lockSpread/_unlockSpread toggle .sea-select--locked class on the combobox; first deposit locks, _resetHand unlocks. _reshuffleDeck Fisher-Yates over combined piles + re-rolls 25% reversal axis on DEL so successive DELs don't re-deal the same hand. Verified Claudezilla: 3 DEL cycles produced distinct lay cards (150 → 114 → 155).

Cover/cross empty slots — subtle dotted outline (transparent bg + 0.25 alpha border) w. --duoUser mask reveal on hover/touch. Per the user spec; rule lives in _card-deck.scss (shared between gameroom + my-sea). Plus matching label-opacity (0.25 idle → 0.6 hover) via CSS :hover ancestor propagation.

DOS spec — Solution moved from cover → crown per user correction. DRAW_ORDER ['loom', 'cross', 'crown']; POSITION_LABELS {loom: Desire, cross: Obstacle, crown: Solution}; SCSS hide list flipped from [leave, crown, lay] → [leave, cover, lay]; FT/IT assertions updated.

SAO → DOS soft-reload bug — Firefox autofill on hidden input restored the previous-session DOS value, tripping combobox.js's change-event guard. Fix: autocomplete=off + force-sync hidden.value from server-rendered aria-selected option in init. Captured as feedback_firefox_autofill_hidden_inputs (generalizable trap).

.sea-pos-label outside .sea-card-slot — moved label to be a sibling of the slot in the cell, so SeaDeal innerHTML clobber on draw doesn't erase it. Per-position absolute positioning touching slot borders: crown/cover above (translate -50%, 0.1rem, scaleY 1.2); lay/cross below (translate -50%, -0.1rem, scaleY 1.2); leave left, CCW (writing-mode vertical-rl + rotate 180deg + scaleX 1.2); loom right, CW (writing-mode vertical-rl + scaleX 1.2). scaleX for rotated labels (not scaleY) — perpendicular to text-flow is the visible-width direction after rotation. .my-sea-cross gap bumped to 1.75rem for label clearance.

Escape Velocity label swaps — POSITION_LABELS for escape-velocity: {crown: Crown, leave: Lay, cover: Cover, cross: Cross, loom: Loom, lay: Leave}. Replaces the Waite-Smith Behind/Beneath/Before per user spec.

SPREAD dropdown portal — .my-sea-form-col .sea-form-main { overflow: visible } + .sea-select-list { z-index: 1000 } so the dropdown extends past the form-main scroll area + sits above the picker stacking ints. Gameroom .sea-form-main still scrolls (only my-sea opts out).

Major Arcana polarity-split rendering — added 9 missing _card_dict keys to my-sea's _my_sea_deck_data to match gameroom epic.views.sea_deck's contract: levity_emanation, gravity_emanation, levity_reversal, gravity_reversal, italic_word, keywords_upright, keywords_reversed, energies, operations. Without these StageCard.populateCard falls through to plain name_title for trumps 19-21 + cards 48-49. Iter 4b cleanup candidate: extract apps.epic.utils.card_dict() to DRY the now-identical helpers.

Tests deferred — user explicitly belayed FT runs during the bug-fix substage. Iter 4b will re-establish a green sweep before its commit lands.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 22:02:27 -04:00

1800 lines
65 KiB
SCSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─── Card deck primitives — fan cards + sig-select overlay ─────────────────────
//
// Shared card display classes (.fan-card, .fan-card-corner, .fan-card-face, .fan-nav)
// extracted from _game-kit.scss; sig-select overlay extracted from _room.scss.
// ── Shared stage-card polarity rules ─────────────────────────────────────────
//
// Used by .sea-stage-card (Sea Select polarity is fixed by the deck-stack the
// gamer drew from) and .fan-card[data-polarity="..."] (Game Kit fan, FLIP-able).
// Sets title/qualifier color uniformly across both upright and reversal slots;
// optionally inverts the card frame (bg ⇄ border) for levity polarity, and
// applies an optional text-shadow (Sea uses one for the deeper card-art look;
// Fan does not).
@mixin stage-card-polarity($titles-color, $text-shadow: null, $invert-frame: false) {
@if $invert-frame {
background: rgba(var(--secUser), 1);
border-color: rgba(var(--priUser), 1);
}
.fan-card-name,
.sig-qualifier-above,
.sig-qualifier-below,
.fan-card-reversal-name,
.fan-card-reversal-qualifier {
color: $titles-color;
@if $text-shadow { text-shadow: $text-shadow; }
}
}
// ── Shared stat-block contents ───────────────────────────────────────────────
//
// Used by .sig-stat-block (Sig Select), .sea-stat-block (Sea Select), and
// .fan-stage-block (Game Kit fan). The mixin emits the *inner* rules — stat-face
// padding/swap, stat-keywords, sig-info tooltip + header/title/type/effect/index.
// Each call site keeps its own outer rule (background, border, animation, button
// positioning + class hooks, visibility triggers).
@mixin stat-block-shared {
// SPIN / FYI buttons — pinned to the top-right edge
.spin-btn { position: absolute; top: -1rem; right: -1rem; margin: 0; z-index: 50; }
.fyi-btn { position: absolute; top: 1.25rem; right: -1rem; margin: 0; z-index: 50; }
// PRV / NXT — pinned to bottom corners; hidden by default. Sig + fan reveal
// them on .fyi-open (see each site's outer rule). Sea overrides to always
// visible (its FYI panel is permanent, not hover-toggled).
.fyi-prev, .fyi-next {
display: none;
position: absolute;
bottom: -1rem;
margin: 0;
z-index: 70;
}
.fyi-prev { left: -1rem; }
.fyi-next { right: -1rem; }
.stat-face {
display: none;
padding: calc(var(--sig-card-w, 120px) * 0.37)
calc(var(--sig-card-w, 120px) * 0.1)
calc(var(--sig-card-w, 120px) * 0.08);
}
.stat-face--upright { display: block; }
&.is-reversed {
.stat-face--upright { display: none; }
.stat-face--reversed { display: block; }
}
.stat-face-label {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
text-transform: uppercase;
letter-spacing: 0.09em;
opacity: 0.7;
color: rgba(var(--terUser), 1);
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
}
.stat-keywords {
list-style: none;
padding: 0;
margin: 0;
li {
font-size: calc(var(--sig-card-w, 120px) * 0.083);
padding: calc(var(--sig-card-w, 120px) * 0.042) 0;
opacity: 1;
border-bottom: 0.05rem solid rgba(var(--terUser), 0.18);
&:last-child { border-bottom: none; }
}
}
// FYI tooltip — covers the entire stat block when open
.sig-info {
display: none;
position: absolute;
inset: 0;
z-index: 60;
background-color: rgba(var(--tooltip-bg), 0.6);
backdrop-filter: blur(6px);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--priYl), 0.35);
padding: 0.75rem;
flex-direction: column;
gap: 0.4rem;
overflow-y: auto;
}
.sig-info-header { display: flex; flex-direction: column; gap: 0.1rem; }
.sig-info-title {
font-size: calc(var(--sig-card-w, 120px) * 0.093);
font-weight: 700;
margin: 0;
&--energies, &--operations { color: rgba(var(--quaUser), 1); }
}
.sig-info-type {
font-size: calc(var(--sig-card-w, 120px) * 0.058);
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.sig-info-effect {
flex: 1;
font-size: calc(var(--sig-card-w, 120px) * 0.075);
margin: 0;
line-height: 1.55;
.card-ref { color: rgba(var(--terUser), 1); font-weight: 600; }
}
.sig-info-index {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
opacity: 0.55;
}
}
// ── Tarot fan modal ──────────────────────────────────────────────────────────
#id_tarot_fan_dialog {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
max-width: none;
max-height: none;
margin: 0;
padding: 0;
border: none;
background: rgba(0, 0, 0, 0.88);
overflow: hidden;
&::backdrop { display: none; } // Dialog IS the backdrop
}
.tarot-fan-wrap {
// Fan card dimensions + carousel layout — overrideable per breakpoint via
// @media rules. game-kit.js reads --fan-carousel-step at runtime so the JS
// transform stack stays in sync with the CSS-driven sizing.
--fan-card-w: 220px;
--fan-card-h: 340px;
--fan-stage-shift: 130px;
--fan-carousel-step: 200px;
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
perspective: 900px;
button {
box-shadow: none;
&:hover, &.active {
box-shadow: none;
}
}
}
.tarot-fan {
position: relative;
width: var(--fan-card-w);
height: var(--fan-card-h);
// Shift the whole carousel left so the focused card sits left-of-center,
// making symmetric room on the right for .fan-stage-block.
transform: translateX(calc(-1 * var(--fan-stage-shift)));
transition: transform 0.3s ease;
}
// ── Mobile breakpoints ───────────────────────────────────────────────────────
// Override the four geometry vars on .tarot-fan-wrap; everything downstream
// (.tarot-fan size, .fan-card size, carousel stride read by JS, .fan-stage-block
// width via --sig-card-w, stat-face / stat-keywords typography) scales from them.
// Portrait mobile — true phone widths (≤ 480px). Narrow desktop windows in
// portrait orientation stay on the desktop default.
@media (orientation: portrait) and (max-width: 480px) {
.tarot-fan-wrap {
--fan-card-w: 150px;
--fan-card-h: 230px;
--fan-stage-shift: 90px;
--fan-carousel-step: 130px;
}
}
// Landscape mobile — short viewport, fan needs to stay above the fold
@media (orientation: landscape) and (max-height: 500px) {
.tarot-fan-wrap {
--fan-card-w: 150px;
--fan-card-h: 235px;
--fan-stage-shift: 90px;
--fan-carousel-step: 130px;
}
}
// ── Fan stage block — symmetric right-of-focused-card stat panel ───────────
//
// Visual styling mirrors .sig-stat-block / .sea-stat-block (see Step-6 DRY note).
// Uses --sig-card-w: 220px so the cascaded font-size / padding calc() rules in
// the sea-stat-block block (which key off --sig-card-w) produce fan-card-sized
// typography. Animation/positioning is fan-specific (carousel symmetric slot,
// careen-out on nav, idle-reveal).
.fan-stage-block {
--sig-card-w: var(--fan-card-w);
position: absolute;
top: 50%;
left: 50%;
width: var(--sig-card-w);
height: calc(var(--sig-card-w) * 8 / 5);
background: rgba(var(--priUser), 1);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--terUser), 0.15);
color: rgba(var(--secUser), 1);
z-index: 15;
// Symmetric counterpart to the carousel shift: stat block lives at +stage-shift
// from screen-center, mirroring the focused card at -stage-shift.
transform: translate(calc(-50% + var(--fan-stage-shift)), -50%) translateX(120vw);
opacity: 0;
pointer-events: none;
// Careen-out (default → no .is-revealed): swift exit, fade trails the slide
transition: transform 0.2s cubic-bezier(.5,0,.75,0),
opacity 0.2s ease 0.1s;
&.is-revealed {
transform: translate(calc(-50% + var(--fan-stage-shift)), -50%) translateX(0);
opacity: 1;
pointer-events: auto;
// Reveal: gentler ease-out, opacity leads slightly
transition: transform 0.6s ease-out,
opacity 0.5s ease-out 0.1s;
}
@include stat-block-shared;
&.fyi-open {
.sig-info { display: flex; }
.fyi-prev,
.fyi-next { display: inline-flex; }
}
}
.fan-card {
position: absolute;
inset: 0;
width: var(--fan-card-w);
height: var(--fan-card-h);
border-radius: 0.75rem;
background: rgba(var(--priUser), 1);
border: 0.1rem solid rgba(var(--secUser), 0.4);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.25s ease, opacity 0.25s ease;
transform-style: preserve-3d;
&--active {
border-color: rgba(var(--secUser), 1);
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
}
// SPIN — whole card rotates (carousel transform + rotate(180deg) is combined
// in JS via updateFan/spinBtn handler, since the inline transform owns the
// carousel layout). Inner spans swap opacity so the upright fades and the
// reversal pops — matches sig/sea pattern.
&.stage-card--reversed {
.fan-card-reversal-qualifier,
.fan-card-reversal-name { opacity: 1; }
.fan-card-name,
.sig-qualifier-above,
.sig-qualifier-below { opacity: 0.25; }
}
// FLIP — polarity-aware coloring. Default (no data-polarity) is gravity:
// priUser bg, secUser border. Levity inverts to secUser bg + priUser border.
&[data-polarity="levity"] {
@include stage-card-polarity(
$titles-color: rgba(var(--quiUser), 1),
$invert-frame: true,
);
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
.fan-card-arcana,
.fan-card-name-group { color: rgba(var(--priUser), 0.85); }
}
// FLIP — animation runs via Element.animate() in JS (game-kit.js _flipActive)
// so the rotateY layer stacks on top of the carousel inline transform
// (translateX/rotateY/scale + optional SPIN rotate(180deg)).
.fan-card-corner { padding-top: 0.25rem; }
}
.fan-card-corner {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
line-height: 1;
color: rgba(var(--secUser), 0.75);
padding-left: 0.5rem; // outer-edge breathing room; --br rotation makes this right-side
&--tl { top: 0.4rem; left: 0.4rem; }
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
// Corner rank + suit icon scale with the card so they shrink on mobile
// breakpoints alongside .fan-card. 0.109 of card-width ≈ 24px @ 220px (the
// original 1.5rem default).
.fan-corner-rank {
font-size: calc(var(--fan-card-w, 220px) * 0.109);
font-weight: bold;
padding: 0.18rem 0;
}
// Icon always at the outer card edge regardless of rank width
i {
font-size: calc(var(--fan-card-w, 220px) * 0.109);
align-self: flex-start;
}
}
.fan-card-face {
// Padding + gaps scale with card width so they stay proportional on mobile.
padding: calc(var(--fan-card-w) * 0.057);
text-align: center;
display: flex;
flex-direction: column;
gap: calc(var(--fan-card-w) * 0.023);
// Face flips on SPIN; corners stay put because they live outside .fan-card-face
transition: transform 0.4s ease;
.fan-card-face-upright,
.fan-card-face-reversal {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: calc(var(--fan-card-w) * 0.007);
// Ghost-line: reserve at least two title-line-heights of vertical space
// on each face so emanation + reversal stay symmetric even when one
// side has a single-line title (e.g. trumps 69 reversal "Indulged
// Folly" vs upright "Losing Self-Importance, / Sublimating").
min-height: calc(var(--fan-card-w) * 0.21);
}
// Qualifier shares the name's typography — same line, different content.
// Sizes scale with --fan-card-w so they stay proportional on mobile.
// `text-wrap: balance` distributes lines evenly so a borderline-long title
// breaks at the natural midpoint instead of greedy first-fit (e.g. trump
// 9 wraps as "Erasing / Personal History," instead of "Erasing Personal /
// History,"). Base size lowered from 0.1 → 0.087 (~13%) so all the long
// titles (trumps 8/9/18/36/41 + Queen of Crowns) fit without per-card
// hacks and without asymmetry between upright (h3) and reversal (p).
.sig-qualifier-above,
.sig-qualifier-below,
.fan-card-reversal-qualifier,
.fan-card-reversal-name,
.fan-card-name {
font-size: calc(var(--fan-card-w) * 0.087);
font-weight: bold;
margin: 0;
color: rgba(var(--terUser), 1);
transition: opacity 0.2s;
text-wrap: balance;
}
// Reversal-face spans pre-rotated so they read forward once the card spins
// 180deg via .stage-card--reversed. Matches sig/sea convention (rotation +
// base opacity live on the inner spans, NOT the wrapping .fan-card-face-reversal
// div — otherwise the outer div's transform stacks with sig/sea's scoped rule
// and double-rotates back to upright).
.fan-card-reversal-qualifier,
.fan-card-reversal-name {
transform: rotate(180deg);
opacity: 0.25;
}
.fan-card-number { font-size: calc(var(--fan-card-w) * 0.043); }
.fan-card-name-group { font-size: calc(var(--fan-card-w) * 0.043); margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
.fan-card-arcana { font-size: calc(var(--fan-card-w) * 0.043); text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
.fan-card-correspondence { font-size: calc(var(--fan-card-w) * 0.04); font-style: italic; color: rgba(var(--secUser), 0.5); }
}
// FLIP button — invisible at rest, fades in when the user hovers/taps the wrap.
// Positioned at bottom-left of the focused card slot (carousel-shifted, so its
// translateX matches .tarot-fan's leftward shift).
.fan-flip-btn {
position: absolute;
z-index: 25;
top: 50%;
left: 50%;
transform: translate(calc(-50% - var(--fan-stage-shift) - var(--fan-card-w) / 2 + 1.5rem),
calc(-50% + var(--fan-card-h) / 2 - 1.5rem));
margin: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
// Reveal when the focused card OR the FLIP button itself is hovered. Without
// the `.fan-flip-btn:hover` clause the button (z-index 25, sitting on top of
// the card) steals :hover from the card the moment the cursor moves onto it,
// flipping :has() false, fading the button to opacity:0 + pointer-events:none,
// and letting the in-flight click pass through to the dialog backdrop (which
// closes the modal). Keeping the button in the trigger list pins it visible
// while the cursor is on it.
.tarot-fan-wrap:has(.fan-card--active:hover) .fan-flip-btn,
.tarot-fan-wrap:has(.fan-flip-btn:hover) .fan-flip-btn,
.tarot-fan-wrap.fan-touch-revealed .fan-flip-btn {
opacity: 1;
pointer-events: auto;
}
.tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn {
opacity: 0;
pointer-events: none;
}
.fan-nav {
position: absolute;
z-index: 20;
font-size: 3rem;
line-height: 1;
background: none;
border: none;
text-shadow: 0 0 1px rgba(0, 0, 0, 1);
color: rgba(var(--terUser), 0.6);
cursor: pointer;
padding: 1rem;
transition: color 0.15s;
pointer-events: auto;
outline: none;
box-shadow: none;
&:hover { color: rgba(var(--ninUser), 1); }
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
&--prev { left: 1rem; }
&--next { right: 1rem; }
}
// ─── Sig Select overlay (SIG_SELECT phase) ────────────────────────────────────
//
// Two overlays (levity / gravity) run in parallel, one per polarity group.
// Layout mirrors the gatekeeper: dark Gaussian backdrop + centred modal.
// Inside the modal: upper stage (card preview) + lower mini card grid (no scroll).
html:has(.sig-backdrop) {
overflow: hidden;
}
.sig-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(5px);
z-index: 100;
pointer-events: none;
}
.sig-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: stretch;
justify-content: center;
z-index: 120;
pointer-events: none;
}
.sig-modal {
pointer-events: auto;
display: flex;
flex-direction: column;
width: 100%; // respects overlay padding-right set by JS
max-width: 420px;
max-height: 100%; // respects overlay padding-bottom set by JS
}
// ─── Stage ────────────────────────────────────────────────────────────────────
// flex: 1 — fills all space above the card grid; no background (backdrop blur).
// Row layout: preview card bottom-left, stat block fills the right.
// Card width is set by sizeSigCard() in room.js (smaller of 40% stage width or
// 80% stage height × 5/8) via --sig-card-w CSS variable — libsass can't handle
// container query units inside min().
.sig-stage {
flex: 1;
min-height: 0;
position: relative;
display: flex;
flex-direction: row;
align-items: flex-end;
padding-left: 1.5rem;
gap: 0.75rem;
// Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height.
.sig-stage-card {
flex-shrink: 0;
width: var(--sig-card-w, 120px);
height: auto;
aspect-ratio: 5 / 8;
border-radius: 0.5rem;
background: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 0.6);
display: flex;
flex-direction: column;
position: relative;
padding: 0.25rem;
overflow: hidden;
transition: transform 0.4s ease;
// game-kit sets .fan-card-corner { position: absolute; top/left offsets }
// so these just need display/font overrides; the corners land at the card edges.
// All font-sizes scale with --sig-card-w (ratio = original-rem × 16 / 120).
.fan-card-corner--tl {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.133); font-weight: 700; }
i { font-size: calc(var(--sig-card-w, 120px) * 0.1); }
}
.fan-card-corner--br {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.12); font-weight: 700; }
i { font-size: calc(var(--sig-card-w, 120px) * 0.1); }
}
.fan-card-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.25rem 0.15rem;
gap: 0.2rem;
.fan-card-face-upright { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
.fan-card-face-reversal { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding-top: 0.1rem; }
.fan-card-name-group { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.6; }
// Upright qualifier + name share sizing/weight/color with their reversed counterparts.
// text-wrap: balance distributes lines evenly so longer titles wrap symmetrically;
// base size 0.08 (was 0.093) gives long titles room to fit without per-card hacks.
.sig-qualifier-above,
.sig-qualifier-below,
.fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 120px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; }
.fan-card-name,
.fan-card-reversal-name { font-size: calc(var(--sig-card-w, 120px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; }
.fan-card-arcana { font-size: calc(var(--sig-card-w, 120px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
.fan-card-correspondence{ display: none; } // Minchiate equivalence shown in game-kit only
// Reversed face elements — pre-rotated so they read forward after card spins
.fan-card-reversal-qualifier,
.fan-card-reversal-name {
transform: rotate(180deg);
opacity: 0.25;
}
}
&.stage-card--reversed {
transform: rotate(180deg);
.fan-card-reversal-qualifier,
.fan-card-reversal-name { opacity: 1; }
.fan-card-name,
.sig-qualifier-above,
.sig-qualifier-below { opacity: 0.25; }
}
}
// Stat block — same dimensions as the preview card (width × 5:8 aspect).
// flex: 0 0 auto so it doesn't stretch to fill the stage; the rest of the
// stage row is simply empty, giving the card room to breathe.
.sig-stat-block {
flex: 0 0 auto;
width: var(--sig-card-w, 120px);
height: calc(var(--sig-card-w, 120px) * 8 / 5);
align-self: flex-end;
background: rgba(var(--priUser), 0.5);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--terUser), 0.15);
display: none;
position: relative;
@include stat-block-shared;
}
&.sig-stage--frozen .sig-stat-block { display: block; }
// Unified .fyi-open class — opens the FYI panel + reveals PRV/NXT nav.
.sig-stat-block.fyi-open {
.sig-info { display: flex; }
.fyi-prev, .fyi-next { display: inline-flex; }
}
}
// ─── My Sign picker — sizing + state-gated reveal ────────────────────────────
// Two-phase layout: landing (DRY 1-chair hex w. SCAN SIGN center) → picker
// (sig-card grid below an always-present stage frame). SAVE SIGN rides
// inside .my-sign-stage to its right, sig-select-style. FLIP btn + stat
// block hidden at rest; revealed by `.sig-stage--frozen` (added by JS on
// OK confirm, cleared by NVM). SPIN (orientation 180°) stays in
// `.sig-stat-block`; FLIP toggles polarity (data-polarity on .my-sign-page).
// .my-sign-page mirrors .room-page's flex-column-fill-aperture pattern so
// the DRY hex inside .my-sign-landing gets a non-zero #id_game_table size
// for room.js's scaleTable() to compute against. Without flex:1 + min-height:0
// the container chain collapses + the hex renders unscaled (200×231 inside
// a 360×320 scene, looking elongated/portrait).
.my-sign-page {
--sig-card-w: clamp(140px, 36vw, 220px);
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
position: relative;
}
// Stage frame — fixed slice in picker phase, natural-sized on landing.
// The picker min-height reserves real estate so hover-preview cards don't
// shift adjacent layout; on landing the stage shrinks to its actual content
// (empty or saved-sig preview) so the DRY hex below gets fair vertical
// space. SAVE SIGN form is absolutely positioned (see below) so it stays
// pinned when the stat block reveals on OK confirm.
.my-sign-stage {
flex: 0 0 auto;
position: relative;
}
.my-sign-page[data-phase="picker"] .my-sign-stage {
min-height: calc(var(--sig-card-w, 140px) * 8 / 5 + 1.5rem);
}
// SAVE SIGN form — pinned to the bottom-right of the stage so it stays in
// place across hover/lock states (the stat block reveal would otherwise
// shove a flex-positioned btn around the stage row).
#id_save_sign_form {
position: absolute;
bottom: 0.75rem;
right: 1rem;
margin: 0;
z-index: 6;
}
// Landing phase — DRY hex container. flex:1 + min-height:0 propagates the
// available vertical space into .room-shell → #id_game_table → scaleTable().
.my-sign-landing {
flex: 1;
min-height: 0;
display: flex;
position: relative;
// SCAN SIGN btn — centered in the hex. Default .btn-primary text
// (0.875rem) scales tighter than the room's PICK SIGS btn font; this
// bumps it down a notch so the 2-line "SCAN/SIGN" label sits cleanly
// inside the 4rem circle without crowding the border.
#id_scan_sign_btn {
font-size: 0.75rem;
line-height: 1.1;
white-space: normal;
}
// DEL btn — destructive secondary action, only rendered when a sig
// is saved. Anchored bottom-right of the landing area so it doesn't
// compete w. the centered SCAN SIGN hex for visual weight. .btn-danger
// for the destructive treatment (mirrors post.html gear menu DEL).
.my-sign-clear-form {
position: absolute;
bottom: 0.75rem;
right: 1rem;
margin: 0;
}
}
// Hide SAVE SIGN on landing — the form only makes sense once the user
// has entered the picker. Saved-sig preview on landing is read-only.
.my-sign-page[data-phase="landing"] #id_save_sign_form {
display: none;
}
// Picker phase — bg matches the table hex's interior (--duoUser) so the
// transition from "hex face" → "card pile on felt" reads as a continuous
// surface rather than a context swap. Landing phase keeps the body bg.
.my-sign-page[data-phase="picker"] {
background: rgba(var(--duoUser), 1);
}
.my-sign-flip-btn {
position: absolute;
z-index: 25;
bottom: 0.4rem;
// .sig-stage has padding-left: 1.5rem; this offset places the btn just
// inside the stage card's bottom-left corner (the card sits flex-end /
// flex-start, anchored to the stage's left padding).
left: calc(1.5rem + 0.4rem);
margin: 0;
display: none;
}
// FLIP btn appears only when the stage is frozen (post-OK confirm). Hover-only
// previews don't reveal the polarity toggle — the user hasn't committed yet.
.my-sign-stage.sig-stage--frozen .my-sign-flip-btn {
display: inline-flex;
}
// ─── Mini card grid ───────────────────────────────────────────────────────────
// flex: 0 0 auto — shrinks to card content; no background (backdrop blur).
// align-content: start prevents CSS grid from distributing extra height between rows.
.sig-deck-grid {
flex: 0 0 auto;
display: grid;
grid-template-columns: repeat(6, 1fr);
align-content: start;
gap: 2px;
padding: 4px;
overflow: hidden;
margin: 0 1rem 5rem 4rem;
}
.sig-card {
aspect-ratio: 5 / 8;
border-radius: 0.4rem;
background: rgba(var(--priUser), 0.97);
border: 1px solid rgba(var(--secUser), 0.3);
position: relative;
cursor: grab;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem;
// padding-left:0.5rem }. Sig thumbnails reset position to dead-center; we
// also zero the inherited padding-left (it's there for game-kit fan corners
// that need outer-edge breathing room — at thumb size it nudges the rank +
// icon visibly off-center after the translate).
.fan-card-corner--tl {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding-left: 0;
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
// OK / NVM overlay — appears on click (focused) or own reservation
.sig-card-actions {
position: absolute;
inset: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
background: rgba(var(--priUser), 0.92);
border-radius: inherit;
.sig-nvm-btn { display: none; }
}
&.sig-focused .sig-card-actions { display: flex; }
&.sig-reserved--own .sig-card-actions {
display: flex;
.sig-ok-btn { display: none; }
.sig-nvm-btn { display: flex; }
}
// Cursor strip — hangs below the card bottom edge; overflow: visible allows this.
.sig-card-cursors {
position: absolute;
bottom: -0.6rem;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 0 2px;
}
// Rise above DOM-order siblings when a peer's cursor is active on this card.
// Without this, later cards in the grid paint over the overflowing cursor icons.
&:has(.sig-cursor.active) { z-index: 5; }
&:hover:not([data-reserved-by]) {
border-color: rgba(var(--secUser), 0.8);
box-shadow: 0 0 4px rgba(var(--secUser), 0.25);
}
&.sig-reserved {
cursor: not-allowed;
}
// Role-coloured reservation glow — border/shadow matches the reserving gamer's role.
// data-reserved-by is set by applyReservation() in sig-select.js.
// Own reservation also shows role colour (same as peers see), not a separate style.
&.sig-reserved {
&[data-reserved-by="PC"] { border-color: rgba(var(--priRd), 1); box-shadow: 0 0 0 2px rgba(var(--priRd), 1); }
&[data-reserved-by="NC"] { border-color: rgba(var(--priYl), 1); box-shadow: 0 0 0 2px rgba(var(--priYl), 1); }
&[data-reserved-by="EC"] { border-color: rgba(var(--priGn), 1); box-shadow: 0 0 0 2px rgba(var(--priGn), 1); }
&[data-reserved-by="SC"] { border-color: rgba(var(--priCy), 1); box-shadow: 0 0 0 2px rgba(var(--priCy), 1); }
&[data-reserved-by="AC"] { border-color: rgba(var(--priId), 1); box-shadow: 0 0 0 2px rgba(var(--priId), 1); }
&[data-reserved-by="BC"] { border-color: rgba(var(--priFs), 1); box-shadow: 0 0 0 2px rgba(var(--priFs), 1); }
}
&.sig-reserved--own {
cursor: grabbing;
}
}
// ─── Cursor anchors ───────────────────────────────────────────────────────────
//
// Three tiny dots along the bottom of each mini card, one per role in the group.
// Inactive: invisible. Active (another gamer is hovering): role-coloured dot.
// Position order is fixed per polarity (POLARITY_ROLES in sig-select.js):
// levity (PC / NC / SC) → left / mid / right
// gravity (BC / EC / AC) → left / mid / right
// In-card cursor elements — invisible anchors only.
// Visible icons are portaled to document root by applyHover() in sig-select.js.
.sig-cursor {
display: block;
font-size: 0; // zero-size: no layout impact, just carries .active class
color: transparent;
pointer-events: none;
}
// ─── Floating cursor portal ───────────────────────────────────────────────────
//
// sig-select.js creates these <i> elements inside #id_sig_cursor_portal, a
// position:fixed root-level container, so they escape all overflow/clip contexts.
// Positioned via getBoundingClientRect() on the card element.
#id_sig_cursor_portal {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 200; // above sig-overlay (120), below tray (310)
overflow: visible;
}
.sig-cursor-float {
position: absolute;
font-size: 1.5rem;
line-height: 1;
transform: translateX(-50%); // centre on the x coordinate from JS
pointer-events: none;
}
// Role-specific colour + outline shadow + ninUser glow
.sig-cursor-float[data-role="PC"] {
color: rgba(var(--priRd), 1);
text-shadow: 2px 0 0 rgba(var(--priOr),1), -2px 0 0 rgba(var(--priOr),1),
0 2px 0 rgba(var(--priOr),1), 0 -2px 0 rgba(var(--priOr),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="NC"] {
color: rgba(var(--priYl), 1);
text-shadow: 2px 0 0 rgba(var(--priLm),1), -2px 0 0 rgba(var(--priLm),1),
0 2px 0 rgba(var(--priLm),1), 0 -2px 0 rgba(var(--priLm),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="EC"] {
color: rgba(var(--priGn), 1);
text-shadow: 2px 0 0 rgba(var(--priTk),1), -2px 0 0 rgba(var(--priTk),1),
0 2px 0 rgba(var(--priTk),1), 0 -2px 0 rgba(var(--priTk),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="SC"] {
color: rgba(var(--priCy), 1);
text-shadow: 2px 0 0 rgba(var(--priBl),1), -2px 0 0 rgba(var(--priBl),1),
0 2px 0 rgba(var(--priBl),1), 0 -2px 0 rgba(var(--priBl),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="AC"] {
color: rgba(var(--priId), 1);
text-shadow: 2px 0 0 rgba(var(--priVt),1), -2px 0 0 rgba(var(--priVt),1),
0 2px 0 rgba(var(--priVt),1), 0 -2px 0 rgba(var(--priVt),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="BC"] {
color: rgba(var(--priFs), 1);
text-shadow: 2px 0 0 rgba(var(--priMe),1), -2px 0 0 rgba(var(--priMe),1),
0 2px 0 rgba(var(--priMe),1), 0 -2px 0 rgba(var(--priMe),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
// ─── Polarity theming — card colour inversion ────────────────────────────────
//
// Gravity (Graven): --priUser bg / --secUser text — standard dark palette.
// Levity (Leavened): --secUser bg / --priUser text — inverted, lighter feel.
// Both mini-cards and the stage preview card follow the same rule.
// `.my-sign-page[data-polarity]` parallels `.sig-overlay[data-polarity]` —
// same polarity-themed colour rules apply to the standalone Game Sign picker.
// data-polarity lives on the page wrapper (not on .my-sign-stage) so descendant
// `.sig-card` (in the grid, sibling to the stage) inherits the rules.
.sig-overlay[data-polarity="levity"],
.my-sign-page[data-polarity="levity"] {
// Mini card: inverted palette. game-kit sets explicit colours on .fan-card-name
// and .fan-card-corner that out-specifc the parent color, so re-target them here.
.sig-card {
background: rgba(var(--secUser), 0.97);
border-color: rgba(var(--priUser), 0.3);
color: rgba(var(--priUser), 1);
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
.fan-card-name { color: rgba(var(--quiUser), 1); }
// OK / NVM overlay — must match the inverted card background
.sig-card-actions { background: rgba(var(--secUser), 0.92); }
}
// Stage preview card: same inversion + title colour.
// .fan-card-name-group and .fan-card-arcana have explicit color in the base
// .fan-card-face rule (specificity 0,2,0) — must re-target them here (0,3,0).
// Opacity dim is still applied by the nested sig-stage-card rule.
.sig-stage-card {
background: rgba(var(--secUser), 1);
border-color: rgba(var(--priUser), 0.6);
color: rgba(var(--priUser), 1);
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
.fan-card-name-group{ color: rgba(var(--priUser), 1); }
.fan-card-name { color: rgba(var(--quiUser), 1); }
.fan-card-arcana { color: rgba(var(--priUser), 1); }
}
// Polarity title + qualifier text: --quiUser for levity (paired w. gravity's --terUser).
// All five selectors prefixed w. .sig-stage-card to match (or beat) the 0,4,0 specificity
// of the default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule —
// without the prefix the polarity color loses the cascade on .sig-qualifier-*.
.sig-stage-card .fan-card-name,
.sig-stage-card .fan-card-reversal-name,
.sig-stage-card .fan-card-reversal-qualifier,
.sig-stage-card .sig-qualifier-above,
.sig-stage-card .sig-qualifier-below { color: rgba(var(--quiUser), 1); }
// Stat-face label: levity stat-block bg is --priUser (opposite of levity card's
// --secUser bg), so the label takes the gravity-card text color (--terUser) to
// stay legible against the dark stat-block.
.sig-stat-block .stat-face-label { color: rgba(var(--terUser), 1); }
// Upright + reversal title glow — levity. Drop-shadow is WHITE here (was 0,0,0
// at 0.55) because the inverted-frame levity card uses a light --secUser bg,
// so a dark drop shadow reads as harsh smudge under the --quiUser title text.
.sig-stage-card .fan-card-name,
.sig-stage-card .sig-qualifier-above,
.sig-stage-card .sig-qualifier-below,
.sig-stage-card .fan-card-reversal-name,
.sig-stage-card .fan-card-reversal-qualifier {
text-shadow: 0 1px 1px rgba(255,255,255,0.55), 0 0 0.55rem rgba(var(--ninUser), 0.7);
}
// card-ref spans inside the caution tooltip — must match the base rule's
// .sig-stat-block .sig-info-effect .card-ref specificity (0,3,0) to win.
.sig-info-effect .card-ref { color: rgba(var(--quiUser), 1); }
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
}
.sig-overlay[data-polarity="gravity"],
.my-sign-page[data-polarity="gravity"] {
// Stat block: invert priUser/secUser so gravity gets the same stark contrast as leavened cards
.sig-stat-block {
background: rgba(var(--secUser), 0.75);
color: rgba(var(--priUser), 1);
border-color: rgba(var(--priUser), 0.15);
}
// Caution tooltip: --tooltip-bg is black so priUser text (dark) would be invisible —
// override to secUser (light) so body text reads against the dark backdrop.
.sig-info { color: rgba(var(--secUser), 1); }
// Polarity title + qualifier text: --terUser for gravity (paired w. levity's --quiUser).
// All five selectors prefixed w. .sig-stage-card to meet the 0,4,0 specificity of the
// default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule.
.sig-stage-card .fan-card-name,
.sig-stage-card .fan-card-reversal-name,
.sig-stage-card .fan-card-reversal-qualifier,
.sig-stage-card .sig-qualifier-above,
.sig-stage-card .sig-qualifier-below { color: rgba(var(--terUser), 1); }
// Stat-face label: gravity stat-block bg is --secUser (opposite of gravity card's
// --priUser bg), so the label takes the levity-card text color (--quiUser) to
// stay legible against the lighter stat-block.
.sig-stat-block .stat-face-label { color: rgba(var(--quiUser), 1); }
// Upright + reversal title glow — gravity
.sig-stage-card .fan-card-name,
.sig-stage-card .sig-qualifier-above,
.sig-stage-card .sig-qualifier-below,
.sig-stage-card .fan-card-reversal-name,
.sig-stage-card .fan-card-reversal-qualifier {
text-shadow: 1px 1px 0 rgba(0,0,0,1), 0 0 0.25rem rgba(var(--ninUser), 0.25);
}
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
}
// ─── Sig select: landscape overrides ─────────────────────────────────────────
// Cascade (each step is a SUPERSET of the prior):
// narrow landscape → 6 cols × 2.5rem, row layout (stage beside grid)
// ≥ 900px → 9×2 grid of 3rem cards, column layout (stage above)
// ≥ 1400px → 18×1 row of 3rem cards (wide enough that 18×3rem
// + ~7rem modal margins clears even at rem=22)
// ≥ 1800px → 18×1 row of 5rem cards + doubled sidebar padding
// Grid margins reset to 0 — overlay padding handles all edge clearance.
@media (orientation: landscape) {
.sig-modal {
max-width: none;
flex-direction: row; // grid to the right, stage + card preview to the left
margin-left: 4rem;
margin-right: 3rem;
}
.sig-overlay .sig-stage {
min-width: 0; // allow shrinking in row layout; align-items:flex-end already set
}
// Scoped to .sig-overlay — the room sig-select modal has its own width
// budget. .my-sign-page gets its own breakpoints below (different col
// counts + thresholds tuned for the full content area).
.sig-overlay .sig-deck-grid {
grid-template-columns: repeat(6, 2.5rem);
margin: 0;
align-self: flex-end; // sit at the bottom of the modal row
}
}
@media (orientation: landscape) and (min-width: 900px) {
// Middling landscape: stacked layout (stage top, 9×2 grid bottom).
.sig-modal {
flex-direction: column;
align-items: stretch;
}
.sig-overlay .sig-stage {
min-width: auto;
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
margin-left: 3rem;
}
.sig-overlay .sig-deck-grid {
grid-template-columns: repeat(9, 3rem);
align-self: center;
}
}
@media (orientation: landscape) and (min-width: 1400px) {
// Wide landscape: 18-card single-row grid. 18×3rem + ~7rem modal margins
// clears the viewport here even at the fluid-rem ceiling (rem=22 → ~1376px).
.sig-overlay .sig-deck-grid {
grid-template-columns: repeat(18, 3rem);
}
}
@media (orientation: landscape) and (min-width: 1800px) {
// Sig overlay: clear doubled sidebars (8rem each instead of 4rem/6rem)
.sig-overlay { padding-left: 8rem; padding-right: 8rem; }
.sig-overlay .sig-stage {
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
margin-left: 3rem;
}
.sig-overlay .sig-deck-grid {
grid-template-columns: repeat(18, 5rem);
align-self: center;
}
// Room menu: base right: 0.5rem (same-specificity ID rule) overrides _applets.scss
// XL block because _card-deck.scss is imported after _applets.scss. Re-declare here to win the cascade.
#id_room_menu { right: 2.5rem; }
}
// ─── My Sign picker grid ────────────────────────────────────────────────────
// align-self:center horizontally centers the shrink-to-content grid in
// .my-sign-page's flex column; overflow:visible avoids the modal's hidden
// clip. Col counts ramp up at wider viewports — sig-select's breakpoints
// are tuned for the modal's width budget so we use our own thresholds that
// account for the navbar/footer sidebars (~5rem each) eating viewport width.
// Portrait: fixed rem cols (default repeat(6, 1fr) collapses to 0 width
// w. align-self:center because 1fr has no defined parent to fr against).
.my-sign-deck-grid {
align-self: center;
margin: 1rem auto;
overflow: visible;
grid-template-columns: repeat(6, 3rem);
}
@media (orientation: landscape) and (min-width: 900px) {
// Middling landscape: 9-card row × 2 (mirrors sig-select's middling step).
.my-sign-deck-grid {
grid-template-columns: repeat(9, 3rem);
}
}
@media (orientation: landscape) and (min-width: 1600px) {
// Wide landscape: 18-card single row. Bumped from sig-select's 1400px so
// 18×3rem + the doubled-sidebar margins (~10rem) still clears the viewport
// at the fluid-rem ceiling (rem=22 → 18×3rem=1188px + 220px margins = 1408,
// safe with 1600px floor).
.my-sign-deck-grid {
grid-template-columns: repeat(18, 3rem);
}
}
@media (orientation: landscape) and (min-width: 2200px) {
// XL landscape: 18×5rem. Bumped from sig-select's 1800px — 18×5rem=1980px
// at rem=22 needs ~2200px viewport after sidebar/footer clearance.
.my-sign-deck-grid {
grid-template-columns: repeat(18, 5rem);
}
}
// ── DRAW SEA overlay ─────────────────────────────────────────────────────────
// Mirrors .sky-* structure but with columns reversed:
// left = transparent (Celtic Cross card positions)
// right = rgba(--priUser) opaque (spread select)
.sea-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
z-index: 200;
}
html.sea-open .sea-backdrop { display: block; }
.sea-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 201;
overflow-y: auto;
align-items: center;
justify-content: center;
}
html.sea-open .sea-overlay { display: flex; }
.sea-modal-wrap {
position: relative;
width: 90vw;
max-width: 60rem;
max-height: 90vh;
margin: auto;
opacity: 0;
transform: translateY(1.5rem);
transition: opacity 0.25s, transform 0.25s;
}
html.sea-open .sea-modal-wrap {
opacity: 1;
transform: translateY(0);
}
.sea-modal {
border-radius: 0.5rem;
overflow: hidden;
width: 100%;
}
.sea-modal-header {
padding: 0.75rem 1.25rem;
background: rgba(var(--priUser), 1);
h2 { font-size: 1.4rem; margin: 0; }
p { margin: 0.2rem 0 0; font-size: 0.85rem; opacity: 0.8; }
}
.sea-modal-body {
display: flex;
min-height: 20rem;
}
// ── Cards column (transparent / left) ────────────────────────────────────────
.sea-cards-col {
flex: 1 1 55%;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.sea-cross {
display: grid;
grid-template-areas:
". crown . "
"leave core loom "
". lay . ";
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto auto auto;
gap: 0.5rem;
align-items: center;
justify-items: center;
}
.sea-crucifix-cell { display: flex; align-items: center; justify-content: center; }
.sea-pos-crown { grid-area: crown; }
.sea-pos-leave { grid-area: leave; }
.sea-pos-core { grid-area: core; }
.sea-pos-loom { grid-area: loom; }
.sea-pos-lay { grid-area: lay; }
$sea-card-w: 4rem;
$sea-card-h: 6.5rem;
.sea-card-slot {
width: $sea-card-w;
height: $sea-card-h;
background-color: rgba(var(--duoUser), 1);
border: 0.15rem dashed rgba(var(--terUser), 1);
box-shadow: 0 0 2px rgba(var(--priUser), 0.5);
border-radius: 0.3rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
color: rgba(var(--terUser), 0.6);
}
.sea-card-slot--crossing {
// Keep portrait dimensions; rotate(90deg) in .sea-pos-cross supplies the landscape visual.
// Swapping w/h here caused flex-shrink to squish the longer edge to fit the 4rem container.
width: $sea-card-w;
height: $sea-card-h;
}
.sea-card-slot--filled {
// Start invisible; transition to .sea-card-slot--visible on deposit
opacity: 0;
transition: opacity 1s ease;
border: 0.15rem solid transparent;
border-radius: 0.3rem;
flex-direction: column;
gap: 0.15rem;
.fan-corner-rank { font-size: 1.15rem; font-weight: 700; line-height: 1; }
i { font-size: 0.9rem; }
}
// Levity drawn card — secUser bg, priUser text + border (matches stage card polarity)
.sea-card-slot--filled.sea-card-slot--levity {
color: rgba(var(--priUser), 0.9);
background: rgba(var(--secUser), 1);
border-color: rgba(var(--priUser), 1);
}
// Gravity drawn card — priUser bg, secUser text + border
.sea-card-slot--filled.sea-card-slot--gravity {
color: rgba(var(--secUser), 0.9);
background: rgba(var(--priUser), 1);
border-color: rgba(var(--secUser), 0.6);
}
// Reversed — pre-rolled by sea_deck server-side. Rotate the whole slot
// (background + border + content) so the rank/icon stacking order also
// flips (rank-top + icon-bottom upright → icon-top + rank-bottom reversed),
// not just each character upside-down in place.
.sea-card-slot--reversed { transform: rotate(180deg); }
// Cross-position adds 90° already; reversed cross combines to 270°. Higher
// specificity than the .sea-pos-cross .sea-card-slot rule so it wins.
.sea-pos-cross .sea-card-slot--reversed { transform: rotate(270deg); }
// Long Roman numerals (≥ 5 chars: XVIII, XXIII, XXVIII, XXXIII, XXXVIII,
// XLIII, XLVIII) — squeeze horizontally via scaleX so they fit the slot
// without dropping font-size (height stays the same). Class added in
// _fillSlot when card.corner_rank.length >= 5. Slot-level reversed rotation
// already carries the rank along, so scaleX is the only inner transform
// regardless of reversal state.
.sea-card-slot--rank-long .fan-corner-rank {
display: inline-block;
transform: scaleX(0.7);
letter-spacing: -0.05em;
}
// Deposited — fully opaque by default; Cover/Cross are semi-transparent
.sea-card-slot--visible { opacity: 1; transition: opacity 1s ease, box-shadow 0.15s ease; }
@keyframes sea-cover-appear {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0.3; }
}
@keyframes sea-cross-appear {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0.15; }
}
.sea-pos-cover .sea-card-slot--visible { opacity: 0.3; animation: sea-cover-appear 2s ease; }
.sea-pos-cross .sea-card-slot--visible { opacity: 0.15; animation: sea-cross-appear 2s ease; }
// Hover: reveal fully (snappy)
.sea-pos-cover .sea-card-slot--visible:hover,
.sea-pos-cross .sea-card-slot--visible:hover { opacity: 1; transition: opacity 0.15s ease; }
// Focused (first tap): persist at opacity 1 + selection glow until modal opens
.sea-card-slot--focused {
opacity: 1 !important;
transition: opacity 0.15s ease, box-shadow 0.15s ease;
box-shadow: 0 0 0.5rem 0.25rem rgba(var(--ninUser), 0.35), 0 0 0.4rem rgba(0, 0, 0, 0.85);
}
// Cover + Cross — absolutely overlaid on the Sig card in .sea-pos-core
.sea-pos-core { position: relative; }
.sea-pos-cover,
.sea-pos-cross {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
.sea-card-slot { pointer-events: auto; }
}
.sea-pos-cover { z-index: 3; } // above sig (z-index: 2)
.sea-pos-cross { z-index: 4; } // above cover
// Empty Cover/Cross slots — subtle dotted outline (no fill) so the
// underlying Sig card shows through. Hovering/touching reveals the
// full --duoUser mask, opaquing the slot + obscuring the Sig behind.
// Border + label dim to 0.25 alpha default; bounce to full on hover.
// The filled-slot hover behavior (opacity 0.3/0.15 → 1) at lines 1300-
// 1301 is untouched — this only restyles the EMPTY state.
.sea-pos-cover .sea-card-slot--empty,
.sea-pos-cross .sea-card-slot--empty {
background-color: transparent;
border-color: rgba(var(--terUser), 0.25);
box-shadow: none;
pointer-events: auto;
transition: background-color 0.15s ease, border-color 0.15s ease;
.sea-pos-label { opacity: 0.25; }
}
.sea-pos-cover .sea-card-slot--empty:hover,
.sea-pos-cross .sea-card-slot--empty:hover {
background-color: rgba(var(--duoUser), 1);
border-color: rgba(var(--terUser), 1);
.sea-pos-label { opacity: 0.6; }
}
.sea-pos-cross .sea-card-slot { transform: rotate(90deg); }
// Sig card in center slot — compact rank + icon display; tilted CCW so Cover slot peeks through
.sea-sig-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.2rem;
transform: rotate(-5deg);
position: relative;
z-index: 2;
.fan-corner-rank {
font-size: 1.2rem;
font-weight: 700;
line-height: 1;
color: rgba(var(--secUser), 0.85);
}
i { font-size: 1rem; color: rgba(var(--secUser), 0.75); }
}
// .sig-stage-card is normally scoped inside .sig-stage — re-apply the card shell
// here so it renders correctly outside that context. Class-based selector so it
// also applies in the tray (.tray-sig-card .sig-stage-card.sea-sig-card).
.sig-stage-card.sea-sig-card {
flex-shrink: 0;
width: var(--sig-card-w, #{$sea-card-w});
height: auto;
aspect-ratio: 5 / 8;
border-radius: 0.5rem;
background: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 0.6);
display: flex;
flex-direction: column;
position: relative;
padding: 0.25rem;
overflow: hidden;
.fan-card-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.25rem 0.15rem;
.fan-card-name,
.sig-qualifier-above,
.sig-qualifier-below { font-size: 0.5rem; font-weight: 600; white-space: normal; word-break: break-word; line-height: 1.3; margin: 0; }
}
}
// ── Form column (priUser / opaque / right) ────────────────────────────────────
.sea-form-col {
flex: 0 0 auto;
width: 16rem;
display: flex;
flex-direction: column;
padding: 1.25rem;
background: rgba(var(--priUser), 1);
}
// Mobile: stack crucifix on top, form (select / stacks / LOCK HAND / DEL) below
@media (max-width: 600px) {
.sea-modal-body { flex-direction: column; }
.sea-cards-col { flex: 0 0 auto; padding: 1.25rem 1rem; }
.sea-form-col { width: 100%; }
}
.sea-form-main {
flex: 1;
overflow-y: auto;
}
.sea-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 1rem;
label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
}
// Forthcoming-feature hint between SPREAD label and the combobox; rendered
// value comes from apps.epic.utils.stack_reversal_probability via the view
// context.
.sea-reversal-hint {
font-size: 0.7rem;
opacity: 0.55;
margin: -0.1rem 0 0;
font-style: italic;
}
// Custom combobox replacement for native <select>. See combobox.js for the
// expected markup; SCSS owns all visuals because the OS-native dropdown ignored
// option background/color anyway.
.sea-select {
position: relative;
cursor: pointer;
user-select: none;
background: rgba(var(--duoUser), 0.6);
border: 1px solid rgba(var(--terUser), 0.3);
border-radius: 0.3rem;
color: inherit;
padding: 0.4rem 0.5rem;
font-size: 0.85rem;
width: 100%;
max-width: 12.5rem;
outline: none;
&:focus-visible { box-shadow: 0 0 0 2px rgba(var(--terUser), 0.5); }
.sea-select-current {
display: block;
padding-right: 1.1rem; // room for the arrow
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sea-select-arrow {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
opacity: 0.6;
pointer-events: none;
font-size: 0.75rem;
}
.sea-select-list {
display: none;
list-style: none;
margin: 0.2rem 0 0;
padding: 0.15rem;
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 100;
background: rgba(var(--undUser), 1);
border: 1px solid rgba(var(--terUser), 0.5);
border-radius: 0.3rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4);
li[role="option"] {
padding: 0.4rem 0.5rem;
border-radius: 0.2rem;
color: rgba(var(--priUser), 1);
cursor: pointer;
transition: background 0.1s, color 0.1s;
}
// Hover + keyboard-focus + selected all share the inverted scheme so
// the visual feedback stays consistent regardless of input device.
li[role="option"]:hover,
li[role="option"].sea-select-option--focus,
li[role="option"][aria-selected="true"] {
background: rgba(var(--priUser), 1);
color: rgba(var(--secUser), 1);
}
}
&[aria-expanded="true"] {
.sea-select-list { display: block; }
.sea-select-arrow { transform: translateY(-50%) rotate(180deg); }
}
}
// Deck stacks — DECKS label + gravity + levity piles
.sea-stacks {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 1rem 0;
}
.sea-stacks-label {
writing-mode: vertical-rl;
transform: rotate(180deg);
text-transform: uppercase;
// Fill the full card height ($sea-card-h: 6.5rem) with 5 letters
font-size: 1rem;
letter-spacing: 0.32em;
font-weight: 700;
opacity: 0.5;
white-space: nowrap;
flex-shrink: 0;
align-self: center;
}
.sea-deck-stack {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
cursor: pointer;
}
.sea-stack-face {
position: relative;
width: $sea-card-w;
height: $sea-card-h;
border-radius: 0.3rem;
border: 0.15rem solid;
display: flex;
align-items: center;
justify-content: center;
transition: box-shadow 0.15s;
z-index: 1; // sits above the name label
}
.sea-stack-ok {
position: absolute;
top: 50%;
left: 0;
right: 0;
margin: 0 auto;
transform: translateY(-50%);
z-index: 5;
}
.sea-deck-stack { gap: 0; } // remove gap so name slides under the face
.sea-stack-name {
font-size: 0.65rem;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 600;
opacity: 0.6;
// Pull top of label partially under the stack face
// margin-top: -0.1rem;
transform: scaleY(1.2);
transform-origin: top center;
z-index: 0;
}
.sea-deck-stack--gravity .sea-stack-name { color: rgba(var(--quaUser), 1); }
.sea-deck-stack--levity .sea-stack-name { color: rgba(var(--terUser), 1); }
// Deck backs — face-down pile colour identifies polarity
$_sea-shadow: 1px 2px 0 rgba(0,0,0,0.7), 0 4px 0 rgba(0,0,0,0.18), 2px 5px 5px rgba(0,0,0,0.5);
$_glow-levity: 0 0 0.8rem 0.15rem rgba(var(--ninUser), 0.6);
$_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
.sea-deck-stack--levity .sea-stack-face {
background: rgba(var(--terUser), 0.88);
border-color: rgba(var(--ninUser), 0.65);
box-shadow: $_sea-shadow;
}
.sea-deck-stack--gravity .sea-stack-face {
background: rgba(var(--quiUser), 0.88);
border-color: rgba(var(--quaUser), 0.65);
box-shadow: $_sea-shadow;
}
// Glow on hover, :active, and while OK is showing (--active class set by JS)
.sea-deck-stack--levity:hover .sea-stack-face,
.sea-deck-stack--levity:active .sea-stack-face,
.sea-deck-stack--levity.sea-deck-stack--active .sea-stack-face { box-shadow: $_sea-shadow, $_glow-levity; }
.sea-deck-stack--gravity:hover .sea-stack-face,
.sea-deck-stack--gravity:active .sea-stack-face,
.sea-deck-stack--gravity.sea-deck-stack--active .sea-stack-face { box-shadow: $_sea-shadow, $_glow-gravity; }
// Form action row — LOCK HAND + DEL side by side at the bottom
.sea-form-actions {
display: flex;
gap: 0.5rem;
margin-top: auto;
padding-top: 0.75rem;
}
// NVM button — same positioning as .sky-modal-wrap > .btn-cancel
.sea-modal-wrap > .btn-cancel {
position: absolute;
top: -1rem;
right: -1rem;
z-index: 10;
}
// ── Sea stage — big card viewer ───────────────────────────────────────────────
.sea-stage {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
}
.sea-stage-backdrop {
position: absolute;
inset: 0;
cursor: pointer;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(4px);
}
.sea-stage-content {
// Card width drives the stage-card AND the stat block (which reuses
// --sig-card-w via the shared stat-block-shared mixin's calc rules).
// Override per breakpoint below to keep the stage from blowing up on small
// screens.
--sig-card-w: 180px;
position: relative;
z-index: 1;
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 0.75rem;
padding: 1.5rem;
}
// Sea-stage mobile breakpoints — mirror the fan modal's portrait/landscape
// trigger thresholds so behavior across staging surfaces stays consistent.
@media (orientation: portrait) and (max-width: 480px) {
.sea-stage-content { --sig-card-w: 130px; }
}
@media (orientation: landscape) and (max-height: 500px) {
.sea-stage-content { --sig-card-w: 130px; }
}
// Stage card — size matches sig-select stage (--sig-card-w driven by inline style)
.sea-stage-card {
flex-shrink: 0;
width: var(--sig-card-w, 140px);
height: auto;
aspect-ratio: 5 / 8;
border-radius: 0.5rem;
background: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 0.6);
display: flex;
flex-direction: column;
position: relative;
padding: 0.25rem;
overflow: hidden;
transform-style: preserve-3d;
transition: transform 0.4s ease;
// Flip-in animation when stage opens
&--shown {
animation: sea-flip-in 0.35s ease forwards;
}
.fan-card-corner--tl,
.fan-card-corner--br {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: calc(var(--sig-card-w, 140px) * 0.133); font-weight: 700; }
i { font-size: calc(var(--sig-card-w, 140px) * 0.1); }
}
.fan-card-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.25rem 0.15rem;
gap: 0.2rem;
.fan-card-face-upright { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
.fan-card-face-reversal { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding-top: 0.1rem; }
.fan-card-name-group { font-size: calc(var(--sig-card-w, 140px) * 0.073); opacity: 0.6; }
.sig-qualifier-above,
.sig-qualifier-below,
.fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 140px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; }
.fan-card-name,
.fan-card-reversal-name { font-size: calc(var(--sig-card-w, 140px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; }
.fan-card-arcana { font-size: calc(var(--sig-card-w, 140px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
.fan-card-reversal-qualifier,
.fan-card-reversal-name { transform: rotate(180deg); opacity: 0.25; }
}
&.stage-card--reversed {
transform: rotate(180deg);
.fan-card-reversal-qualifier,
.fan-card-reversal-name { opacity: 1; }
.fan-card-name,
.sig-qualifier-above,
.sig-qualifier-below { opacity: 0.25; }
}
}
@keyframes sea-flip-in {
0% { transform: perspective(600px) rotateY(-90deg) scale(0.4); opacity: 0; }
60% { transform: perspective(600px) rotateY(8deg) scale(1.03); opacity: 1; }
100% { transform: perspective(600px) rotateY(0deg) scale(1); opacity: 1; }
}
// Sea stage card title — polarity-specific colour + glow. Drop shadow polarity-split:
// levity card has a light --secUser bg so a dark drop reads as smudge under the
// --quiUser title; gravity card is dark --priUser bg so the dark drop reads clean.
$_sea-title-shadow-levity: 1px 1px 0 rgba(255,255,255,1), 0 0 0.25rem rgba(var(--ninUser), 0.25);
$_sea-title-shadow-gravity: 1px 1px 0 rgba(0,0,0,1), 0 0 0.25rem rgba(var(--ninUser), 0.25);
$_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .fan-card-reversal-name, .fan-card-reversal-qualifier';
.sea-stage--levity .sea-stage-card {
@include stage-card-polarity(
$titles-color: rgba(var(--quiUser), 1),
$text-shadow: $_sea-title-shadow-levity,
$invert-frame: true,
);
color: rgba(var(--priUser), 1);
.fan-card-arcana,
.fan-card-corner { color: rgba(var(--priUser), 1); }
}
.sea-stage--gravity .sea-stage-card {
@include stage-card-polarity(
$titles-color: rgba(var(--terUser), 1),
$text-shadow: $_sea-title-shadow-gravity,
);
}
// Sea stat block — reuses sig-select stat-block sizing, scoped to sea-stage
.sea-stage-content .sea-stat-block {
flex: 0 0 auto;
width: var(--sig-card-w, 140px);
height: calc(var(--sig-card-w, 140px) * 8 / 5);
background: rgba(var(--priUser), 0.85);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--terUser), 0.15);
position: relative;
display: block;
@include stat-block-shared;
// PRV/NXT only appear once the FYI tooltip is open (matches sig + fan).
&.fyi-open {
.fyi-prev, .fyi-next { display: inline-flex; }
}
}
@media (orientation: landscape) {
html.sea-open body .container .navbar,
html.sea-open body #id_footer {
z-index: 90;
}
}