Files
python-tdd/src/static_src/scss/_gameboard.scss
Disco DeDisco 86a349b64e
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
wallet shop: free ($0) RWS + Fiorentine decks — FREE ITEM claim unlocks to Game Kit — TDD
- model: DeckVariant.free_in_shop flag (0015 schema); data migration 0016
  seeds RWS + Minchiate Fiorentine True (Earthman stays False — it's auto-
  granted at signup, not shopped)
- view: _free_decks_for decorates the free-in-shop catalog w. a per-user
  .owned flag; shop_claim_free POST endpoint adds the deck to unlocked_decks
  (idempotent M2M add) — the free_in_shop filter is the guard that stops the
  $0 endpoint unlocking paid/auto-granted decks (404 otherwise). free_decks
  wired into both the wallet view + toggle_wallet_applets HX context
- url: wallet/shop/claim (action, no trailing slash)
- template: free-deck tiles reuse the deck's own Game Kit tooltip prose
  (name / card-count / description / stock-version line) + a $0 .tt-price
  pinned top-right like paid tiles; .tt-micro carries .tt-free-btn (FREE
  ITEM) or the same .tt-already-owned pill once owned; reuses
  _deck_stack_icon.html
- js: wallet-shop.js _onFreeClick → _doClaimFree POSTs deck_slug → reload
  (server-rendered owned pill, same posture as the BUY reload). No guard
  portal — free = one-click. Rides the SAME delegated roots as BUY +
  idempotent wiring
- css: FREE ITEM wraps to 2 lines like BUY ITEM (extend the mini-portal
  .tt-buy-btn white-space:normal rule to .tt-free-btn); shop deck tiles get
  the Game Kit fan-out on hover/active by adding .shop-tile-deck to the
  .deck-stack-icon splay trigger list — DRY, no transform duplication
- tests: 8 ITs (shop_claim_free behaviors + free_decks context owned flag);
  FT claims RWS → 'Already owned' swap → id_kit_tarot_deck appears in Game
  Kit; 3 Jasmine specs F1-F3 (claim POST / no-guard / idempotent wiring);
  679 dashboard+epic green, no regressions
- trap: hover-hidden microtooltip btn → .text is '' under Selenium; read
  get_attribute('textContent') instead [[feedback-selenium-opacity-zero]]

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:51:21 -04:00

1108 lines
44 KiB
SCSS
Raw Blame History

This file contains ambiguous Unicode characters

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

// Aperture foundation (html/body/.container overflow + flex-column) lives
// universally in _base.scss. Gameboard's only divergence: `overflow: clip`
// on .container instead of `hidden` — `clip` prevents the seat tooltip
// scroll-anchoring quirk Firefox triggers under overflow:hidden. The
// `.row { margin-bottom: -1rem }` pull mirrors the billboard/dashboard
// h2-row tightening.
body.page-gameboard {
.container {
overflow: clip;
}
.row {
margin-bottom: -1rem;
}
}
.gameboard-page {
flex: 1;
min-width: 425px;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
}
@media (max-width: 550px) {
.gameboard-page {
min-width: 0;
overflow: hidden;
}
}
@media (min-width: 738px) {
.gameboard-page {
min-width: 666px;
}
body.page-gameboard .container {
overflow: visible;
}
}
@media (orientation: landscape) {
// Restore clip in landscape — overrides the >738px overflow:visible above,
// preventing the gameboard applets from bleeding into the footer sidebar.
body.page-gameboard .container {
overflow: clip;
}
// Reset the 666px min-width so gameboard-page shrinks to fit within the
// sidebar-bounded container rather than overflowing into the footer sidebar.
.gameboard-page {
min-width: 0;
}
}
#id_applet_game_kit {
display: flex;
flex-direction: column;
#id_game_kit {
flex: 1;
position: relative;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: space-evenly;
overflow-x: visible;
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
.token { position: static; }
.token:hover .token-tooltip,
.token:hover .tt { display: none; } // JS portal handles show/hide
.token,
.kit-item { font-size: 1.5rem; }
.kit-item { opacity: 0.6; }
}
}
// Sprint A.4 — card-deck stack icon (.deck-stack-icon) replaces the
// fa-regular fa-id-badge wherever a deck appears in icon form: gameboard's
// .token.deck-variant, kit-bag dialog's .kit-bag-deck, future room.html pile
// + deck-bag. Lifted out of the #id_applet_game_kit nest so the base sizing
// + rest-state SCSS applies in any deck-icon context. 3 stacked card-back
// rects, 5° CW rest tilt; see [[project-card-deck-icon]] for the design rules.
// When the deck has card-images, the rect fills are overridden inline w. an
// SVG <pattern> referencing the deck's <deck-slug>-back.png; otherwise the
// placeholder `fill: rgba(--priUser, 1)` shows through.
.deck-stack-icon {
display: inline-block;
// 2026-05-25 PM user spec: 1.5× the prior fa-id-badge visual weight
// since the icon is no longer constrained to placeholder-icon dimensions
// (now a meaningful first-class deck visualization).
width: 2.25rem; // 1.5rem × 1.5
height: 3.6rem; // 1.5× while preserving 5:8 tarot card aspect
color: rgba(var(--terUser), 1); // stroke color via currentColor
overflow: visible; // fan-out exceeds the viewBox bounds
filter: drop-shadow(0.08rem 0.08rem 0.15rem rgba(0, 0, 0, 0.6));
.deck-stack-icon__stack {
transform: rotate(5deg);
transform-origin: 50% 50%;
transform-box: fill-box;
transition: transform 0.25s ease;
}
.deck-stack-icon__card {
fill: rgba(var(--priUser), 1);
stroke: currentColor;
stroke-width: 1;
transform-origin: 50% 50%;
transform-box: fill-box;
transition: transform 0.25s ease;
}
// Rest: tightly stacked w. tiny vertical offsets (suggests stack
// depth without separating the cards visually).
.deck-stack-icon__card--1 { transform: translateY(-0.4px); }
.deck-stack-icon__card--3 { transform: translateY( 0.4px); }
}
// Sprint A.4 — fan-out trigger is wrapper-only (NOT self-triggered on the
// SVG itself) so placeholder-mode icons inside .kit-bag-placeholder stay
// static + dim like the empty dice slot. Real-deck wrappers (.token.deck-
// variant on gameboard, .kit-bag-deck in kit-bag dialog, future room.html
// pile + deck-bag) drive the splay; cards 2 + 3 fan out from under card 1,
// card 1 stays put. Tooltip portal is wired to the same `.token:hover` /
// `.kit-bag-deck:hover` triggers via JS so splay + tooltip-appearance
// co-activate.
//
// `.shop-tile-deck` (the wallet Shop applet's free-deck tiles) rides the
// SAME splay transforms — added to this trigger list rather than re-
// declaring the card-2/card-3 offsets, so the shop decks fan out on
// hover/active exactly like Game Kit.
.token.deck-variant:hover .deck-stack-icon,
.token.deck-variant:active .deck-stack-icon,
.token.deck-variant:focus .deck-stack-icon,
.kit-bag-deck:hover .deck-stack-icon,
.kit-bag-deck:active .deck-stack-icon,
.kit-bag-deck:focus .deck-stack-icon,
.shop-tile-deck:hover .deck-stack-icon,
.shop-tile-deck:active .deck-stack-icon,
.shop-tile-deck:focus .deck-stack-icon {
.deck-stack-icon__card--2 { transform: translate(-5px, -2px) rotate(-12deg); }
.deck-stack-icon__card--3 { transform: translate( 5px, -2px) rotate( 12deg); }
}
// Sprint A.4 — placeholder-mode dim styling. When the icon is inside a
// .kit-bag-placeholder (no deck equipped) or game_kit applet's empty-state
// .kit-item (no decks unlocked), color + fill drop to `--quaUser` at 0.3
// alpha to match the existing empty-slot treatment (`.kit-bag-placeholder`
// at `_game-kit.scss:143`, `.kit-item { opacity: 0.6 }` here). No animation
// since the wrapper isn't in the splay-trigger list above.
.kit-bag-placeholder .deck-stack-icon,
.kit-item .deck-stack-icon {
color: rgba(var(--quaUser), 0.15);
.deck-stack-icon__card { fill: rgba(var(--quiUser), 0.15); }
}
#id_applet_new_game {
display: flex;
flex-direction: column;
}
#id_applet_my_games {
display: flex;
flex-direction: column;
.applet-list {
flex: 1;
padding-top: 0.25rem;
}
}
#id_tooltip_portal {
position: fixed;
z-index: 9999;
padding: 0.75rem 1.5rem;
@extend %tt-token-fields;
.tt-equip-btns {
position: absolute;
left: -1rem;
top: -1rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
z-index: 1;
.btn { margin: 0; }
}
// Tray sig-card tooltip (Phase 2) — PRV / NXT btns pinned to the bottom
// corners of the portal, 1rem outside the panel so the btn centres land
// exactly on the corners. The shared @stat-block-shared mixin in
// _card-deck.scss already does this for fan / sig / sea contexts; the
// portal isn't covered by that mixin so we re-state the rules here.
.fyi-prev,
.fyi-next {
display: inline-flex;
position: absolute;
bottom: -1rem;
margin: 0;
z-index: 70;
}
.fyi-prev { left: -1rem; }
.fyi-next { right: -1rem; }
&.active { display: block; }
}
#id_mini_tooltip_portal {
position: fixed;
z-index: 9999;
// Polish-8 — bumped from font-size 0.8em → 0.95em + added padding to
// give the mini-portal a bit more presence per user-spec "a bit bigger
// both in dimensions and font-size". Visually closer to the main
// tooltip's text scale w/o approaching it — still clearly subordinate.
font-size: 0.95em;
font-style: italic;
padding: 0.35rem 0.75rem;
border-radius: 0.3rem;
width: fit-content;
white-space: nowrap;
text-align: right;
&.active { display: block; }
}
@media (max-height: 500px) {
body.page-gameboard {
.container {
.row {
padding: 0.25rem 0;
.col-lg-6 h2 {
margin-bottom: 0.5rem;
}
}
}
}
}
// ─── My Sea sign-gate ────────────────────────────────────────────────────────
// REMOVED 2026-05-22 — refactored to a Brief banner. The no-sig nudge now
// fires via `Brief.showBanner` from `_my_sea_sign_gate_brief.html`, which
// portals a `.note-banner.my-sea-sign-gate-brief` to the page h2 (gaussian-
// glass shell, FYI → /billboard/my-sign/, NVM dismisses). All the inline
// `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` styling
// dropped — `.note-banner` rules in `_note.scss:11` cover positioning,
// shell, + button placement DRYly.
// ─── My Sea DRAW SEA landing ─────────────────────────────────────────────────
// Sprint 5 iter 1 of [[project-my-sea-roadmap]]. When a user has a saved
// significator (gate passed), /gameboard/my-sea/ renders this landing
// screen: DRY table hex w. 6 chair seats labeled 1C-6C + central DRAW
// SEA btn. Mirrors my-sign's `.my-sign-page` + `.my-sign-landing`
// structure — same room-shell chain so room.js's scaleTable() can size
// the hex; same flex setup so the container chain propagates real
// height down for the scale calc.
.my-sea-page {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
position: relative;
}
.my-sea-landing {
flex: 1;
min-height: 0;
display: flex;
// FREE DRAW btn — centered in the hex, mirrors SCAN SIGN's 2-line
// font sizing so "FREE/DRAW" sits cleanly inside the 4rem circle.
#id_draw_sea_btn {
white-space: normal;
}
// Chair-position labels (1C-6C). Mirrors the room's `.seat-role-
// label` grid placement (col 2, row 1 by default; flips to col 1
// for left-side seats 3/4/5 so the label sits closest to the hex)
// but uses a role-free class name — my-sea is the solo draw flow,
// no role-pick phase, so the room's role-grammar doesn't apply.
.table-seat .seat-position-label {
grid-column: 2;
grid-row: 1;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.05em;
color: rgba(var(--secUser), 1);
}
.table-seat[data-slot="3"] .seat-position-label,
.table-seat[data-slot="4"] .seat-position-label,
.table-seat[data-slot="5"] .seat-position-label {
grid-column: 1;
}
// NOTE: the steady seated-chair look is owned by `_room.scss`'s
// `.table-seat.seated .fa-chair` (--secUser, no glow) — the one-shot
// --terUser/--ninUser flare is the transient `.seat-just-seated`
// animation (my-sea-seat-flare, 2s forwards). An earlier `.my-sea-landing`
// override here forced the seated chair to PERMANENT --terUser + glow,
// which snapped back after the flare settled (it out-specified the
// _room.scss settle) — removed 2026-05-29 so a seated chair eases in then
// rests at opaque --secUser as spec'd.
}
// Picker phase bg — `--duoUser` matches the table hex's interior so
// the landing→picker swap reads as a continuous surface (parallels
// `.my-sign-page[data-phase="picker"]` in _card-deck.scss line 704).
.my-sea-page[data-phase="picker"] {
background: rgba(var(--duoUser), 1);
}
// Landing phase bg — explicit `--priUser` revert per user spec
// (2026-05-20). The hex INTERIOR is `--duoUser` (set on `.table-hex`
// in _room.scss); the aperture AROUND the hex should be the default
// body color. Defensive override so any bf-cache / stale-CSS state
// can't leak the picker-phase green bg onto a landing render.
.my-sea-page[data-phase="landing"] {
background: rgba(var(--priUser), 1);
}
// Sprint 6 iter 6a — gatekeeper page bg + modal chrome. The page bg
// is uniform `--duoUser` (matches the hex interior on landing /
// picker so the visual transitions read as a continuous surface);
// the `.gate-overlay`/`.gate-modal` rules in `_room.scss` already
// give us the darkened Gaussian-glass modal centered over it. No hex
// or chair-seats on this page — the gatekeeper is a transient in-
// flight UI per user spec 2026-05-20.
.my-sea-page[data-phase="gate"] {
background: rgba(var(--duoUser), 1);
flex: 1;
min-height: 0;
}
// Spectator (bud-sea) draw wrapper — `display: contents` makes it layout-
// transparent so its `.my-sea-picker` child becomes a direct flex item of
// `.my-sea-page` and fills/centres EXACTLY like the owner's picker, with no
// separate visit-only sizing rules (DRY). VIEW DRAW toggles its display + the
// page's data-phase, so the cross rides the shared picker felt + layout.
.my-sea-visit-draw {
display: contents;
}
.my-sea-picker {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
// Portrait — stack the cross spread above the form col (mirrors the
// gameroom SEA SELECT modal's `@media (max-width: 600px)` stack
// pattern in `_card-deck.scss`). Landscape keeps the side-by-side
// layout since horizontal real-estate is the abundant axis there.
// User-spec 2026-05-20.
@media (orientation: portrait) {
flex-direction: column;
}
}
// .my-sea-cross renders all 6 surrounding positions (crown/leave/lay/
// loom + cover/cross overlaid on core) unconditionally. The SPREAD
// dropdown sets `data-spread="<name>"` on this element; per-spread
// rules below hide the positions each spread doesn't use. Inherits
// the 3×3 `grid-template-areas` from _card-deck.scss line 1189-1200
// so visible cells land in their canonical positions; hidden cells
// just leave their grid slots empty.
//
// Per-spread position subsets — user-locked 2026-05-19:
// PPF: leave (1) cover (2) loom (3) — horizontal middle row
// SAO: lay (1) cover (2) crown (3) — vertical center column
// MBS: crown (1) lay (2) loom (3) — T-shape (crown + lay vertical, loom right)
// DOS: loom (1) cross (2) crown (3) — loom right · cross overlay · crown above
// CC variants: all 6 positions (Waite-Smith / Escape Velocity differ in DRAW ORDER only,
// not in position visibility).
// Bump grid gap on my-sea (gameroom .sea-cross stays at 0.5rem since
// gameroom slots have no per-position labels). The vertical leave/loom
// labels need ~1.5rem of horizontal clearance from adjacent cells, and
// the horizontal crown/cover/lay/cross labels need ~1rem of vertical
// clearance so they don't overlap into the next row.
.my-sea-cross {
gap: 1rem !important;
}
.my-sea-cross[data-spread="past-present-future"] {
.sea-pos-crown,
.sea-pos-cross,
.sea-pos-lay { display: none; }
}
.my-sea-cross[data-spread="situation-action-outcome"] {
.sea-pos-leave,
.sea-pos-loom,
.sea-pos-cross { display: none; }
}
.my-sea-cross[data-spread="mind-body-spirit"] {
.sea-pos-leave,
.sea-pos-cover,
.sea-pos-cross { display: none; }
}
.my-sea-cross[data-spread="desire-obstacle-solution"] {
.sea-pos-leave,
.sea-pos-cover,
.sea-pos-lay { display: none; }
}
// Celtic Cross variants (waite-smith / escape-velocity) — all positions
// visible by default. No `display: none` overrides needed.
// Position-name caption — re-appropriates the GRAVITY/LEVITY
// `.sea-stack-name` typographic look (_card-deck.scss line 1557):
// small uppercase letter-spaced w. a subtle scaleY stretch,
// --terUser ink at 0.6 opacity. No polarity coloring — these are
// spread-position labels, not deck identifiers.
//
// Labels live OUTSIDE the .sea-card-slot (sibling, inside the crucifix
// cell or the cover/cross wrapper) so they survive SeaDeal._fillSlot's
// `slot.innerHTML = …` clobber on draw. Each label is absolute-
// positioned to nearly touch the slot's nearest border per the user-
// locked spec:
// crown / cover — above top border
// lay / cross — below bottom border
// leave — left of left border, rotated 90° CCW
// loom — right of right border, rotated 90° CW
.sea-pos-label {
font-size: 0.65rem;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 600;
opacity: 1;
color: rgba(var(--secUser), 1);
text-shadow: 0 0 0.25rem rgba(var(--priUser), 1);
text-align: center;
pointer-events: none;
white-space: nowrap;
position: absolute;
// z-index 0 (was 2) — labels sit BEHIND the slot so the slot's
// downward shadow can visually obscure the label's top edge per the
// .sea-stack-name "tuck under" treatment (user spec 2026-05-26).
z-index: 0;
}
// Cells need `position: relative` so absolute label children anchor
// to them. `.sea-pos-core` already has `position: relative` per the
// existing rule in _card-deck.scss line 1311; the other crucifix
// cells need it added.
.my-sea-cross .sea-crucifix-cell { position: relative; }
// CROWN + COVER labels — ABOVE the slot per user spec 2026-05-26.
// Cross convention: crown is at the top + cover is the central card
// being covered, so their labels belong above. `bottom: 100%` anchors
// label's bottom edge to slot's top; `-0.4rem` translate-Y pushes it
// further away so the labels read clearly w. breathing room.
.sea-pos-crown > .sea-pos-label,
.sea-pos-cover > .sea-pos-label {
bottom: 100%;
left: 50%;
transform: translate(-50%, -0.4rem) scaleY(1.2);
}
// LAY + CROSS labels — BELOW the slot. `0.3rem` translate-Y pushes each
// label a bit further from its slot's bottom edge than the prior tuck-
// under (which crowded the label up against the slot's border). The
// filled-card shadow (added below to `.sea-card-slot--filled`, NOT the
// empty slot per user clarification 2026-05-26) extends 0.25rem downward
// via the `$_sea-shadow` chain — covers the label's top edge w/o the
// label having to physically overlap the card's border-box.
.sea-pos-lay > .sea-pos-label,
.sea-pos-cross > .sea-pos-label {
top: 100%;
left: 50%;
transform: translate(-50%, 0.3rem) scaleY(1.2);
}
// Breathing room around COVER + CROSS labels — bump CROWN cell UP by
// 0.5rem + LAY cell DOWN by 0.5rem so the COVER label (below the central
// sig card) + CROSS label (also in the central row) have vertical space
// w/o colliding into the crown's slot from above or the lay's slot from
// below. Translate (not margin) so the surrounding grid layout doesn't
// reflow — cells stay in their grid-areas but visually shift.
.my-sea-cross .sea-pos-crown { transform: translateY(-0.5rem); }
.my-sea-cross .sea-pos-lay { transform: translateY( 0.5rem); }
// Filled-card downward shadow — only on the my-sea Cross page (NOT the
// picker or other surfaces using `.sea-card-slot`), and only the FILLED
// variant (the dashed empty slot stays shadowless per user clarification
// 2026-05-26: "the slots themselves should not have box-shadows ... only
// the cards that replace them should"). Mirrors `.sea-stack-face`'s
// `$_sea-shadow` chain in `_card-deck.scss:1976` (the GRAVITY/LEVITY deck
// stacks): solid-black 1px×2px offset shadow + a softer 4px-down spread +
// a 2px×5px blurred falloff. The image-mode slot still keeps its filter-
// chain contour-stroke + 1px depth shadow from `_card-deck.scss:837` — the
// box-shadow layers cleanly over it for a richer depth read.
.my-sea-cross .sea-card-slot--filled {
box-shadow:
1px 2px 0 rgba(0, 0, 0, 0.7),
0 4px 0 rgba(0, 0, 0, 0.18),
2px 5px 5px rgba(0, 0, 0, 0.5);
position: relative;
z-index: 1;
}
// Rotated-card shadow corrections — `box-shadow` rotates w. the element's
// `transform`, so any rotated card's down-right shadow rotates to a wrong
// direction. Each rotation case needs its offsets pre-inverted so the
// post-rotation render still reads down-right.
//
// Derivation: CSS rotate(θ) CW maps offset (a, b) → screen (a·cos θ
// b·sin θ, a·sin θ + b·cos θ). To get screen (1, 2) after rotation, solve
// for unrotated (a, b). Below shows the result for each rotation in play:
// • 180° (reversed, `.sea-card-slot--reversed` at _card-deck:1616):
// (1, 2) → (1, 2). Flip all signs.
// • 90° (cross, `.sea-pos-cross .sea-card-slot` at _card-deck:1705):
// (1, 2) → (2, 1). Swap + negate y.
// • 270° (cross + reversed at _card-deck:1620): (1, 2) → (2, 1).
//
// Specificity ladder: base filled (0,2,0) < reversed-only (0,3,0) = cross-
// only (0,3,0) < cross+reversed (0,4,0). For a card matching multiple, the
// most-specific rule wins; cross-only + reversed-only tie at (0,3,0) but
// only one matches per card (cross OR non-cross), so no source-order trap.
.my-sea-cross .sea-card-slot--filled.sea-card-slot--reversed {
box-shadow:
-1px -2px 0 rgba(0, 0, 0, 0.7),
0 -4px 0 rgba(0, 0, 0, 0.18),
-2px -5px 5px rgba(0, 0, 0, 0.5);
}
.my-sea-cross .sea-pos-cross .sea-card-slot--filled {
box-shadow:
2px -1px 0 rgba(0, 0, 0, 0.7),
4px 0 0 rgba(0, 0, 0, 0.18),
5px -2px 5px rgba(0, 0, 0, 0.5);
}
.my-sea-cross .sea-pos-cross .sea-card-slot--filled.sea-card-slot--reversed {
box-shadow:
-2px 1px 0 rgba(0, 0, 0, 0.7),
-4px 0 0 rgba(0, 0, 0, 0.18),
-5px 2px 5px rgba(0, 0, 0, 0.5);
}
// Cover + cross labels dim w. their slots — they sit on top of the
// sig card so a vivid label would compete w. the sig at idle. Default
// 0.5 opacity matches the slot's faint dotted-outline at idle; the
// parent's :hover state (propagated up when the inside `.sea-card-
// slot:hover` fires per CSS hover-ancestor rules) boosts to full.
.sea-pos-cover > .sea-pos-label,
.sea-pos-cross > .sea-pos-label {
opacity: 0.5;
transition: opacity 0.15s ease;
}
.sea-pos-cover:hover > .sea-pos-label,
.sea-pos-cross:hover > .sea-pos-label {
opacity: 1;
}
// Left of left border, rotated 90° CCW — text reads bottom-to-top.
// `writing-mode: vertical-rl` puts text top-to-bottom (CW); a 180°
// rotation flips it to read bottom-to-top (CCW), satisfying the user-
// locked "Leave: counterclockwise" spec.
//
// `scaleX(1.2)` (instead of the horizontal labels' scaleY) widens the
// character column (perpendicular to text-flow) — for vertical-rl
// labels, that's the visible "width" the user noticed had been lost
// at this angle. Without it, the rotated labels look squat.
.sea-pos-leave > .sea-pos-label {
right: 100%;
top: 50%;
writing-mode: vertical-rl;
// User spec 2026-05-26: parity w. CROWN/LAY's ~0.4rem breathing room.
// Was `0.1rem` overlap (tuck-under); now `-0.4rem` pulls the label
// further LEFT, AWAY from the slot's left edge so it reads cleanly w/o
// colliding into the slot's border-radius zone.
transform: translate(-0.3rem, -50%) rotate(180deg) scaleX(1.2);
}
// Right of right border, rotated 90° CW — text reads top-to-bottom.
// Native `writing-mode: vertical-rl` direction; no extra rotation.
.sea-pos-loom > .sea-pos-label {
left: 100%;
top: 50%;
writing-mode: vertical-rl;
// User spec 2026-05-26: same `0.4rem` AWAY distance as LEAVE (mirrored).
transform: translate(0.3rem, -50%) scaleX(1.2);
}
// Section dividers inside the SPREAD combobox — labels "3-card spreads"
// / "6-card spreads" separating the option groups. Styled to echo the
// `.kit-bag-label` treatment (small uppercase underlined letter-spaced
// --quaUser) but horizontal rather than vertical (kit-bag uses writing-
// mode: vertical-rl; this is a flat dropdown).
.sea-select-list .sea-select-divider {
font-size: 0.55rem;
text-transform: uppercase;
text-decoration: underline;
letter-spacing: 0.12em;
color: rgba(var(--quaUser), 0.75);
padding: 0.4rem 0.6rem 0.2rem;
pointer-events: none; // not selectable; combobox.js skips it
// (no role=option), but belt-and-braces
// against accidental hover/click styles.
list-style: none;
}
// Form col on my-sea — same DRY treatment as the gameroom sea-overlay
// `.sea-form-col` (handled in _card-deck.scss) but sits next to the
// picker's cross on a `--duoUser` page. Just constrain the width so it
// doesn't fight the cross for horizontal space.
.my-sea-form-col {
flex: 0 0 16rem;
max-width: 16rem;
// Portal the SPREAD dropdown out of `.sea-form-main`'s overflow
// clip — by default the gameroom's `.sea-form-main { overflow-y:
// auto }` (from _card-deck.scss:1424) keeps the modal contents
// scrollable, but for my-sea's much shorter form the dropdown gets
// clipped instead of overlaying the LOCK HAND / DEL btns below.
// Setting overflow visible here lets the absolute-positioned
// `.sea-select-list` extend past the form area + sit "above
// everything else" via its existing z-index: 100.
.sea-form-main {
overflow: visible;
}
// Portrait — split the form into two columns. LEFT carries the
// SPREAD field (label + reversal hint + combobox) above the action
// btns (AUTO DRAW / GATE VIEW + DEL); RIGHT carries the DECKS
// section (label + GRAVITY/LEVITY stacks) spanning both rows.
// Without this rearrange, on a phone-portrait viewport the stacked
// form col runs off the bottom of the viewport — DECKS lives above
// the action btns + everything below the fold (user-spec
// 2026-05-21). `.sea-form-main` uses `display: contents` so its
// `.sea-field` + `.sea-stacks` children act as direct grid items of
// the form col despite the intermediate wrapper in the DOM.
//
// Selector chains `.sea-form-col.my-sea-form-col` to win against
// `_card-deck.scss`'s base `.sea-form-col { display: flex; flex-
// direction: column }` (same 1-class specificity; card-deck loads
// AFTER gameboard in `core.scss`, so source order would otherwise
// overrule my-sea's grid). Chained 2-class selector pulls
// specificity ahead regardless of source order.
@media (orientation: portrait) {
&.sea-form-col {
flex: 0 0 auto;
max-width: none;
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-areas:
"field stacks"
"actions stacks";
column-gap: 1rem;
row-gap: 0.5rem;
align-items: start;
.sea-form-main { display: contents; }
.sea-field { grid-area: field; margin-bottom: 0; }
.sea-stacks { grid-area: stacks; margin: 0; justify-content: center; }
.sea-form-actions { grid-area: actions; align-self: end; padding-top: 0; }
}
}
// Bump the dropdown z-index well above the picker's stacking ints
// (cover z:3, cross z:4, modal stage z:9999 only opens on draw
// anyway). 1000 sits above any in-page layer the user might be
// interacting w. when they open the SPREAD picker.
.sea-select-list {
z-index: 1000;
}
// Portrait — open the SPREAD dropdown UPWARD instead of downward.
// The portrait form col sits at the bottom of the viewport (below
// the cross spread) w. the navbar/footer pinned beneath it, so the
// default `top: 100%` dropdown extends BELOW the visible aperture
// + the user can't scroll to it. Flipping to `bottom: 100%` makes
// the list grow upward into the abundant green aperture above.
// Chained `&.sea-form-col` to beat card-deck's later-loaded base
// (per [[feedback-scss-import-order-specificity]]).
@media (orientation: portrait) {
&.sea-form-col .sea-select .sea-select-list {
top: auto;
bottom: 100%;
margin: 0 0 0.2rem;
}
}
}
// LOCK HAND post-commit visual-lock: dim everything that mutates the
// hand. `.btn-disabled` is the project's existing soft-disabled
// treatment per [[feedback_btn_disabled_pointer_events]] — pointer-
// events:none + opacity reduction. The deck stacks aren't buttons
// themselves so we apply the class manually + the rule below ensures
// they stop responding to clicks.
.my-sea-picker--locked {
.sea-deck-stack.btn-disabled {
pointer-events: none;
opacity: 0.5;
cursor: default;
}
}
// SPREAD combobox lock — applied after the first deposit so the user
// can't switch spread mid-draw + scramble the in-progress hand's
// position-to-card mapping. DEL releases the lock by removing this
// class. Same `pointer-events: none` treatment as `.btn-disabled` per
// [[feedback_btn_disabled_pointer_events]].
.sea-select.sea-select--locked {
pointer-events: none;
opacity: 0.5;
cursor: default;
}
// Third hop of the first-draw glow handoff (see _burger.scss for the
// full chain). .sea-select gets a --terUser border + --ninUser glow but
// NOT the font-color change — its current spread text reads normally.
.sea-select.glow-handoff {
border-color: rgba(var(--terUser), 1);
box-shadow:
0 0 0.5rem 0.1rem rgba(var(--ninUser), 0.75),
0 0 1.2rem 0.3rem rgba(var(--ninUser), 0.35);
}
// ── My Sea spread modal (Phase 2 of the burger Sea sub-btn rollout) ──
//
// Holds the .sea-form-col chrome (spread combobox + AUTO DRAW + DEL).
// Hidden by default via the `hidden` attribute; JS removes the attr to
// open. Backdrop is click-to-dismiss, Escape also closes (handlers in
// the inline <script> at the bottom of my_sea.html).
.my-sea-spread-modal {
position: fixed;
inset: 0;
z-index: 320; // above burger (314), kit (318), bud (318), dialog (316)
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; // children re-enable
&[hidden] {
// `hidden` attr would already display:none — kept explicit so
// any later cascade still respects the closed state.
display: none;
}
&__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(0.25rem);
pointer-events: auto;
}
&__panel {
position: relative;
z-index: 1;
background: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 1);
border-radius: 0.75rem;
box-shadow:
0 0 1rem rgba(var(--secUser), 0.5),
0.15rem 0.15rem 0.5rem rgba(0, 0, 0, 0.5);
padding: 1.25rem;
pointer-events: auto;
max-width: 90vw;
.sea-form-col {
// Already-styled .sea-form-col container handles internal layout.
// Drop the fixed width inside the modal — modal panel sizes to
// content + the form is the content.
width: auto;
min-width: 16rem;
}
}
}
// ── Relocated deck-stacks (Phase 2) ──────────────────────────────────
//
// .sea-stacks was a child of .sea-form-col; now lives on the page in
// .my-sea-stacks-wrap so it stays visible when the spread modal is
// closed. Pin to the bottom-right of the aperture (above the bud +
// burger btns, below the modal).
.my-sea-stacks-wrap {
position: absolute;
bottom: 4.5rem; // clear of the bud/burger pair at bottom
right: 1rem;
z-index: 5; // above .my-sea-cross, below modal (320)
pointer-events: auto;
}
// Spectator (bud-sea) deck stack — the VISITOR's own deck, pinned TOP-LEFT
// (across the table from the owner, who deals bottom-right). FLIP renders
// disabled server-side (read-only). For a dubbodeck the Gravity/Levity
// `.sea-stack-name` flips ABOVE the face + upside-down, signalling that
// someone across the table is dealing. (user-spec 2026-05-29)
.my-sea-stacks-wrap.my-sea-stacks-wrap--visit {
top: 1rem;
left: 1rem;
bottom: auto;
right: auto;
// Name above the face (DOM order is face → name) + rotated 180°.
.sea-deck-stack { flex-direction: column-reverse; }
.sea-stack-name {
transform: scaleY(1.2) rotate(180deg);
transform-origin: center;
}
// FLIP reveal (hover-fade + click-persist) is now the SHARED `.sea-stack-ok`
// behaviour in _card-deck.scss — unified with the owner picker per user-spec
// 2026-05-30, so the visitor's prior hover-only override is gone. The
// spectator's click-persist (`.sea-deck-stack--active`) is wired in
// my_sea_visit.html (the read-only FLIP stays disabled throughout).
}
// ── Iter 4b: Brief banner + DEL guard portal ─────────────────────────────────
// Both reuse shared chrome: the Brief is `.note-banner` from note.js
// (portaled atop h2 w. Gaussian glass); the DEL guard is `#id_guard_portal`
// from base.html (the same one the room gear-menu DEL uses, positioned
// above the anchor button w. Gaussian glass + no backdrop). The picker IIFE
// invokes it via `window.showGuard(delBtn, "Are you sure?", confirmFn,
// null, {yesLabel: "DEL"})`. No my-sea-specific SCSS needed.
// ── My Sea applet (billboard-style gameboard applet) ─────────────────────────
// The applet at `_applet-my-sea.html` lists the active draw's slots in
// DRAW_ORDER — drawn cards filled + empty slots placeholder'd, each
// w. a label caption tucked tight against the slot's bottom edge.
// Horizontal-scroll mirrors the Palettes applet (`.palette` in
// `_palette-picker.scss:1`): row of fixed-size items + `overflow-x:
// auto`, so 6-card spreads scroll while 3-card spreads fit. Slots use
// the same `.sig-stage-card` layout language as the my_sign.html stage
// card (corner-tl + face w. name + corner-br) at applet scale —
// container queries on `.my-sea-scroll` lift `--slot-w` to fill the
// scroll's vertical aperture (minus label) so cards span the whole
// applet height per user spec 2026-05-22.
#id_applet_my_sea {
display: flex;
flex-direction: column;
// Anchor for #id_applet_sky_delete_btn's absolute centering.
position: relative;
background-color: rgba(var(--duoUser), 1) !important;
h2 {
flex-shrink: 0;
background-color: rgba(var(--priUser), 1);
box-shadow: rgba(0, 0, 0, 1) !important;
}
.my-sea-scroll {
flex: 1;
min-height: 0;
display: flex;
flex-direction: row;
align-items: stretch;
gap: 0.75rem;
padding: 0.25rem 0.5rem 0.5rem;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
container-type: size;
}
.my-sea-slot-wrap {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
scroll-snap-align: start;
height: 100%;
// No gap — the label sits directly against the slot's bottom
// border per user spec ("tighter [...] practically overlapping").
// Slight negative margin pulls the label baseline up into the
// slot's border line so the two visually merge.
}
// Slot shell — 5:8 card, sized to fill the wrap's height minus the
// label row. `--slot-w` resolves via container queries: 100cqi-cap
// when the scroll is wide-but-shallow, 100cqh*5/8 = 62.5cqh - tiny-
// label-reservation otherwise. The `- 1rem` carves out the label
// row + tight gap so the card doesn't overshoot the applet floor.
.my-sea-slot {
--slot-w: min(100cqi, calc((100cqh - 1rem) * 5 / 8));
width: var(--slot-w);
aspect-ratio: 5 / 8;
border-radius: 0.4rem;
border: 0.12rem solid rgba(var(--secUser), 0.6);
padding: 0.35rem;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
flex: 0 0 auto;
.fan-card-corner--tl,
.fan-card-corner--br {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.05;
gap: 0.05rem;
position: absolute;
.fan-corner-rank {
font-size: calc(var(--slot-w) * 0.16);
font-weight: 700;
}
i { font-size: calc(var(--slot-w) * 0.13); }
}
.fan-card-corner--tl { top: 0.25rem; left: 0.3rem; }
.fan-card-corner--br {
bottom: 0.25rem; right: 0.3rem;
transform: rotate(180deg);
}
// `gap: 0` so qualifier sits directly above the title at the
// title's own line-height (no flex gap between them); `.fan-card-
// arcana` carries its own margin-top to restore breathing room
// between title block and arcana label.
.fan-card-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
text-align: center;
padding: 0 0.2rem;
}
// Qualifier + title share the same typography (per `_card-deck.scss`
// convention at lines 568-572 / 1821-1823) — both bold, same size,
// same wrap, same line-height. Color inherits from the slot's
// polarity-driven `color:` (set on `--gravity` / `--levity`).
.fan-card-qualifier,
.fan-card-name {
margin: 0;
font-size: calc(var(--slot-w) * 0.105);
font-weight: 700;
line-height: 1.15;
text-wrap: balance;
}
.fan-card-qualifier:empty { display: none; }
.fan-card-arcana {
margin: calc(var(--slot-w) * 0.05) 0 0;
font-size: calc(var(--slot-w) * 0.07);
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.6;
}
}
// Filled slot polarity — mirrors `.sea-card-slot--gravity` / `--levity`
// in `_card-deck.scss:1332-1341`. Gravity = priUser bg + quiUser text;
// levity = inverted (secUser bg + priUser text). `.fan-card-name`,
// `.fan-card-qualifier`, `.fan-card-corner` + `.fan-card-arcana` all
// pin `color: inherit` so they pick up the slot's polarity color
// uniformly — the global `.fan-card-face .fan-card-name { color:
// --terUser }` rule in `_card-deck.scss:376-383` loads AFTER gameboard
// (per `core.scss` import order) and otherwise wins at matching 0,2,0
// specificity, stranding the title at --terUser while the qualifier
// inherits the slot color. Explicit `inherit` here at 0,3,0 beats it.
.my-sea-slot--filled.my-sea-slot--gravity {
background: rgba(var(--priUser), 1);
color: rgba(var(--quiUser), 1);
border-color: rgba(var(--secUser), 0.6);
.fan-card-corner { color: inherit; }
.fan-card-qualifier { color: inherit; }
.fan-card-name { color: inherit; }
.fan-card-arcana { color: inherit; opacity: 0.6; }
}
.my-sea-slot--filled.my-sea-slot--levity {
background: rgba(var(--secUser), 1);
color: rgba(var(--priUser), 1);
border-color: rgba(var(--priUser), 1);
.fan-card-corner { color: inherit; }
.fan-card-qualifier { color: inherit; }
.fan-card-name { color: inherit; }
.fan-card-arcana { color: inherit; opacity: 0.7; }
}
.my-sea-slot--filled.my-sea-slot--reversed { transform: rotate(180deg); }
// Sprint A.7 — image-mode slot override. `_card-deck.scss` imports
// after `_gameboard.scss`, but the polarity rules above (`.my-sea-slot--
// filled.my-sea-slot--gravity` / `--levity`) are nested inside
// `#id_applet_my_sea` (specificity 1,2,0) and beat the top-level shared
// `.my-sea-slot.my-sea-slot--image` rule (0,2,0) on bg + border + color.
// Re-state the transparency here at matching nested specificity so the
// PNG card-back is unobstructed. Filter-chain / contour-stroke / depth
// shadow on `.sig-stage-card-img` still come from the shared rule (no
// collision — different selector target).
//
// `overflow: visible` is critical — the base `.my-sea-slot` rule above
// sets `overflow: hidden` at (1,1,0) which BEATS the shared image-mode
// rule's `overflow: visible` at (0,2,0) on the ID axis. Without re-
// stating here, the fourfold contour-stroke drop-shadows get clipped
// by the slot bounding box → reads as a uniform rectangular frame
// around the image, defeating the "stroke follows alpha contour"
// illusion. See [[feedback-scss-id-context-specificity-trap]].
.my-sea-slot--filled.my-sea-slot--image {
background: transparent;
border: 0;
padding: 0;
overflow: visible;
}
// Empty slot — matches the my_sea.html picker's empty `.sea-card-
// slot` style (`_card-deck.scss:1299-1303`): 0.15rem DASHED border in
// --terUser at full opacity, --duoUser fill. Same width + dash
// frequency as the picker so the applet reads as a true "miniature"
// of the picker rather than a cousin w. different dotting cadence.
// `!important` on the three border properties: the base `.my-sea-
// slot` border shorthand sits at the same (1,1,0) specificity, so
// belt-and-suspenders the override.
.my-sea-slot--empty {
background-color: rgba(var(--duoUser), 1);
border-style: dashed !important;
border-color: rgba(var(--terUser), 1) !important;
border-width: 0.15rem !important;
}
// Label — sibling of the slot inside the wrap, sits BELOW the slot.
// User spec 2026-05-26: parity w. my_sea.html's `.sea-pos-lay` label
// treatment — label tucked just below the slot w. the filled card's
// downward shadow (added below to `.my-sea-slot--filled`) obscuring
// the label's top edge. Empty dashed slots stay shadowless so the
// label sits cleanly below them.
.my-sea-slot-label {
position: relative;
// z-index 0 (was 2) — slot's shadow lives on top + casts over the
// label's top edge; the label is behind the slot in stacking order.
z-index: 0;
// Small gap (was `-0.05rem` flush margin) so the label doesn't
// glue to the slot's bottom border on the empty-slot case where
// there's no shadow to bridge the visual gap.
margin-top: 0.15rem;
padding: 0 0.2rem;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(var(--secUser), 0.85);
text-shadow: 0 0 0.25rem rgba(var(--priUser), 1);
text-align: center;
white-space: nowrap;
line-height: 1.1;
transform: scaleY(1.3);
transform-origin: top center;
}
// Filled-card downward shadow — applet parity w. my_sea.html's
// `.my-sea-cross .sea-card-slot--filled` rule. Empty dashed slots
// stay shadowless (per user clarification 2026-05-26: only the cards
// that replace the slots should carry a shadow).
.my-sea-slot--filled {
box-shadow:
1px 2px 0 rgba(0, 0, 0, 0.7),
0 4px 0 rgba(0, 0, 0, 0.18),
2px 5px 5px rgba(0, 0, 0, 0.5);
position: relative;
z-index: 1;
}
// Reversed-card shadow inversion — parity w. my_sea.html's
// `.sea-card-slot--reversed` override below. The applet's `.my-sea-
// slot--reversed` (line 843) also adds `rotate(180deg)` which flips the
// shadow up-left; invert ALL offsets so post-rotation it reads down-
// right. See [[feedback-css-transform-rotates-box-shadow]] (or similar
// future-note) for the gotcha.
.my-sea-slot--filled.my-sea-slot--reversed {
box-shadow:
-1px -2px 0 rgba(0, 0, 0, 0.7),
0 -4px 0 rgba(0, 0, 0, 0.18),
-2px -5px 5px rgba(0, 0, 0, 0.5);
}
// `.my-sea-slot-label--empty` intentionally has NO per-state recolor
// — the empty-state label keeps the same `--secUser` ink as the
// filled-slot label per user spec 2026-05-22 (pins position identity
// Cover/Cross/etc. across the row regardless of fill state).
// Previously dimmed to --terUser to echo the dashed border tone —
// but that broke title cohesion when most slots were empty.
// No-draws empty state — centred italic, mirrors the Brief / applet-
// list-entry--empty pattern in `_billboard.scss:29-38`.
.my-sea-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-style: italic;
opacity: 0.6;
margin: 0;
}
}