Files
python-tdd/src/static_src/scss/_card-deck.scss
Disco DeDisco 1e2041ed9f
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
A.7.5-polish-5 DRY _stat_face.html partial + FLIP-btn SCSS unification + my_sign FLIP DOM move-into-card + universal hover-reveal + instant mid-flip vanish + sea-stage-card image-mode bg fix + multi-line comment syntax cleanup. User-spec 2026-05-25 PM bundle of 5 cleanup threads atop polish-4 (4554c71).
**(1) `_stat_face.html` partial** — extracted to `templates/core/_partials/_stat_face.html` per user 2026-05-25 PM: "Why are there so many individual instances of this feature? Couldn't we call the same DRY partial for each?". One partial covers all 4 stat-block surfaces (sig-stat-block / sea-stat-block / fan-stage-block / my-sign-applet-stat-block) — ~80 lines of duplicated markup collapse to 7 `{% include %}` sites (3 surfaces × 2 faces + applet × 1 face). Args: `face_modifier` (required: "upright"|"reversed"), `label_text` (required: "Emanation"|"Reversal"), `card` (optional TarotCard for applet's server-render path), `keywords_ul_id` (optional id attr on the keyword `<ul>` — sea_stage + fan need `id_sea_stat_upright/reversed` + `id_fan_stat_upright/reversed` for stage-card.js's `populateKeywords` surface-specific selector overrides). The `.stat-face` wrapper that the partial introduces is a no-op for the applet — applet's bespoke `.my-sign-applet-stat-block` rule doesn't `@include stat-block-shared` so `.stat-face` inherits no padding / display-none from the shared mixin.

**(2) FLIP-btn `@mixin flip-btn-base` + `%flip-btn-revealed` + `%flip-btn-mid-flip` primitives** — `_card-deck.scss` head per user 2026-05-25 PM: "unify the many disparate calculations we use for when we allow that FLIP btn to appear and where it appears". Each surface's flip-btn declaration now `@include`s the base (position absolute + zero margin + hidden default opacity 0 + 0.3s transition) and `@extend`s `%flip-btn-revealed` on its surface-specific reveal trigger + `%flip-btn-mid-flip` on its surface-specific `[data-flipping]` selector chain. ~30 lines of duplication collapsed to 6 lines of mixin/placeholder + 3 `@include` + 4 `@extend` calls.

**(3) my_sign FLIP btn moved INSIDE `.sig-stage-card`** + `.my-sign-flip-btn` + `.my-sign-applet-flip-btn` share one positioning rule (`bottom: 0.6rem; left: 0.6rem`) — was a sibling under `.my-sign-stage` positioned via stage-padding-relative `calc(1.5rem + 0.4rem)`. Polish-5 nests it INSIDE the card so positioning is naturally card-relative + the separate `.my-sign-page[data-current-card-id]` centered-mode geometric override (re-deriving offsets from the centred-row layout) is DROPPED entirely. The applet was already inside-card positioned; same `bottom: 0.6rem; left: 0.6rem` rule combines both surfaces in a single `_card-deck.scss` declaration. The applet's `_billboard.scss` flip-btn rule is now just a shim `@include` + `@extend` (the positioning got DRY'd up to the shared rule).

**(4) Hover-reveal everywhere** + instant mid-flip vanish — user-spec 2026-05-25 PM: "The .btn-reveal behavior here should now (1) disappear much earlier, so no independent ease-in/-out logic needed on clicking FLIP; (2) calculate its position more dynamically; be mirrored in the gameboard's My Sign applet. In all places does the hover-to-reveal-FLIP-.btn-reveal effect abate while the card is finishing a FLIP". my_sign main flipped from `display: none → display: inline-flex` (frozen-gated) to opacity-based hover-reveal on `.sig-stage-card:hover` (still gated by `.sig-stage--frozen`). Applet flipped from always-visible to opacity-based hover-reveal on `.my-sign-applet-card:hover`. Fan kept its existing hover-reveal. Mid-flip-hide changed from `opacity: 0 + pointer-events: none` (faded out over the 0.3s transition, which competed w. the click) to `display: none` — INSTANT vanish, no ease-out animation. All 3 surfaces consolidated into one combined `[data-flipping] -> flip-btn` selector list extending `%flip-btn-mid-flip`. The `:has(.flip-btn:hover)` self-pin clause (already present on fan) added to my_sign + applet too — keeps the btn visible while the cursor is on it, otherwise the btn (z-index 25, on top of the card) steals `:hover` from the card the moment the cursor moves onto it + retracts the reveal mid-click.

**(5) `.sea-stage--levity .sea-stage-card` image-mode bg fix** — user-reported 2026-05-25 PM: "the card preview stage in my_sea.html still sports the old card bg (the --secUser here) behind the card img (with the --quiUser box-shadow border)". Same source-order collision pattern as the sea-sig-card fix in polish-4: `.sea-stage--levity .sea-stage-card`'s `@include stage-card-polarity($invert-frame: true)` sets `background: rgba(var(--secUser), 1) + border-color: rgba(var(--priUser), 1)` at specificity 0,2,0 — matches the shared `.sig-stage-card.sig-stage-card--image` comma-list rule's specificity but source-loses to it (levity rule lives at line 2150, comma-list at line 705). Fix: add a `&.sig-stage-card--image { background: transparent; border: 0; }` nested override (0,3,0 specificity) — re-states the transparency under the levity polarity branch so image-mode drawn cards (Minchiate today) don't show a beige card-shape behind the PNG art. The gravity branch was already fine (its mixin call doesn't pass `$invert-frame`).

**(6) Multi-line `{# #}` comment syntax cleanup** — user-spotted 2026-05-25 PM after my polish-5 partial extraction caused visible comment text to leak into rendered HTML on 4 templates (per [[feedback-django-multiline-comments]] / [[feedback-django-comments-single-line-only]] traps the user has flagged before). All multi-line block comments I added in this polish converted to `{% comment %}...{% endcomment %}` form — covers the `_stat_face.html` partial header + 4 template include sites (my_sign.html × 2 blocks, _applet-my-sign.html, _sea_stage.html, game_kit.html).

Tests: 1314/1314 IT+UT total green (72s). No new tests — existing chip-presence + image-mode ITs from polish-4 still pass through the partial extraction. Visual verify 2026-05-25 PM via Claudezilla: my_sign main page (Queen of Coins) renders cleanly via partial w. card+stat-block; applet renders cleanly w. server-filled chip + title; carousel + sea_stage modal work via JS-populated partial includes; my_sign FLIP btn moved into card + hover-reveals + vanishes instantly on FLIP click; sea-stage-card no longer shows --secUser bg behind image-mode PNG art under levity. DRY partial extraction was held out of polish-4 as user-requested separate concern: "hold it for a separate commit, but fold the FLIP btn unification into it as the styling cleanup part" — done.

**Follow-up parked for next sprint**: user-flagged 2026-05-25 PM "If it's interfering to have bespoke rules, just allow the FLIP btn everywhere, including in my_sea.html". This needs (a) dropping the `not card.deck_variant.is_polarized` server-render gate in the applet template, (b) adding a FLIP btn + back-img element to the `_sea_stage.html` modal scaffold, (c) wiring a JS handler in sea.js (currently has no FLIP behavior for drawn-card stage). Out of scope for the polish-5 commit since it's template + JS scope; will pick up as polish-6 or a fresh sprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:22:08 -04:00

2235 lines
88 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 FLIP-btn primitives ───────────────────────────────────────────────
//
// The FLIP btn lives on 3 surfaces (my_sign main stage, my_sign applet, game
// kit fan). Polish-5 2026-05-25 PM unification per user spec: hover-reveal
// everywhere (was display-toggle on my_sign + always-visible on applet);
// `display: none` mid-flip for INSTANT vanish (was opacity-fade — felt sluggish
// since clicks happen faster than the 0.3s ease). The reveal-on-hover transition
// stays smooth for everyday hover, but the moment a FLIP starts the btn pops
// out of layout entirely so it can't visually interfere w. the rotateY mid-spin.
//
// @mixin flip-btn-base Position absolute + zero margin + hidden default
// (opacity:0 + pointer-events:none) + 0.3s opacity
// transition for the smooth hover-reveal. Surfaces
// add z-index + position offsets (most use bottom-
// left-of-card; fan uses transform-based carousel-
// shift since its btn is at wrap-level).
// %flip-btn-revealed opacity:1 + pointer-events:auto — applied by
// each surface's hover-trigger selector chain.
// %flip-btn-mid-flip display:none — applied by each surface's
// `[data-flipping]` selector chain. Instant vanish
// (display isn't animatable); supersedes the
// hover-reveal entirely while rotation is in
// flight. Each surface's selector chain differs
// because the btn-to-card DOM relationship varies
// (inside the card on my_sign + applet post-polish-
// 5; sibling under common wrap for the fan).
@mixin flip-btn-base {
position: absolute;
margin: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
%flip-btn-revealed {
opacity: 1;
pointer-events: auto;
}
%flip-btn-mid-flip {
display: none;
}
// ── 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;
// Sprint A.7.5-polish-4 — top-pinned content per user-spec 2026-05-25 PM
// ("pin the number/alphanumeric at the top and the rest of the content
// cascades down from it, instead of pinning the arcana type in the center
// and stacking the rest of the content atop it"). Was top: 0.37 of card-w
// (visually-centered the arcana mid-stat-block); now uniform 0.1 so the
// chip header sits at the actual top edge + cascade flows down.
padding: calc(var(--sig-card-w, 120px) * 0.1)
calc(var(--sig-card-w, 120px) * 0.1)
calc(var(--sig-card-w, 120px) * 0.08);
}
.stat-face--upright { display: block; }
&.is-reversed {
opacity: 1;
.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;
// Sprint A.7.5 user-spec 2026-05-25 PM — label color flipped from
// --terUser to --secUser so EMANATION/REVERSAL recedes visually and
// lets the title stay the focal text. Gravity-polarity overrides
// below still flip to --quiUser since the gravity stat-block bg is
// --secUser (a --secUser label would be invisible).
color: rgba(var(--secUser), 1);
margin: 0;
// text-decoration: underline dropped in polish-4 — the new
// `.stat-face-header` border-bottom underscores the whole header
// (chip + icon + label) as a single visual unit, separating it
// from the title block below.
}
// Sprint A.7.5-polish-4 — header is now a two-row vertical stack so
// long Roman numerals (e.g. XXVIII) get their own line w. room to
// breathe; row-2 holds the suit-icon + EMANATION/REVERSAL label
// inline (the icon is always 1 char so it never overflows). The
// border-bottom underscores both rows as one header unit, replacing
// the prior per-label text-decoration: underline.
.stat-face-header {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: calc(var(--sig-card-w, 120px) * 0.02);
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
padding-bottom: calc(var(--sig-card-w, 120px) * 0.04);
border-bottom: 0.05rem solid rgba(var(--secUser), 0.4);
}
.stat-chip-rank {
font-size: calc(var(--sig-card-w, 120px) * 0.105);
font-weight: bold;
line-height: 1;
color: rgba(var(--secUser), 1);
&:empty { display: none; }
}
.stat-chip-tag {
display: inline-flex;
align-items: baseline;
gap: calc(var(--sig-card-w, 120px) * 0.04);
line-height: 1;
i {
font-size: calc(var(--sig-card-w, 120px) * 0.083);
color: rgba(var(--secUser), 1);
}
}
// Sprint A.7-polish-3 — title + arcana fields per locked Q3 spec.
// Title color keys off the stat-block's `data-arcana-key` attr (set by
// stage-card.js populateStatExtras OR server-side in the applet partial):
// - MAJOR → --terUser (gold)
// - MINOR / MIDDLE → --quaUser (bright yellow-gold, user-spec 2026-
// 05-25 PM "only the My Sign applet has --quaUser as a font color;
// the rest are --quiUser. Let's change the latter to match the
// former" — applet's `.stat-face-title` was already --quaUser;
// shared mixin now matches so all 4 stat-block surfaces unify).
.stat-face-title {
font-size: calc(var(--sig-card-w, 120px) * 0.105);
font-weight: 700;
line-height: 1.15;
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.03);
text-wrap: balance;
color: rgba(var(--quaUser), 1);
}
[data-arcana-key="MAJOR"] .stat-face-title {
color: rgba(var(--terUser), 1);
}
.stat-face-arcana {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.6;
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
}
// `:empty` rule hides title + arcana when stage-card.js hasn't populated
// them yet (rest state) — prevents zero-height paragraphs from inflating
// the stat block vertical layout.
.stat-face-title:empty,
.stat-face-arcana:empty { display: none; }
.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);
// Fallback bg when no `.tarot-fan-wrap[data-polarity]` parent (test
// fixtures, etc.). Live polarity inversion lives in the parent rule
// below — `.tarot-fan-wrap[data-polarity=...] .fan-stage-block`.
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-stage-block polarity inversion — sig convention applied to Game Kit
// (user-spec 2026-05-23). The active card's polarity is mirrored onto the
// shared `.tarot-fan-wrap` ancestor by `game-kit.js:_populateStage` and
// `_flipActive` so the stat block can pick up the opposite-polarity bg
// without JS having to touch the stat block directly.
// Sprint A.7.5 user-spec 2026-05-25 PM — gravity polarity stat-block bg
// flipped from --secUser to --priUser to match the applet's pattern (which
// keeps the stat-block bg as --priUser under both polarities). User
// observation: "polarity seems to be reversed everywhere but the My Sign
// applet". Card + stat-block now share the SAME polarity bg (--priUser
// under gravity) — explicit revision of the prior opposite-polarity rule.
// Inner color overrides (label/chip/keywords) collapse to match the levity
// branch since both now sit on --priUser bg.
.tarot-fan-wrap[data-polarity="gravity"] .fan-stage-block,
.tarot-fan-wrap[data-polarity="levity"] .fan-stage-block {
// Sprint A.7.5-polish-3 — alpha bumped to 1.0 unified across all 4 stat-
// block surfaces (user-spec 2026-05-25 PM, supersedes polish-2's 0.5).
background: rgba(var(--priUser), 1);
border-color: rgba(var(--terUser), 0.15);
color: rgba(var(--secUser), 1);
.stat-face-label { color: rgba(var(--secUser), 1); }
.stat-keywords li {
color: rgba(var(--quiUser), 1);
border-bottom-color: rgba(var(--terUser), 0.18);
}
}
// Levity rule above (combined w. gravity since both now use --priUser bg).
.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 {
@include flip-btn-base;
z-index: 25;
top: 50%;
left: 50%;
// Carousel-shifted bottom-left-of-focused-card. The btn sits at wrap level
// (not inside any card) so positioning has to derive from the carousel
// geometry vars rather than the bottom-left-of-card pattern the other 2
// surfaces use. --fan-stage-shift + --fan-card-w/h scale w. breakpoint
// via the parent `.tarot-fan-wrap`.
transform: translate(calc(-50% - var(--fan-stage-shift) - var(--fan-card-w) / 2 + 1.5rem),
calc(-50% + var(--fan-card-h) / 2 - 1.5rem));
}
// Hover-reveal. The `.fan-flip-btn:hover` clause pins the btn visible while
// the cursor is on it — without it, the btn (z-index 25, on top of the card)
// steals :hover from the card the moment the cursor moves onto it, retracting
// the reveal + letting the in-flight click pass through to the dialog backdrop
// (which closes the modal). `.fan-touch-revealed` is the touch-device fallback
// (no :hover on touchscreens) — game-kit.js toggles it on first card tap.
.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 {
@extend %flip-btn-revealed;
}
// Mid-flip-hide selector for the fan is consolidated into the unified
// 3-surface rule below the `.my-sign-flip-btn` / `.my-sign-applet-flip-btn`
// declarations. See `_card-deck.scss` polish-5 FLIP-btn block.
.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;
// Sprint A.7.5-polish-3 — alpha bumped to 1.0 to unify w. the applet
// + sea_stage + fan_stage at full opacity per user spec 2026-05-25 PM
// ("set all of them to the higher opacity that My Sign just had").
background: rgba(var(--priUser), 1);
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; }
}
}
// Sprint A.3 / A.5 — image-rendering mode for decks w. DeckVariant.has_card_images=True
// (Minchiate Fiorentine 1860-1890 today; future image-equipped decks flip
// the flag to opt in). LIFTED OUT of the `.sig-stage` nest in A.5 polish so
// the same rule applies wherever `.sig-stage-card.sig-stage-card--image`
// renders — my_sign.html's stage card (inside .sig-stage), my_sea.html's
// central sig card (.sea-sig-card inside .sea-pos-core, NOT in .sig-stage),
// future surface drops. When `.sig-stage-card--image` is set (either by
// stage-card.js _setImageMode or server-side template branch), the text
// scaffold (fan-card-* + .fan-corner-rank text children) hides and an
// <img.sig-stage-card-img> renders inside the same shell. Card bg + border
// go away — the transparent PNG carries its own irregular outline; four
// cardinal-direction drop-shadows on the <img> render a stroke-like outline
// that FOLLOWS the alpha contour (user spec 2026-05-25 PM — NOT a rectangular
// border around the bounding box). Color is arcana-driven: `--quiUser` (cream)
// for minor + middle, `--terUser` (gold) for major per
// [[project-image-based-deck-face-rendering]]'s Q2 lock.
.sig-stage-card.sig-stage-card--image,
.my-sign-applet-card.my-sign-applet-card--image,
.my-sea-slot.my-sea-slot--image,
.sea-card-slot.sea-card-slot--image,
.fan-card.fan-card--image {
--img-stroke-color: rgba(var(--quiUser), 1);
background: transparent;
border: 0;
padding: 0;
overflow: visible;
&[data-arcana-key="MAJOR"] {
--img-stroke-color: rgba(var(--terUser), 1);
}
.fan-card-corner,
.fan-card-face,
.fan-corner-rank,
> i.fa-solid {
display: none;
}
.sig-stage-card-img,
.sig-stage-card-back-img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
// Filter chain (order matters — each drop-shadow operates on
// the prior result):
// 1-4: 4 cardinal-direction drop-shadows at 0.2rem (~3.2px)
// each → contour-following stroke. Combined apparent width
// ~6.4px. Bump to 8-direction stack if we ever go past
// ~0.5rem so curved edges stay even.
// 5: down-right black 1,1 offset 2px-blur drop-shadow
// matches the silhouette shadow `.tray-cell > img` carries
// (`_tray.scss:272`) — "lifted off the felt" depth cue.
// Comes AFTER the strokes so it traces the stroked
// silhouette, not just the original PNG alpha.
// Mobile-safe: filter on raster images works fine cross-browser
// (the [[feedback-mobile-svg-glow]] dead-end was specifically
// SVG glow, not raster drop-shadow).
filter:
drop-shadow( 0.2rem 0 0 var(--img-stroke-color))
drop-shadow(-0.2rem 0 0 var(--img-stroke-color))
drop-shadow( 0 0.2rem 0 var(--img-stroke-color))
drop-shadow( 0 -0.2rem 0 var(--img-stroke-color))
drop-shadow( 1px 1px 2px rgba(0, 0, 0, 1));
}
.sig-stage-card-back-img { display: none; } // shown only when flipped
// Sprint A.5 — FLIP-to-back behavior for non-polarized image-equipped
// decks (Minchiate today). When `.is-flipped-to-back` is toggled by
// my_sign's flip-btn handler, the front face img hides + the deck
// card-back img shows. Stat block + arcana-key stroke color stay put —
// FLIP is purely a visual reveal of the card's back, no polarity-cycle
// or content swap. User spec 2026-05-25 PM.
&.is-flipped-to-back {
.sig-stage-card-img { display: none; }
.sig-stage-card-back-img { display: block; }
}
}
// ─── 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;
}
// Saved-sig read-only state — page bg shifts to --duoUser so the now-
// hexless aperture reads as a distinct mode (mirrors how `.my-sea-page
// [data-phase="picker"]` swaps bg in `_gameboard.scss`). Keyed on the
// presence of `data-current-card-id` since that attribute renders only
// when the user has a saved significator. Stage card + stat block also
// center in the now-empty page aperture (default landing keeps stage
// natural-sized at the top above the hex; here there's no hex so the
// stage gets to grow + middle itself).
.my-sign-page[data-current-card-id] {
background-color: rgba(var(--duoUser), 1);
// Stage grows to fill the available column space + centres its card
// row both horizontally + vertically. Override `.sig-stage`'s default
// `align-items: flex-end` + `padding-left: 1.5rem` so card + stat
// block land truly centred.
.my-sign-stage {
flex: 1;
min-height: 0;
justify-content: center;
align-items: center;
padding-left: 0;
}
// `.sig-stat-block`'s default `align-self: flex-end` (line 599)
// overrides the parent's `align-items: center` on the cross axis,
// so the stat block was floating to the bottom of the stage while
// the card sat at vertical-centre. Force `center` here to keep the
// pair aligned in the centred row.
.sig-stat-block { align-self: center; }
// Polish-5: FLIP-btn centered-mode offset override DROPPED. The btn is
// now positioned INSIDE the card (`.sig-stage-card { position: relative }`
// + btn `bottom: 0.6rem; left: 0.6rem`), so it follows the card naturally
// wherever the stage positions it — no per-layout-mode geometric calc.
// Landing collapses since the hex is server-side gone — just DEL is
// left + that's `position: absolute`. `position: static` here drops
// landing's positioning context so DEL walks up to `.my-sign-page`
// (already `position: relative`) + pins to the page corner.
.my-sign-landing {
flex: 0 0 auto;
min-height: 0;
position: static;
}
}
// 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 {
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);
}
// Polish-5: my_sign main + applet FLIP btns share one positioning rule.
// Both live INSIDE their card (`.sig-stage-card` / `.my-sign-applet-card`,
// both `position: relative`) so `bottom: 0.6rem; left: 0.6rem` anchors
// universally to card-bottom-left w/o needing surface-specific calc().
.my-sign-flip-btn,
.my-sign-applet-flip-btn {
@include flip-btn-base;
z-index: 25;
bottom: 0.6rem;
left: 0.6rem;
}
// Hover-reveal on the parent card. `:has(.flip-btn:hover)` pins the btn
// visible while the cursor is on it — without this clause, the btn (z-index
// 25, on top of the card) steals :hover from the card the moment the cursor
// moves onto it, retracting the reveal + breaking the click flow. Same
// pattern the fan carousel uses.
//
// my_sign main still gates on `.sig-stage--frozen` — hover-only previews
// don't reveal the polarity toggle until the user has committed a sig.
.my-sign-stage.sig-stage--frozen .sig-stage-card:hover .my-sign-flip-btn,
.my-sign-stage.sig-stage--frozen .sig-stage-card:has(.my-sign-flip-btn:hover) .my-sign-flip-btn,
.my-sign-applet-card:hover .my-sign-applet-flip-btn,
.my-sign-applet-card:has(.my-sign-applet-flip-btn:hover) .my-sign-applet-flip-btn {
@extend %flip-btn-revealed;
}
// Unified mid-flip-hide across all 3 surfaces. `[data-flipping]="1"` is set on
// the card by each surface's FLIP handler for the 500ms rotation duration;
// `%flip-btn-mid-flip`'s `display: none` makes the btn vanish INSTANTLY (per
// user spec — no ease-out logic competing w. the click). Selector chains
// differ per surface because the btn-to-card DOM relationship varies (btn is
// INSIDE the card on my_sign + applet post-polish-5; sibling under .tarot-
// fan-wrap for the fan).
.sig-stage-card[data-flipping] .my-sign-flip-btn,
.my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn,
.tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn {
@extend %flip-btn-mid-flip;
}
// ─── 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.
// NOTE: `.my-sea-page[data-polarity="..."]` deliberately NOT in this shared
// selector list (was bitten 2026-05-21 by drawn-card stage bleed). My-sea's
// drawn cards open into the `.sea-stage` modal whose `.sig-stage-card.sea-
// stage-card` element has BOTH classes — so a `.my-sea-page[data-polarity]
// .sig-stage-card` rule (0,3,0) silently overrides the proper card-specific
// `.sea-stage--levity/--gravity .sea-stage-card` polarity (0,2,0), forcing
// every drawn card's stage to inherit the user's sig polarity. My-sea's own
// page polarity rules live below + target ONLY `.sea-sig-card` (the spread-
// center sig). See [[feedback-page-polarity-scope-trap]].
.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. Per A.7.5 user-spec
// 2026-05-25 PM the label uses --secUser (was --terUser) so EMANATION /
// REVERSAL recedes against the title — same convention as the applet.
.sig-stat-block .stat-face-label { color: rgba(var(--secUser), 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"] {
// Sprint A.7.5 user-spec 2026-05-25 PM — stat-block bg under gravity
// collapses to the default --priUser (was --secUser w. inverted
// priUser/secUser), matching the My Sign applet's universal --priUser
// stat-block. Label/chip/keyword overrides below collapse too — the
// default rules (tuned for --priUser bg) cover both polarities now.
.sig-stat-block {
// bg falls through to the default `rgba(var(--priUser), 0.5)` set
// at `.sig-stage .sig-stat-block` above; no per-polarity override.
}
// 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); }
// Sprint A.7.5 — label + chip overrides under gravity dropped; the
// shared --secUser default (tuned for --priUser bg) applies in both
// polarities now that gravity stat-block bg = --priUser.
// 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)
}
// ── My-sea page polarity: scoped to `.sea-sig-card` only ──────────────────────
// The user's chosen sig (rendered as `.sig-stage-card.sea-sig-card` in the
// spread-center cell) is the ONLY element on my-sea whose colours track the
// page-level `data-polarity` (= `User.significator_reversed`). Drawn cards
// belong to their own polarity from the deck-stack they were pulled from +
// must NOT inherit the user's sig polarity — see big NOTE above the shared
// `.sig-overlay`/`.my-sign-page` block for the bleed trap that prompted this
// scoping (2026-05-21 bug).
//
// Gravity is the default rendering (`.sig-stage-card.sea-sig-card` base rule
// sets `background: --priUser, color: --secUser` at `_card-deck.scss:1379`)
// so we only need an override for the LEVITY case here — same idea as the
// `.sig-overlay[data-polarity="levity"] .sig-stage-card` block above.
.my-sea-page[data-polarity="levity"] .sig-stage-card.sea-sig-card {
background: rgba(var(--secUser), 1);
border-color: rgba(var(--priUser), 0.6);
color: rgba(var(--priUser), 1); // currentColor propagates to .fan-corner-rank + i
// Sprint A.7.5-polish-4 — same image-mode override as the base rule
// above. Without this the 0,3,0 levity rule's --secUser bg would
// re-clothe the sea-sig-card under levity even in image mode.
&.sig-stage-card--image {
background: transparent;
border: 0;
}
}
// ─── 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;
// Corner-rank + suit-icon track `color` so the polarity rules below
// (which set `.sig-stage-card { color: ... }` to --priUser for levity)
// flip the contrast w. the card's bg. The default (gravity, --priUser
// bg) inherits --secUser from the `.sig-stage-card.sea-sig-card` rule
// below; the levity polarity rule overrides `.sig-stage-card { color }`
// to --priUser, which propagates down through currentColor.
.fan-corner-rank {
font-size: 1.2rem;
font-weight: 700;
line-height: 1;
color: currentColor;
opacity: 0.85;
}
i { font-size: 1rem; color: currentColor; opacity: 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);
color: rgba(var(--secUser), 1); // default (gravity) text color; `[data-polarity="levity"] .sig-stage-card { color: --priUser }` overrides for levity. Corner-rank + suit-icon track currentColor.
display: flex;
flex-direction: column;
position: relative;
padding: 0.25rem;
overflow: hidden;
// Sprint A.7.5-polish-4 — image-mode override. `.sig-stage-card.sea-sig-
// card` (0,2,0) matches the shared `.sig-stage-card.sig-stage-card--image`
// comma-list rule's specificity but source-loses to it — so we re-state
// the transparency here AT 0,3,0 (parent + 2 sibling classes). Mirrors
// the my_sign-applet pattern in `_billboard.scss`. Filter chain on
// `.sig-stage-card-img` is still inherited from the shared rule (the
// image-mode rule's img descendant selector doesn't lose anywhere).
&.sig-stage-card--image {
background: transparent;
border: 0;
padding: 0;
}
.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;
}
// Sprint A.7-polish — single (non-polarized) deck stack for my_sea.html
// when the equipped deck has no polarity (Minchiate today). Neutral palette
// vs. gravity/levity polarity colors. When the deck also has card images,
// the actual back-image overlays the face via .sea-stack-face-img (object-
// fit: cover so the PNG fills the rect cleanly; positioned absolute so the
// FLIP btn on top still sits center).
.sea-deck-stack--single .sea-stack-face {
background: rgba(var(--priUser), 0.88);
border-color: rgba(var(--terUser), 0.65);
box-shadow: $_sea-shadow;
overflow: hidden; // clip the back-img to the rounded-rect face
}
.sea-stack-face-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.15rem; // inner radius matches the face's outer
z-index: 0;
}
// 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; }
// Single-stack hover/active glow — neutral --terUser tone vs. gravity's
// --quaUser + levity's --ninUser, matching the face border color.
.sea-deck-stack--single:hover .sea-stack-face,
.sea-deck-stack--single:active .sea-stack-face,
.sea-deck-stack--single.sea-deck-stack--active .sea-stack-face {
box-shadow: $_sea-shadow, 0 0 0.8rem 0.15rem rgba(var(--terUser), 0.6);
}
// 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); }
// Polish-5: image-mode override mirrors the sea-sig-card pattern. The
// `$invert-frame: true` arg above sets `--secUser` bg + `--priUser`
// border, which then OUT-CASCADES the shared image-mode comma-list rule
// (same 0,2,0 specificity but later in source). Re-state transparency
// here so image-mode drawn cards (Minchiate today) don't show a beige
// card-shape behind the PNG art.
&.sig-stage-card--image {
background: transparent;
border: 0;
}
}
.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.
// `background` left blank here; the `.sea-stage--gravity` / `.sea-stage--
// levity` parent rules below set the polarity-aware bg (sig convention,
// user-spec 2026-05-23: stat block always carries the OPPOSITE-polarity
// color of its adjacent card — gravity card / secUser stat block, levity
// card / priUser stat block). Fallback `--priUser` 0.85 stays for any
// stray `.sea-stat-block` rendered outside the polarity-classed parent.
.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);
// Sprint A.7.5-polish-3 — unified 1.0 alpha across all stat-block surfaces.
background: rgba(var(--priUser), 1);
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; }
}
}
// Sea stat block — polarity inversion (sig convention applied to sea,
// user-spec 2026-05-23). The drawn card's polarity (set by SeaDeal at
// stage-open time via `.sea-stage--gravity` / `.sea-stage--levity` on
// the stage root) cascades to the stat block here. SPIN/face-swap is
// unchanged — `.is-reversed` still just toggles which face renders;
// it does NOT shift the bg (orientation is preview-only, polarity is
// the persisted axis that paints the surfaces).
// Sprint A.7.5 user-spec 2026-05-25 PM — gravity stat-block bg flipped to
// match the My Sign applet (always --priUser regardless of polarity). Both
// gravity + levity collapse to the same colors since both sit on --priUser
// bg now; the original opposite-polarity inversion is dropped.
.sea-stage--gravity .sea-stat-block,
.sea-stage--levity .sea-stat-block {
// Sprint A.7.5-polish-3 — unified 1.0 alpha across all stat-block surfaces.
background: rgba(var(--priUser), 1);
border-color: rgba(var(--terUser), 0.15);
.stat-face-label { color: rgba(var(--secUser), 1); }
.stat-keywords li {
color: rgba(var(--quiUser), 1);
border-bottom-color: rgba(var(--terUser), 0.18);
}
}
// Levity rule above (combined w. gravity since both now use --priUser bg).
@media (orientation: landscape) {
html.sea-open body .container .navbar,
html.sea-open body #id_footer {
z-index: 90;
}
}