// ─── Card deck primitives — fan cards + sig-select overlay ───────────────────── // // Shared card display classes (.fan-card, .fan-card-corner, .fan-card-face, .fan-nav) // extracted from _game-kit.scss; sig-select overlay extracted from _room.scss. // ── Shared stage-card polarity rules ───────────────────────────────────────── // // Used by .sea-stage-card (Sea Select polarity is fixed by the deck-stack the // gamer drew from) and .fan-card[data-polarity="..."] (Game Kit fan, FLIP-able). // Sets title/qualifier color uniformly across both upright and reversal slots; // optionally inverts the card frame (bg ⇄ border) for levity polarity, and // applies an optional text-shadow (Sea uses one for the deeper card-art look; // Fan does not). @mixin stage-card-polarity($titles-color, $text-shadow: null, $invert-frame: false) { @if $invert-frame { background: rgba(var(--secUser), 1); border-color: rgba(var(--priUser), 1); } .fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .fan-card-reversal-name, .fan-card-reversal-qualifier { color: $titles-color; @if $text-shadow { text-shadow: $text-shadow; } } } // ── Shared stat-block contents ─────────────────────────────────────────────── // // Used by .sig-stat-block (Sig Select), .sea-stat-block (Sea Select), and // .fan-stage-block (Game Kit fan). The mixin emits the *inner* rules — stat-face // padding/swap, stat-keywords, sig-info tooltip + header/title/type/effect/index. // Each call site keeps its own outer rule (background, border, animation, button // positioning + class hooks, visibility triggers). @mixin stat-block-shared { // SPIN / FYI buttons — pinned to the top-right edge .spin-btn { position: absolute; top: -1rem; right: -1rem; margin: 0; z-index: 50; } .fyi-btn { position: absolute; top: 1.25rem; right: -1rem; margin: 0; z-index: 50; } // PRV / NXT — pinned to bottom corners; hidden by default. Sig + fan reveal // them on .fyi-open (see each site's outer rule). Sea overrides to always // visible (its FYI panel is permanent, not hover-toggled). .fyi-prev, .fyi-next { display: none; position: absolute; bottom: -1rem; margin: 0; z-index: 70; } .fyi-prev { left: -1rem; } .fyi-next { right: -1rem; } .stat-face { display: none; padding: calc(var(--sig-card-w, 120px) * 0.37) calc(var(--sig-card-w, 120px) * 0.1) calc(var(--sig-card-w, 120px) * 0.08); } .stat-face--upright { display: block; } &.is-reversed { 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 0 calc(var(--sig-card-w, 120px) * 0.07); // Sprint A.7-polish-3 — underline per user spec 2026-05-25 PM // (the original A.3 Q3 lock referred to underlined Emanation / // Reversal headers in the image-mode stat block; same spec re- // applied universally here so non-image-mode stat blocks get the // same visual treatment). text-decoration: underline; text-underline-offset: 0.15em; } // Sprint A.7.5 — `.stat-face-header` is the flex wrapper holding the // new top-left rank+suit chip inline w. the EMANATION/REVERSAL label. // Per [[project-image-based-deck-face-rendering]]'s A.3 Q3 spec the // chip is the chosen home for rank+suit on image-mode decks (where // the card itself has no corners). On text-mode decks the chip is a // benign duplicate of the corner rank+suit — won't bite until we // delete the text-mode rendering entirely. User-spec 2026-05-25 PM: // mirror the `.fan-card-corner` pattern (vertically stacked rank + // icon, --secUser, no bg/border) rather than the original bordered- // pill draft. The label sits inline-right of the chip, top-aligned // (chip is 2 rows tall, label is 1 — "vaguely inline" per spec). .stat-face-header { display: flex; align-items: flex-start; gap: calc(var(--sig-card-w, 120px) * 0.05); margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07); .stat-face-label { margin: 0; } // header owns the bottom margin now } .stat-face-chip { display: inline-flex; flex-direction: column; align-items: center; gap: calc(var(--sig-card-w, 120px) * 0.012); line-height: 1; color: rgba(var(--secUser), 1); font-weight: bold; .stat-chip-rank { font-size: calc(var(--sig-card-w, 120px) * 0.092); } i { font-size: calc(var(--sig-card-w, 120px) * 0.092); align-self: flex-start; } // Empty rank (JS-populated surfaces before first paint) — collapse // the rank line so the chip doesn't leave a stray empty row. .stat-chip-rank:empty { display: none; } } // 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 → --quiUser (cream) // Matches the contour-stroke color on the image-mode card so card + // title read as a coordinated pair. .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(--quiUser), 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. .tarot-fan-wrap[data-polarity="gravity"] .fan-stage-block { background: rgba(var(--secUser), 1); border-color: rgba(var(--priUser), 0.15); color: rgba(var(--priUser), 1); .stat-face-label { color: rgba(var(--quiUser), 1); } // Sprint A.7.5 — chip uses --secUser by default; under gravity the // stat-block bg IS --secUser, so the chip would be invisible. Flip // to --priUser to stay on the opposite-polarity side per the // [[feedback-card-polarity-convention]]. .stat-face-chip { color: rgba(var(--priUser), 1); } .stat-keywords li { color: rgba(var(--priUser), 1); border-bottom-color: rgba(var(--priUser), 0.18); } } .tarot-fan-wrap[data-polarity="levity"] .fan-stage-block { background: rgba(var(--priUser), 1); border-color: rgba(var(--terUser), 0.15); color: rgba(var(--secUser), 1); // Sprint A.7.5 — label drops to --secUser to match the new applet // convention. Was --terUser; --secUser still has comfortable contrast // against the --priUser bg + lets the title (--quaUser/--terUser per // arcana) stay the focal text. .stat-face-label { color: rgba(var(--secUser), 1); } .stat-keywords li { color: rgba(var(--quiUser), 1); border-bottom-color: rgba(var(--terUser), 0.18); } } .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 { position: absolute; z-index: 25; top: 50%; left: 50%; transform: translate(calc(-50% - var(--fan-stage-shift) - var(--fan-card-w) / 2 + 1.5rem), calc(-50% + var(--fan-card-h) / 2 - 1.5rem)); margin: 0; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; } // Reveal when the focused card OR the FLIP button itself is hovered. Without // the `.fan-flip-btn:hover` clause the button (z-index 25, sitting on top of // the card) steals :hover from the card the moment the cursor moves onto it, // flipping :has() false, fading the button to opacity:0 + pointer-events:none, // and letting the in-flight click pass through to the dialog backdrop (which // closes the modal). Keeping the button in the trigger list pins it visible // while the cursor is on it. .tarot-fan-wrap:has(.fan-card--active:hover) .fan-flip-btn, .tarot-fan-wrap:has(.fan-flip-btn:hover) .fan-flip-btn, .tarot-fan-wrap.fan-touch-revealed .fan-flip-btn { opacity: 1; pointer-events: auto; } .tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn { opacity: 0; pointer-events: none; } .fan-nav { position: absolute; z-index: 20; font-size: 3rem; line-height: 1; background: none; border: none; text-shadow: 0 0 1px rgba(0, 0, 0, 1); color: rgba(var(--terUser), 0.6); cursor: pointer; padding: 1rem; transition: color 0.15s; pointer-events: auto; outline: none; box-shadow: none; &:hover { color: rgba(var(--ninUser), 1); } // Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav &--prev { left: 1rem; } &--next { right: 1rem; } } // ─── Sig Select overlay (SIG_SELECT phase) ──────────────────────────────────── // // Two overlays (levity / gravity) run in parallel, one per polarity group. // Layout mirrors the gatekeeper: dark Gaussian backdrop + centred modal. // Inside the modal: upper stage (card preview) + lower mini card grid (no scroll). html:has(.sig-backdrop) { overflow: hidden; } .sig-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(5px); z-index: 100; pointer-events: none; } .sig-overlay { position: fixed; inset: 0; display: flex; align-items: stretch; justify-content: center; z-index: 120; pointer-events: none; } .sig-modal { pointer-events: auto; display: flex; flex-direction: column; width: 100%; // respects overlay padding-right set by JS max-width: 420px; max-height: 100%; // respects overlay padding-bottom set by JS } // ─── Stage ──────────────────────────────────────────────────────────────────── // flex: 1 — fills all space above the card grid; no background (backdrop blur). // Row layout: preview card bottom-left, stat block fills the right. // Card width is set by sizeSigCard() in room.js (smaller of 40% stage width or // 80% stage height × 5/8) via --sig-card-w CSS variable — libsass can't handle // container query units inside min(). .sig-stage { flex: 1; min-height: 0; position: relative; display: flex; flex-direction: row; align-items: flex-end; padding-left: 1.5rem; gap: 0.75rem; // Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height. .sig-stage-card { flex-shrink: 0; width: var(--sig-card-w, 120px); height: auto; aspect-ratio: 5 / 8; border-radius: 0.5rem; background: rgba(var(--priUser), 1); border: 0.15rem solid rgba(var(--secUser), 0.6); display: flex; flex-direction: column; position: relative; padding: 0.25rem; overflow: hidden; transition: transform 0.4s ease; // game-kit sets .fan-card-corner { position: absolute; top/left offsets } // so these just need display/font overrides; the corners land at the card edges. // All font-sizes scale with --sig-card-w (ratio = original-rem × 16 / 120). .fan-card-corner--tl { display: flex; flex-direction: column; align-items: center; line-height: 1.1; gap: 0.1rem; .fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.133); font-weight: 700; } i { font-size: calc(var(--sig-card-w, 120px) * 0.1); } } .fan-card-corner--br { display: flex; flex-direction: column; align-items: center; line-height: 1.1; gap: 0.1rem; .fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.12); font-weight: 700; } i { font-size: calc(var(--sig-card-w, 120px) * 0.1); } } .fan-card-face { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 0.25rem 0.15rem; gap: 0.2rem; .fan-card-face-upright { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; } .fan-card-face-reversal { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding-top: 0.1rem; } .fan-card-name-group { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.6; } // Upright qualifier + name share sizing/weight/color with their reversed counterparts. // text-wrap: balance distributes lines evenly so longer titles wrap symmetrically; // base size 0.08 (was 0.093) gives long titles room to fit without per-card hacks. .sig-qualifier-above, .sig-qualifier-below, .fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 120px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; } .fan-card-name, .fan-card-reversal-name { font-size: calc(var(--sig-card-w, 120px) * 0.08); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; text-wrap: balance; } .fan-card-arcana { font-size: calc(var(--sig-card-w, 120px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; } .fan-card-correspondence{ display: none; } // Minchiate equivalence shown in game-kit only // Reversed face elements — pre-rotated so they read forward after card spins .fan-card-reversal-qualifier, .fan-card-reversal-name { transform: rotate(180deg); opacity: 0.25; } } &.stage-card--reversed { transform: rotate(180deg); .fan-card-reversal-qualifier, .fan-card-reversal-name { opacity: 1; } .fan-card-name, .sig-qualifier-above, .sig-qualifier-below { opacity: 0.25; } } } // Stat block — same dimensions as the preview card (width × 5:8 aspect). // flex: 0 0 auto so it doesn't stretch to fill the stage; the rest of the // stage row is simply empty, giving the card room to breathe. .sig-stat-block { flex: 0 0 auto; width: var(--sig-card-w, 120px); height: calc(var(--sig-card-w, 120px) * 8 / 5); align-self: flex-end; background: rgba(var(--priUser), 0.5); border-radius: 0.4rem; border: 0.1rem solid rgba(var(--terUser), 0.15); display: none; position: relative; @include stat-block-shared; } &.sig-stage--frozen .sig-stat-block { display: block; } // Unified .fyi-open class — opens the FYI panel + reveals PRV/NXT nav. .sig-stat-block.fyi-open { .sig-info { display: flex; } .fyi-prev, .fyi-next { display: inline-flex; } } } // 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; } // FLIP was positioned via `left: calc(1.5rem + 0.4rem)` (default // rule below) assuming the card sat flush against the stage's // padded-left edge — true on the picker's left-anchored layout but // wrong here w. `justify-content: center` (the card moves to // wherever the group's left edge lands). // Re-derive FLIP's offsets from the centred geometry: // group width = card + gap + stat = 2 * --sig-card-w + 0.75rem // card's left edge (in stage) = (100% - group width) / 2 // card's bottom edge (in stage) = 50% - (cardHeight / 2) // = 50% - --sig-card-w * 0.8 // (cardHeight = w × 8/5 = w × 1.6) // The +0.4rem on each lands FLIP just inside the card's bottom-left // corner, matching the picker-side positioning intent. .my-sign-flip-btn { left: calc((100% - 2 * var(--sig-card-w) - 0.75rem) / 2 + 0.4rem); bottom: calc(50% - var(--sig-card-w) * 0.8 + 0.4rem); } // 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); } .my-sign-flip-btn { position: absolute; z-index: 25; bottom: 0.4rem; // .sig-stage has padding-left: 1.5rem; this offset places the btn just // inside the stage card's bottom-left corner (the card sits flex-end / // flex-start, anchored to the stage's left padding). left: calc(1.5rem + 0.4rem); margin: 0; display: none; } // FLIP btn appears only when the stage is frozen (post-OK confirm). Hover-only // previews don't reveal the polarity toggle — the user hasn't committed yet. .my-sign-stage.sig-stage--frozen .my-sign-flip-btn { display: inline-flex; } // Sprint A.5 — hide FLIP btn during the flip animation. `data-flipping="1"` // is set on .sig-stage-card by _flipPolarityAnimated (polarized) AND // _flipToBackAnimated (non-polarized) for the 500ms animation duration; CSS // :has() selects the parent .my-sign-stage when any child carries that attr // and zeros the btn so it doesn't visually interfere w. the rotateY mid-spin. // Mirrors the tarot-fan view's pattern (`_card-deck.scss:459` — // `.tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn`). .my-sign-stage:has(.sig-stage-card[data-flipping]) .my-sign-flip-btn { opacity: 0; pointer-events: none; } // ─── 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"] { // Stat block: invert priUser/secUser so gravity gets the same stark contrast as leavened cards .sig-stat-block { background: rgba(var(--secUser), 0.75); color: rgba(var(--priUser), 1); border-color: rgba(var(--priUser), 0.15); } // Caution tooltip: --tooltip-bg is black so priUser text (dark) would be invisible — // override to secUser (light) so body text reads against the dark backdrop. .sig-info { color: rgba(var(--secUser), 1); } // Polarity title + qualifier text: --terUser for gravity (paired w. levity's --quiUser). // All five selectors prefixed w. .sig-stage-card to meet the 0,4,0 specificity of the // default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule. .sig-stage-card .fan-card-name, .sig-stage-card .fan-card-reversal-name, .sig-stage-card .fan-card-reversal-qualifier, .sig-stage-card .sig-qualifier-above, .sig-stage-card .sig-qualifier-below { color: rgba(var(--terUser), 1); } // Stat-face label: gravity stat-block bg is --secUser (opposite of gravity card's // --priUser bg), so the label takes the levity-card text color (--quiUser) to // stay legible against the lighter stat-block. .sig-stat-block .stat-face-label { color: rgba(var(--quiUser), 1); } // Sprint A.7.5 — chip flips to --priUser under gravity (default --secUser // would be invisible on the --secUser stat-block bg). .sig-stat-block .stat-face-chip { color: rgba(var(--priUser), 1); } // Upright + reversal title glow — gravity .sig-stage-card .fan-card-name, .sig-stage-card .sig-qualifier-above, .sig-stage-card .sig-qualifier-below, .sig-stage-card .fan-card-reversal-name, .sig-stage-card .fan-card-reversal-qualifier { text-shadow: 1px 1px 0 rgba(0,0,0,1), 0 0 0.25rem rgba(var(--ninUser), 0.25); } // Cursor colours live in .sig-cursor-float[data-role] rules (portal elements) } // ── 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 } // ─── 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; .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