polish + bugfix session — wallet/Game Kit applet realign; my_sea label/shadow polish; DEL/FLIP state machine; sig-change cooldown loophole closure; sky-wheel planet shadow; Fiorentine additive numerals; kit-bag DOFF async refresh — TDD
End-of-session bundle 2026-05-26 covering ~10 distinct threads atop the A.7.5-polish-8 sky-wheel mini-portal commit (9cdd2cd). A.8 room.html sprint deferred per user — waiting on image scraping for RWS + future decks so the room can apply the image-mode pattern uniformly w.o. straddling text-mode fallback for unequippable Earthman Shabby Cardstock.
**(1) Game Kit + My Wallet applet realignment** — user spec "this isn't a place for tokens" / "only equippables should be there". Game Kit applet (/gameboard/, _applet-game-kit.html) drops the Free Token block — only PASS/BAND/CARTE/COIN trinkets + decks + dice remain. Free + Tithe tokens MOVED to the My Wallet applet on /dashboard/ (_applet-wallet.html rewrite). All trinkets COPIED into Wallet w. same .tt tooltip + DON/DOFF wiring so the user can equip from either surface. Stacked free/tithe icons (single icon per type) carry a .shop-badge ×N count (fa-coins for free, fa-piggy-bank for tithe — the latter standardized from outlier fa-hand-holding-dollar, now matching wallet / kit_bag / shop seed / FTs). Writs placeholder gets the same .token + .tt chrome ("Base currency unit ; Earned at the gate, spent in the shop"). 99+ cap on all badges. home_page view in apps/dashboard/views.py now passes pass/band/carte/coin + free/tithe tokens + counts + equipped_trinket_id. gameboard.js loaded on dashboard for the hover-portal tooltip system; #id_game_kit wrapper added (uses display: contents to stay transparent to the section-grid layout). Standalone game_kit.html page (_game_kit_sections.html) also reorganized — trinkets/tokens/decks each use bare .token icons w. centered flex row + 2rem gap, 1.5rem font-size to match gameboard sizing. id_game_kit outer wrapper data attrs (equipped-id, equipped-deck-id, in-use-deck-ids) feed buildMiniContent() for Equipped/Not Equipped/In-Use status.
**(2) My Sea label + shadow polish (my_sea.html Cross + applet)** — user spec "labels appear below and beneath the card, w. the card's shadow obscuring the very top of the label" per the GRAVITY/LEVITY .sea-stack-name pattern. .sea-pos-label repositioning: CROWN + COVER ABOVE slot (bottom: 100%; translate(-50%, -0.4rem)), LAY + CROSS BELOW slot (top: 100%; translate(-50%, 0.3rem)), LEAVE + LOOM increased breathing room (translate -0.4rem LEFT / 0.4rem RIGHT — was 0.1rem overlap). CROWN cell translateY(-0.5rem) UP + LAY cell translateY(0.5rem) DOWN for COVER/CROSS label breathing room. Filled-card downward shadow chain (1px 2px 0 black, 0 4px 0 black-faint, 2px 5px 5px black-blur) scoped to .my-sea-cross .sea-card-slot--filled only — empty dashed placeholders stay shadowless per user spec ("only the cards that replace [slots] should [have shadows]"). Four rotation-correction overrides for box-shadow rotating w. element transform: base (0deg), reversed (180deg sign-flip), cross (90deg matrix rotation → 2px -1px), cross+reversed (270deg → -2px 1px). Saved here for future reference since the matrix derivation is non-obvious: CSS rotate(θ) CW maps offset (a, b) → screen (a·cos θ − b·sin θ, a·sin θ + b·cos θ); solving for unrotated offsets that produce screen-down-right post-rotation gives the 4 chains. My Sea applet .my-sea-slot-label (z-index 0, margin-top 0.15rem) + .my-sea-slot--filled shadow + reversed-variant shadow inversion all mirror the page treatment.
**(3) DEL btn + FLIP btn state machine** — user spec: DEL un-disables as soon as ANY card drawn (was gated on hand_complete) ; FLIP btn .btn-disabled + text swap to × once hand complete. _setComplete(on) toggles FLIP btn class + label (parity w. DEL convention: × disabled / word active) ; new _setHasDrawn(on) helper extracted (was bundled in _setComplete). Wired into 4 transitions: (a) manual deposit _filled === 1, (b) initial page-load seed when _filled > 0, (c) AUTO DRAW path post-POST (CRITICAL FIX — was missing, only manual deposit synced DEL even though server already committed all cards on AUTO DRAW), (d) _resetHand spread-switch reset. Template DEL btn gates on saved_by_position (any draw); FLIP btn gates on hand_complete. Test test_partial_hand_del_btn_carries_btn_disabled inverted to test_partial_hand_del_btn_is_enabled per the new spec.
**(4) Sig-change MySeaDraw RESET (cooldown loophole closure)** — user-reported revenue-stream loophole 2026-05-26: switching sig used to re-open the FREE DRAW gate + forfeit any paid-draw credit, because apps/gameboard/views.py:266's `in_cooldown = active_draw is not None` keyed entirely off the MySeaDraw row's existence (NOT off User.last_free_draw_at, which is the cooldown TIMER but doesn't drive the in_cooldown decision). Initial draft DELETED the row on sig change — turned out too aggressive: lost both the cooldown anchor (created_at via the active_draw check) AND the paid-state fields (deposit_token_id, paid_through_at). FIX: save_sign on actual sig change `.update(hand=[], significator_id=new, significator_reversed=new)` — preserves cooldown + paid revenue, just resets the hand + sig snapshot. clear_sign left untouched (sig-cleared user can't draw anyway per my_sea_lock's no_significator guard; row sits dormant until re-pick routes through save_sign's reset). Guarded w. sig_changed so re-saving the same sig is a no-op. User.last_free_draw_at was always safe — User-level field, only ever set in my_sea_lock, never cleared (user confirmed the Brief shows 11:59pm consistently). Subtle architectural note for future: the in_cooldown decision being row-existence-based rather than timestamp-based is the load-bearing implicit dependency this loophole exposed; any refactor that delete()s the row needs to either flip in_cooldown to consult last_free_draw_at OR preserve the row as we did here.
**(5) Kit-bag DOFF async refresh** — user-reported 2026-05-26: deck disappears entirely from kit-bag on first DOFF; only manual page refresh restores the placeholder. Root cause: _syncKitBagDialog() in gameboard.js did card.querySelector('i') for the placeholder icon — worked for trinket/token cards (single FA <i>) but BROKE for image-equipped decks whose card-stack icon is <svg class="deck-stack-icon"> (no <i> to copy → empty placeholder div). DROP the client-side optimization, route both DOFF paths thru _refreshKitDialog() (symmetric w. DON). Single source of truth = server-rendered _kit_bag_panel.html's placeholder branch (re-renders _deck_stack_icon.html w.o. the deck arg for the empty-fill SVG).
**(6) Sky-wheel planet circle shadow** — user spec "tight 1px 1px black shadow at opacity 0.7 on planet circle groups in all sky locations". Base `filter: drop-shadow(1px 1px 0 rgba(0,0,0,0.7))` on .nw-planet-group so planet badges lift off the wheel rings on /dashboard/sky/ + My Sky applet + any future surface. Hover/active state chains shadow + glow ("drop-shadow ... ; drop-shadow(0 0 5px primary-lm)") since CSS filter REPLACES rather than APPENDS — shadow has to be re-stated on the hover rule to persist during interaction. Elements/signs/houses groups keep their glow-only hover (the request was planet-specific).
**(7) TarotCard suit_icon + Fiorentine additive numerals** — (a) suit_icon property pre-checks for major arcana trump 0 → fa-hat-cowboy-side (Fool/Nomad/Matto archetype) and trump 1 → fa-hat-wizard (Magician/Schizo/Bagatto archetype), pinned BEFORE the self.icon branch so even a deck seed supplying a different icon for these ranks normalizes to the convention. Earthman's seed already aligns; Minchiate (empty icon field) used to fall thru to fa-hand-dots. (b) _to_roman() adds _FIORENTINE_ADDITIVE_NUMERALS = {4:'IIII', 19:'XVIIII', 24:'XXIIII', 29:'XXVIIII', 34:'XXXIIII', 39:'XXXVIIII'} pre-check — locked-in 6-exception list per user-corrected spec (initial draft used universal additive form, user clarified "no, only these specific ones, e.g. trump 9 still prints IX + trump 14 still prints XIV per the actual Minchiate deck art"). +2 regression tests: additive overrides + non-overridden subtractive (9=IX, 14=XIV, 44=XLIV, 49=XLIX).
**(8) Gear menu NVM font fix** — _my_sea_gear.html's NVM btn changed from <a class="btn"> to <button onclick="location.href=..."> per [[feedback-btn-vs-anchor-font-family]] (anchor inherits body serif font; button stays sans-serif by browser default). Brief's NVM uses <button> + reads correctly — this matches it.
**(9) Image-mode slot transparency overrides** — 3 surfaces got `overflow: visible` (base overflow: hidden was clipping the contour-stroke filter chain) + transparent bg/border re-states for image-equipped Minchiate cards on (a) .my-sea-cross .sea-card-slot--filled + image variant, (b) .sig-stage-card.sea-sig-card.sig-stage-card--image base + levity-polarity nested override, (c) .sea-deck-stack--single .sea-stack-face:has(.sea-stack-face-img) (using :has() to key off the conditional back-img child). Followup to A.7.5-polish-* sprint — those surfaces' image-mode bg overrides didn't include overflow.
Tests: 1336/1336 IT+UT total green (was 1322 before the session). No FT runs per [[feedback-ft-run-discipline]]; visual verify ongoing by user across the session via Firefox reload.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -434,7 +434,10 @@ body.page-gameboard {
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
// 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
|
||||
@@ -443,22 +446,102 @@ body.page-gameboard {
|
||||
// cells need it added.
|
||||
.my-sea-cross .sea-crucifix-cell { position: relative; }
|
||||
|
||||
// Above top border — overlaps slot's top edge by 0.1rem (per the
|
||||
// `.sea-stack-name` "tuck under" treatment in _card-deck.scss:1564).
|
||||
// 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.1rem) scaleY(1.2);
|
||||
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.25 opacity matches the slot's faint dotted-outline at idle; the
|
||||
// 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 the
|
||||
// `.sea-pos-label` baseline 0.6, matching the slot's `--duoUser` mask
|
||||
// reveal.
|
||||
// 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;
|
||||
@@ -470,14 +553,6 @@ body.page-gameboard {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// Below bottom border — same `0.1rem` overlap but downward.
|
||||
.sea-pos-lay > .sea-pos-label,
|
||||
.sea-pos-cross > .sea-pos-label {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -0.1rem) scaleY(1.2);
|
||||
}
|
||||
|
||||
// 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-
|
||||
@@ -491,7 +566,11 @@ body.page-gameboard {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
writing-mode: vertical-rl;
|
||||
transform: translate(0.1rem, -50%) rotate(180deg) scaleX(1.2);
|
||||
// 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.
|
||||
@@ -500,7 +579,8 @@ body.page-gameboard {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
writing-mode: vertical-rl;
|
||||
transform: translate(-0.1rem, -50%) scaleX(1.2);
|
||||
// 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"
|
||||
@@ -808,10 +888,19 @@ body.page-gameboard {
|
||||
// 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-
|
||||
@@ -829,16 +918,21 @@ body.page-gameboard {
|
||||
border-width: 0.15rem !important;
|
||||
}
|
||||
|
||||
// Label — sibling of the slot inside the wrap, sits BELOW the slot
|
||||
// box (mirrors the my_sea.html picker's `.sea-pos-label` placement).
|
||||
// `margin-top: -0.15rem` crosses the slot's bottom border so the
|
||||
// label's top edge sits flush against it; `position: relative;
|
||||
// z-index: 2` keeps the label text rendering ATOP the slot's bottom
|
||||
// border (dotted for empty slots, solid for filled).
|
||||
// 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: 2;
|
||||
margin-top: -0.05rem;
|
||||
// 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;
|
||||
@@ -852,6 +946,32 @@ body.page-gameboard {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user