// ─── 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 6–9 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 // 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 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 + sea_stage FLIP btns share one positioning // rule. All live INSIDE their card (`.sig-stage-card` / `.my-sign-applet-card` // / `.sig-stage-card.sea-stage-card`, all `position: relative`) so `bottom: // 0.6rem; left: 0.6rem` anchors universally to card-bottom-left w/o needing // surface-specific calc(). Polish-6 added sea-stage-flip-btn for the sea_stage // modal per user-spec "allow the FLIP btn everywhere". .my-sign-flip-btn, .my-sign-applet-flip-btn, .sea-stage-flip-btn { @include flip-btn-base; z-index: 25; bottom: 0.6rem; left: 0.6rem; } // Reversed-card FLIP counter-positioning. The sea_stage card rotates 180° // via `.stage-card--reversed`; without this override the in-card FLIP btn // (anchored to card-local bottom-left) visually rides along to top-right // post-rotation. Re-anchor to card-local top-right + counter-rotate the // btn's own transform so it visually lands at the same bottom-left it // would have if the card hadn't rotated — and the label still reads // upright. Matches the game-kit fan precedent (`.fan-flip-btn` lives // OUTSIDE the rotating card on `.tarot-fan-wrap`, so it never moves) // without restructuring the in-card DOM that my_sign + my_sign-applet // also share. The `[data-spinning]` hide-during-rotate rule below means // the user never sees the btn jump between the two corners; it // disappears mid-spin + reappears at bottom-left after the rotation // lands. Spec 2026-05-26. // Companion my_sign main fix landed 2026-05-26 PM — same in-card FLIP-btn // DOM contract as sea_stage, so the same .stage-card--reversed whack-a-mole // applies. (My_sign-applet is NOT covered — the applet renders the sig in // its polarity but never SPIN-rotates; no `.stage-card--reversed` ever // lands on `.my-sign-applet-card`.) .sea-stage-card.stage-card--reversed .sea-stage-flip-btn, .sig-stage-card.stage-card--reversed .my-sign-flip-btn { bottom: auto; left: auto; top: 0.6rem; right: 0.6rem; transform: rotate(180deg); } // 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, .sea-stage-card:hover .sea-stage-flip-btn, .sea-stage-card:has(.sea-stage-flip-btn:hover) .sea-stage-flip-btn { @extend %flip-btn-revealed; } // Unified mid-flip-hide across all 4 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 + sea_stage; sibling under .tarot-fan- // wrap for the fan carousel). // // `[data-spinning]` on `.sea-stage-card` companions the SPIN-click and // reversed-card open auto-rotate windows in sea.js — same hide treatment as // FLIP-flipping, so the user never sees the btn jump between bottom-left and // (rotated) top-right mid-rotate. Btn reappears at the post-rotation visual // bottom-left via the counter-positioning rule above. .sig-stage-card[data-flipping] .my-sign-flip-btn, .sig-stage-card[data-spinning] .my-sign-flip-btn, .my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn, .sea-stage-card[data-flipping] .sea-stage-flip-btn, .sea-stage-card[data-spinning] .sea-stage-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 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. // `overflow: visible` pinned here too so the stroke-contour isn't // clipped under levity polarity (parity w. base above). &.sig-stage-card--image { background: transparent; border: 0; overflow: visible; } } // ─── 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; } } // ─── 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). // // `overflow: visible` re-stated — base `.sig-stage-card.sea-sig-card` // above sets `overflow: hidden` at (0,2,0) which beats the shared // image-mode rule's `overflow: visible` by source order. Without this // the contour-stroke filter chain (drop-shadow ±0.2rem cardinals) // gets clipped to the card's bounding rectangle, painting a uniform // rectangular frame instead of an alpha-following contour stroke. &.sig-stage-card--image { background: transparent; border: 0; padding: 0; overflow: visible; } .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