Commit Graph

837 Commits

Author SHA1 Message Date
Disco DeDisco
97d5522807 role select scroll log: less robotic phrasing + role code — 'assumes Nth Chair, where SUBJ will start the game as the Role [XC]' — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Was 'assumes Nth Chair; SUBJ will start the game as the Role.' Now joins with ', where' and appends the role code in a no-wrap bracket (the Player [PC], the Builder [BC]…), matching the SkyDrive/Sea abbrev treatment. Chair stays capitalised (user-confirmed). 56 drama ITs green; the billscroll FT's 'assumes 1st Chair' assertion still holds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:19:27 -04:00
Disco DeDisco
b7f943cd38 palette: brighten --secGn to 0,200,100 (re-spread the green ramp); position-status .fa-ban back to --priRd, .fa-circle-check on the new --secGn
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:08:07 -04:00
Disco DeDisco
c683f02676 atlas: tab the merged post-line username off its text (min-width 4rem, like the POST view) so it reads distinctly from a provenance log's running prose
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:02:35 -04:00
Disco DeDisco
031658b80b scroll: keep the fable "'s character" stub BOLD (still --secUser, not --quaUser) — only the username pops
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 23:00:00 -04:00
Disco DeDisco
cadfc5e864 scroll provenance: SkyDrive prose + element abbrevs; Fable tag + character-actor stub; --quaUser usernames; no-wrap abbrevs — TDD
A batch of character-creation provenance polish (user-spec 2026-06-09):

- SKY_SAVED prose: 'beholds the skyscape of POSS birth, which yields OBJ a unique X capacity' -> 'fires up the SkyDrive to observe that POSS cosmic origins yield OBJ a unique X [Xc] capacity' — each capacitor now carries its short code (Ossum [Om], etc.) from a new epic.utils.CAPACITOR_ABBREV (mirrors ELEMENT_INFO.abbr in sky-wheel.js). SEA_DRAWN 'Sea of cards' -> 'Sea of Cards'; the AC clause 'sinister' -> 'offhand connections'.

- Fable tag: a new GameEvent.is_fable (SIG_READY / SKY_SAVED / SEA_DRAWN) makes the shared _scroll.html render data-label='fable' (not 'frame'; struck still wins -> 'redact') + prefix '@handle's character' — a stub for the character's name once set. Both scroll gear filters (room + billscroll) gain a Fable checkbox so fable rows stay filterable.

- Usernames: the scroll @handle <strong> is now bold --quaUser (breaks up the --secUser monotony, mirroring .atlas-row-who); the fable "'s character" stub sits OUTSIDE the strong so it stays --secUser. Mirrored into the ATLAS via .atlas-row-body strong (provenance rows embed the whole SCROLL body).

- No-wrap: card parentheticals (Q [icon]) + element brackets [Om] ride white-space:nowrap spans so an abbreviation never splits across a line wrap. Shared _card_abbrev helper for SIG_READY + SEA_DRAWN.

TDD: drama prose tests reworked (SkyDrive + bracket codes, no-wrap spans, Sea of Cards, offhand); is_fable + scroll-render ITs (fable->fable label + character stub; non-fable->frame; struck fable->redact). 70 drama + 918 epic+billboard ITs green. Existing billscroll/room-scroll filter FTs unaffected (their frame events are ROLE_SELECTED/deposits, their redact are struck SIG_READY).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:54:38 -04:00
Disco DeDisco
a6db8c628f sea affinity prose: reinsert the (rank icon) parenthetical + pluralize the NC verb (they/yo leave, he/she/it leaves) — TDD
Two user follow-ups to the personalized affinity prose: (1) the corner-rank + suit-icon abbrev rides the card name again (e.g. 'the Queen of Pentacles (Q <i class=fa-crown></i>)'), mirroring SIG_READY — majors render just the numeral, e.g. '(I)'. (2) The NC clause conjugates the subject verb: they + yo are treated as PLURAL -> 'they leave' / 'yo leave behind'; the singular he/she/it keep the 3rd-person 'leaves'.

TDD: per-role-clauses test updated to 'they leave'; new yo-pluralizes + he-keeps-leaves cases; a with-abbrev PC case asserting the full rank+icon parenthetical. 51 drama+epic ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:23:19 -04:00
Disco DeDisco
b0d153ebc1 sea affinity scroll log: personalized per-Role prose with pronouns (draws POSS Sea of cards, where the CARD ...) — TDD
Replaces the generic 'finding affinity with the X in the Crown' SEA_DRAWN prose with a role-specific clause per user-spec 2026-06-09 — the @handle is template-prepended, pronouns (subj/poss) resolve via the actor:

  PC: draws POSS Sea of cards, where the CARD crowns all POSS loftiest illusions. NC: ...the CARD traces all the narratives SUBJ leaves behind. EC: ...always looms before POSS calling. SC: ...covers all POSS righteous conduct. AC: ...always crosses POSS sinister connections. BC: ...lays all POSS foundational work.

to_prose branches on data['role']; 'The ' still stripped so a qualifier butts the proper name; an unknown role falls back to a generic 'marks POSS affinity' clause. The stored position_label/corner_rank/suit_icon are now unused by the prose but left in the event data (harmless). TDD: drama prose tests reworked (per-role clauses + bawlmorese pronoun substitution) + the epic affinity-prose IT updated to the PC crown clause. 49 drama+epic ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:08:31 -04:00
Disco DeDisco
144ec78b1f two-browser sig FTs: read --priYl off :root live instead of a hardcoded rgb(255,207,52) — palette-tuning-resilient (build 377 fix)
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Both SigSelectChannelsTest cursor/reservation FTs hardcoded the OLD --priYl = rgb(255,207,52); the palette tuning moved --priYl to 255,227,82, so the rendered NC role colour no longer matched and both failed in the test-two-browser-FTs stage (build 377). Now each reads --priYl off document.documentElement at run time and compares whitespace-stripped (so 255,227,82 matches rgb(255,227,82) and the rgb()/rgba() box-shadow forms alike) — future palette tweaks to --priYl won't re-break them. Docstrings de-hardcoded too. Couldn't run the channels/two-browser stage locally (needs Redis + dual Firefox); verified --priYl is a :root base var + py_compile clean; the assertion now tracks whatever --priYl renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:56:45 -04:00
Disco DeDisco
35d05a6490 drama: migration for the GameEvent.verb choices (SEA_DRAWN + SEA_RELINQUISHED) — unblocks the CI makemigrations --check
The Sea Select Scroll-log sprint (d28046f) added two verbs to GameEvent.VERB_CHOICES but not the AlterField migration Django generates for a choices change, so the pipeline's makemigrations --check failed early. 0005_alter_gameevent_verb regenerates it; applies clean. No data change (choices is validation-only).

- bundled (parallel work): rootvars.scss ongoing palette tuning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:34:27 -04:00
Disco DeDisco
4aee5016c1 dubbodeck per-card FLIP back: the sig stage FLIPs each card to its OWN deck's back, not the seat's — finishes the cross-deck story — TDD
Pairs with the dubbodeck assembly: a mixed pile can now hold cards from up to three decks per polarity, so the single server-rendered back (the viewer's seat deck) was wrong for any cross-deck card. Now per-card: each .sig-card thumbnail carries data-back-image-url (its OWN deck back, only for non-polarized image decks; empty otherwise); stage-card.js fromDataset reads back_image_url + _setImageMode repoints the stage .sig-stage-card-back-img at the FOCUSED card's back and shows/hides the FLIP affordance per card (a backless card in the pile hides FLIP + drops any flipped state).

- _sig_select_overlay.html: the stage back-img + FLIP now render on sig_pile_has_backs (ANY pile card is a non-polarized image deck) instead of the viewer's-seat-deck gate, so an RWS grails/blades card in a PC-earthman viewer's pile can FLIP. Initial src dropped (JS sets it on first focus). epic/views.py computes the flag where sig_cards is set; _court_cards/_major_cards gained select_related(deck_variant) to keep the per-card deck access N+1-free.

TDD: 3 ITs in SigSelectUnifiedStageTest (non-polarized image deck -> per-card /static back url + sig_pile_has_backs + stage back-img/FLIP rendered; polarized deck -> empty back + no FLIP element; text-only deck -> empty back). 562 epic view+model ITs green. The JS repoint/hide is the stage-Jasmine debt (deferred) — manually verified.

- bundled (parallel work): rootvars.scss ongoing palette tuning.

[[project-deck-segment-model]] [[project-image-based-deck-face-rendering]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:33:19 -04:00
Disco DeDisco
034639d335 game room voice: gate on >1 cost-current depositor over the 7d initial period (not grace), not the phase — solo/CARTE-all-6 stays off — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Refines the room voice activation per user-spec: voice mirrors my_sea's window but over the seat's 7d INITIAL cost period (GateSlot.cost_current — within [filled_at, filled_at+7d), NOT the renewal grace). voice_active now needs the gate CLOSED (table_status set = ROLE_SELECT onset) + the viewer seated + MORE THAN ONE distinct gamer holding a FILLED, cost-current seat. A sole depositor — including a CARTE owner occupying all 6 seats (one gamer) — keeps voice OFF (no one to talk to); a 2nd qualifying gamer flips it on; it toggles back off once the cost period lapses into grace. Replaces the earlier phase-set gate (ROLE/SIG/SKY only) — voice now spans the whole 7d window incl. IN_GAME (the phase ceiling at SEA_SELECT made the 7d expiry unreachable).

TDD: RoomVoiceActivationTest reworked (13 ITs) — active w. 6 depositors across ROLE/SIG/SKY + persists in IN_GAME; inactive for a single depositor (CARTE-all-6), flips on once a 2nd gamer qualifies, inactive when filled 8d ago (grace), inactive before the gate closes, inactive for a non-seated viewer; room-id / muted-at passthrough + active-btn markup unchanged. 736 epic+drama ITs green.

[[project-my-sea-invite-voice-blueprint]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:53:59 -04:00
Disco DeDisco
b4ffab186e dubbodeck: assemble each sig pile per-segment from the contributing seat's own deck (cross-deck), not one shared deck — TDD
Each polarity's 18-card sig pile now assembles from THREE seats by segment, each FROM THAT SEAT'S OWN deck: CROWNS/BRANDS courts (8) from the cb role's deck, GRAILS/BLADES courts (8) from the gb role's, majors 0/1 (2) from the tr role's — levity cb/gb/tr = PC/SC/NC, gravity = BC/AC/EC. So an RWS King of Grails (SC seat) can sit beside a Minchiate Queen of Wands (PC seat) in one levity pile, each carrying its own deck_variant -> per-card face/back art, NO schema change (cards already carry deck_variant).

- models.py: new _POLARITY_SEGMENT_ROLES + _seat_deck_for_role + _court_cards + _major_cards + _polarity_sig_cards. A missing seat/deck falls back to _room_deck_variant, so a single-deck (or CARTE-solo) room assembles the IDENTICAL 18-card pile it always did (16 courts + 2 majors). sig_deck_cards is now the UNION of both polarity piles (note-unfiltered) -> select_sig's pick validation (views.py:1664) accepts a card from EITHER deck/polarity with no view change. Dropped the now-dead _sig_unique_cards; _sig_unique_cards_for_deck stays for personal_sig_cards (my_sign, single equipped deck).

- TDD: DubbodeckAssemblyTest (4 ITs) — levity grails/blades come from the SC seat's distinct deck while crowns/brands stay earthman; the gravity pile is unaffected by a levity-seat deck; the validation set spans both decks; CARTE-solo one-deck feeds BOTH polarity piles sharing pks (no cross-polarity dedup). Existing SigDeckCompositionTest (36/16/16/4) + SigCardHelperTest (single-deck counts, note unlocks, share-pks, empty fallback) green — single-deck behavior preserved. 736 epic+drama ITs green.

- bundled (parallel work): rootvars.scss ongoing palette tuning.

[[project-deck-segment-model]] [[project-image-based-deck-face-rendering]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:53:45 -04:00
Disco DeDisco
6f1729010f billscroll FT: backdate the 'recent' event 3h into the clock-time bucket — fixes test_recent_event_shows_time_format after the relative_ts <60min change
The shared relative_ts gained a <60min -> 'N min' bucket (a02f347), so a just-now auto_now_add event renders 'N min', not the g:i a clock time BillscrollEntryLayoutTest.test_recent_event_shows_time_format asserts -> red locally + in CI from that commit on. Backdate the 'recent' event 3h (still recent vs. the >1yr old event, but >60min) so it renders clock time, mirroring the existing old-event backdate. Data-only change; the layout/columns test is age-agnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:53:27 -04:00
Disco DeDisco
2d4a2c5b5c post view: bottom-anchor the post-line thread (flex column + justify-content: flex-end) so short threads sit above the composer instead of the header
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:39:11 -04:00
Disco DeDisco
203596ee29 post view: restore the post-line grid layout (author | text | time, bordered + rounded) minus the --priUser fill & box-shadow — outlined rows, not filled pills
Follow-up to the reelhouse recolor: the POST thread keeps its earlier per-line grid (author / text / time columns, 0.1rem --secUser border at the .form-control radius, margin/padding) but drops the background-color + box-shadow per user-spec, so the lines read as outlined rows on the plain wash. The filled-pill chrome remains salvaged in YARN.

- bundled (parallel work): rootvars.scss ongoing palette tuning (--terPer + neighbouring slots).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:38:35 -04:00
Disco DeDisco
d28046f3da sea select scroll log: publish a Role<->Celtic-position affinity on the completing draw; redact + relinquish on DEL; re-publish on re-draw — TDD
The gameroom DRAW SEA phase now writes drama provenance, mirroring sig/sky. When a gamer's 6-card Celtic Cross COMPLETES, a SEA_DRAWN Scroll log publishes their affinity with the card sitting in their Role-correlated position.

- epic/views.py: ROLE_POSITION_MAP — the user's sixfold index (PC->crown, NC->leave, EC->loom, SC->cover, AC->cross, BC->lay; roles rotate each round, so a seat's CURRENT role drives it) + SEA_POSITION_LABELS (each spread's display label for a position KEY; Waite-Smith's Behind/Before/Beneath + Escape-Velocity's Leave/Loom/Lay both key to the same index). sea_save publishes SEA_DRAWN on the <6->6 completing transition only (a reload that re-POSTs the full hand can't double-publish); a re-draw first redacts the standing relinquishment, then publishes anew. sea_delete redacts the published affinity (the strikethrough) + records SEA_RELINQUISHED in its wake (the redact-pair). _sea_affinity_for mirrors SIG_READY's polarity-qualified name_title + corner abbrev; _redact_standing_sea_event tests 'not retracted' in PYTHON (the SQLite JSONField exclude-NULL trap).

- drama/models.py: SEA_DRAWN + SEA_RELINQUISHED verbs + to_prose ('draws {poss} Celtic Cross, finding affinity with the {card}{abbrev} in the {Position}.' / 'relinquishes {poss} affinity with the {card}.'); 'The ' stripped so a levity/gravity qualifier butts the proper name. The generic struck/retracted property renders the strikethrough + data-label=redact in _scroll.html unchanged.

TDD: 4 drama prose ITs (affinity statement, spread-label passthrough, relinquishment, struck-when-retracted) + 7 epic ITs (publish-on-complete, position=crown for PC, none-before-complete, no-double-publish-on-reload, DEL redacts+relinquishes, re-draw redacts-the-relinquishment+republishes, DEL-noop-when-nothing-published). 459 drama + epic-view ITs green.

- bundled (parallel work): rootvars.scss --sixUser/--sepUser/--octUser slot reassignments across the forest/khaki/blade palettes (tuning the new reelhouse h2 bgs) + a new --terMrb.

[[project-sea-select-scroll-provenance]] [[feedback-jsonfield-exclude-sqlite-null]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:35:45 -04:00
Disco DeDisco
039152a787 game room voice: light the burger voice btn for seated gamers across ROLE/SIG/SKY_SELECT; keep POST's composer inline (OK beside the input) — TDD
Voice (Phase C, room path) — the my_sea voice mesh now extends to the epic game room. room_view sets voice_active for a SEATED gamer (a TableSeat with their gamer) while table_status is in {ROLE_SELECT, SIG_SELECT, SKY_SELECT} — from ROLE_SELECT onset (all tokens committed, seats pre-created by pick_roles) continuously through SKY_SELECT, which hosts the in-page DRAW SEA felt; dark at IN_GAME. voice_room_id = the room UUID (the WebRTC mesh key), voice_muted_at = the persisted mute so an in-arc nav/refresh rejoins muted (mirrors my_sea). The burger fan's shared #id_voice_btn lights .active + carries data-room-id; room.html now includes voice-glow.js (the glow/pulse machine, coexisting w. the sea-btn glow handoff).

No consumer/JS change needed: RoomVoiceConsumer._can_join already gates an epic room on TableSeat membership, the ws/voice/<str:room_id> route serves both mysea-… + bare-UUID keys, and burger-btn.js/voice-mesh.js read data-room-id generically. TDD: RoomVoiceActivationTest (9 ITs — active across the arc, dark at IN_GAME, dark for a non-seated viewer, room-id = UUID, muted-at passthrough) + RoomVoiceConsumerEpicGateTest (2 channels ITs — seated gamer admitted + receives welcome; non-seated refused). 390 epic-view + 11 voice channels ITs green.

- _room.scss: POST composer kept as a flex row — the OK btn sits inline beside the 'Enter a post line' input (the felt/pill chrome stays salvaged in YARN, but the input<->OK row layout is retained; follow-up to 577ef30).

- bundled (parallel work): _room.scss reelhouse h2 font --priUser -> --secUser (SCROLL/POST/PULSE); rootvars.scss chroma-hue primaries brightened (yellow/lime/cyan/indigo/violet/fuschia/magenta).

[[project-my-sea-invite-voice-blueprint]] [[project-character-creation-spec]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:08:38 -04:00
Disco DeDisco
577ef30f5c room reelhouse: recolor per-view h2s; salvage POST's pill styling into YARN; revert POST to plain wash; colour-code ATLAS row accents; drop letter-spacing on the Sea of Cards italic 'of'
- _room.scss: SCROLL h2 --priUser/--sixUser, POST h2 --priUser/--sepUser, PULSE h2 --priUser/--octUser. New .room-view--yarn block = verbatim salvage of POST's --duoUser green-felt + input-pill styling (.post-* renamed .yarn-*, #id_post_table -> #id_yarn_table), ready for when YARN's backing model + markup land. POST reverts to the plain dark %applet-box wash — only its h2 override + a bare-<ul> reset on #id_post_table (no felt/pills/bullets) remain. ATLAS per-source row accents colour-coded to echo the source h2 bgs: [data-source=provenance] -> --sixUser, [data-source=post] -> --sepUser (YARN/PULSE don't feed it yet).

- _gameboard.scss: .my-sea-title-of gets letter-spacing: normal, dropping the h2's enforced 0.2em tracking so the short italic 'of' doesn't read gappy.

- bundled (parallel work): rootvars.scss chroma-hue primaries brightened (yellow/lime/cyan/indigo/violet/fuschia/magenta pri+ter ramps).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:55:34 -04:00
Disco DeDisco
a02f3473d5 tooltips: tense-aware expiry (expires/expired) + a <60min 'N min' bucket in the shared relative_ts — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
The token + position-circle expiry tooltips hardcoded 'expires' and never
flipped to 'expired' once the time passed — a seat whose `cost_current_until`
(the 7d cost clock) lapsed still read 'expires 11:30 p.m.' (staging 2026-06-08).

- New `expiry_phrase(dt)` filter (lyric_extras): 'expires <when>' for a FUTURE
  datetime, 'expired <when>' for a PAST one — the verb carries the tense so the
  underlying `relative_ts` stays direction-agnostic. Wired into
  `Token.tooltip_expiry` + the position-circle `data-tt-expiry` (position-tooltip.js
  copies it verbatim, so no JS change).
- `relative_ts` gains a <60min → 'N min' bucket (buckets: 60min / 24h / 7d /
  12mo / >12mo). Per user-spec it stays SHARED, so scroll.html's provenance feed
  (+ post.html / my-games row-ts) now reads 'N min' for very recent events too.

TDD: relative_ts <60min past+future + the 1h boundary; expiry_phrase
none/future/past/wraps-relative_ts; billboard post-line test updated (3h→clock-
time, + new just-posted→'N min'). 727 lyric+billboard+gameboard ITs green.

Bundled (parallel work): rootvars.scss chroma-hue primaries brightened
(--priRd/Or/Gn/Tk/Bl/Id + --terGn).

[[project-position-circle-tooltips]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:20:09 -04:00
Disco DeDisco
a0499723d3 sig select: redirect a seatless multi-seat (CARTE) owner to ?seat=<owned[0]> so tray + overlay + reserve align — TDD
A CARTE owner (all 6 seats, both polarities) entering SIG_SELECT with no ?seat
saw the sig overlay + reserve URL locked to the canonical PC seat (one polarity)
while the TRAY followed `current_slot` = owned[0] (the lowest owned slot, often
the OTHER polarity). A sig reserved from that view filed against the WRONG seat
— the seat the owner thought he covered stayed empty — so that polarity never
reached 3-ready, its 12s countdown ran to 0 but the server-side `_fire` bailed at
`len(ready) < 3` and never advanced; the other polarity proceeded. Switching
pos-circles via GATE VIEW (sets ?seat) re-aligned every surface and unstuck it.

A 3-agent trace confirmed the mechanism + corrected my first guess: this is NOT a
WS problem (the cursor group only drives the cosmetic flashing numeral; the
SIG→SKY advance is a threading.Timer broadcasting to the room_<id> group every
socket joins). The stall is the misfiled reservation → 3-ready COMPLETENESS
failure, rooted in two seat resolvers disagreeing when seatless: the overlay /
sig_confirm use `_canonical_user_seat` (PC-first) while the tray / reserve use
`_viewer_current_slot` owned[0].

Fix: `room_view` redirects a seatless multi-seat owner (gate_slots.filter(gamer)
.count() > 1) in SIG_SELECT to ?seat=<current_slot>, so EVERY surface (tray,
overlay, reserve URL, WS cursor group) resolves to one seat via the already-correct
?seat path — the same realignment a GATE-VIEW switch does. SIG_SELECT-only
(SKY_SELECT already keys off selected_seat); single-seat gamers / non-owners / anon
fail the guard. Unaffected: the multi-gamer sig FTs (one seat each) + the WS-direct
CarteCursorGroupTest.

TDD: 6 ITs in CarteTrayFollowsSelectedSeatTest (redirect / ?seat-present no-redirect
/ overlay+tray agree post-redirect / single-seat / non-owner / SKY_SELECT
no-redirect); the red `'PC' != 'BC'` was the divergence itself. 381 epic-view ITs
green.

[[project-sig-select-seat-switch-open-problems]] [[feedback-ws-cursor-group-must-match-acting-seat]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:05:03 -04:00
Disco DeDisco
dcfa54f522 game kit: free a deposited trinket 7d after it goes in-use; retire COIN's room cooldown — TDD
A deposited trinket binds `Token.current_room` (the 'In-Use: <room>' Game Kit
label), set on deposit + cleared only on MANUAL return. Once a seat's token cost
lapsed (7d → GATE VIEW returns to prompt re-deposit) nothing freed the binding —
`cost_current` is a render-time prop, so no 7d mutation — leaving e.g. a CARTE
used a week prior stuck 'In-Use' (staging, 2026-06-08).

Fix — a uniform, type-agnostic in-use clock:
- New `Token.in_use_since`, stamped when `current_room` is set (`debit_token`
  COIN, `drop_token` CARTE). `release_lapsed_trinkets(tokens)` frees
  `current_room` + `in_use_since` + `slots_claimed` once held >= the room's
  `renewal_period` (7d). The SEAT is untouched — renewal grace (to 2xspan) +
  auto-BYE stay `_expire_lapsed_seats`'s job (a separate, later threshold).
- PASS/BAND never bind (reusable keys) -> no-op; COIN + CARTE covered by the one
  rule. Fires on the gameboard / `_game_kit_context` render (the gamer sees it
  freed immediately) + the cron backstop (`expire_lapsed_room_seats`).
- Migration 0018 backfills `in_use_since` from the earliest backing-slot
  `filled_at` so existing staging bindings release on accurate timing (old free
  on next sweep, recent keep grace) instead of every legacy NULL releasing early.

Retired COIN's bespoke ROOM cooldown: `debit_token` no longer sets
`next_ready_at` + `return_token` no longer clears it. `next_ready_at` is now
My-Sea-exclusive (the 24h DRAW cooldown, no room context) — the two clocks no
longer share a meaning + can't clash (user-spec 2026-06-08).

TDD: ReleaseLapsedTrinketsTest (6) + cron release + 2 gameboard-immediacy tests +
updated return/tooltip/bind assertions; 1124 lyric+epic+gameboard ITs green.

[[feedback-equip-slot-gates-trinket-use]] [[feedback-my-sea-cooldown-design]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:35:35 -04:00
Disco DeDisco
d50645b216 sea deck stack: rename the stale .sea-stack-ok class to .sea-stack-flip (the btn renders FLIP now) — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Pure rename, no behavior change — the deck-stack reveal btn kept the name `.sea-stack-ok` from when it said OK; it has rendered FLIP for a while. Renamed across all 8 references: _card-deck.scss (reveal + pointer-events rules) + a _gameboard.scss comment; the 3 templates that emit or query it (_sea_overlay.html, _my_sea_deck_stack.html, my_sea.html inline JS); and the 3 tests that select it (test_game_room_select_sea FT, test_game_my_sea FT, test_sea_visit IT rendered-HTML asserts). Verified 47 green across all three surfaces (visit IT + gameroom PickSeaDeal stack FT + my_sea CardDraw FT).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:04:51 -04:00
Disco DeDisco
c9fc5a2fd4 sea-select deal FT: scroll the revealed FLIP btn into view before the is_displayed assert — TDD
test_clicking_stack_shows_ok_btn is the only PickSeaDealTest that pairs _choose_spread w. an is_displayed() assert (the six-draws test checks the class instead). Once a spread is OK'd the felt scroll-snaps onto two pages + the deck stacks sit on the page the post-OK rAF scroll-to-cross targets — but headless Firefox DROPS that delayed scroll, leaving the clicked stack's revealed FLIP btn off the visible page → is_displayed False. scrollIntoView the btn first so the assert reflects the `--active` reveal, not the scroll position. Surfaced in CI build 374's channels stage (a flake that 373 survived on retry; my bd9155c preview edits only shifted the timing).

[[feedback-headless-delayed-scroll-dropped]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:00:51 -04:00
Disco DeDisco
1a83c5f01c sea stage: gate the FLIP-back 0.3 polarity tint to the cloned dubbodeck (Sea Select); my_sea/visit monodeck backs render un-tinted
- the `.sea-stage--gravity/levity .sea-stage-card.is-flipped-to-back::after` 0.3 fill differentiates the two halves of a monodeck CLONED into a two-toned dubbodeck — only meaningful in Sea Select. my_sea / my_sea_visit draw a single monodeck that is never cloned, so the tint was just noise on their FLIP'd card-back
- the shared `_sea_stage.html` now takes a `sea_stage_dubbo` flag; only `_sea_overlay.html` (the gameroom Sea Select felt) passes it → `.sea-stage--dubbo` on #id_sea_stage. The two tint rules gate on `.sea-stage--dubbo.sea-stage--gravity/levity`; non-dubbo stages fall through to the empty, fill-less base `::after` (no visible overlay)

[[project-deck-segment-model]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:29:04 -04:00
Disco DeDisco
bd9155c13b my sea preview: namespace cells .sea-prev-pos-* to de-collide from the live cross; harden spread dropdown + CI retry — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
- rename the spread-preview's `.sea-pos-*` cells to `.sea-prev-pos-*` (template + aliased Celtic-cross geometry under `.sea-cross--preview` in _gameboard.scss + the SeaDealSpec fixture) so a bare `.sea-pos-*` matches ONLY the live `.my-sea-cross`; fixes test_picker_renders_sao_default_position_subset (was 2≠1 — the preview duplicated every cross cell)
- spread dropdown: cap `.sea-select-list` w. max-height + overflow-y:auto, & JS-click the option in the `_pick` FT helper, so the last option (escape-velocity) can't land below the un-scrollable picker-modal fold in landscape (ElementNotInteractable, seen in CI build 373)
- _retry_failed.sh: anchor the FAIL/ERROR label sed to the FIRST paren group so parameterized subTest failures retry the real method label, not a bogus `position='…'` module (ModuleNotFoundError)

[[project-my-sea-roadmap]] [[feedback-collectstatic-before-ft]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:19:34 -04:00
Disco DeDisco
a0ded7f09b Sea Select FTs: drop the removed-LOCK-HAND tests + reveal the cross before stack interaction — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
The 06-07 modal→felt scroll-snap refactor dropped LOCK HAND (→ AUTO DRAW +
auto-completion on the 6th draw) and moved the deck stacks into the cross-col
(`.sea-page--room:not(.sea-spread-chosen) .sea-cross-col{display:none}`), but the
channels `PickSeaDealTest` was never updated → 3 reds in the two-browser/channels
CI stage:
• test_lock_hand_btn_present_and_disabled + test_lock_hand_enables_after_six_draws
  — looked up the removed `id_sea_lock_hand` (NoSuchElement).
• test_clicking_stack_shows_ok_btn — the stack OK is hidden until a spread is OK'd.

Fix: deleted the disabled-LOCK-HAND test; rewrote the six-draws test to assert the
new synchronous completion (the deck-stack FLIP btns gain `.btn-disabled` the
instant the 6th card lands, before the 3s felt cascade — SEED MAP is IT-covered);
added a `_choose_spread()` helper (clicks `id_sea_confirm_spread`) so the
stack-interaction tests run against the revealed cross page. Pre-existing 06-07
staleness — NOT tonight's glow/swap/applet commits (verified: `_load_sea_overlay`
opens the sea felt with no sky felt open, so the new swap guard is a no-op).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 02:00:47 -04:00
Disco DeDisco
564100cadb Applets: retitle My Sky → SkyDrive + My Sea → Sea of Cards w. planetary recolor; match the gear-menu entries — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Applet titles + gear-menu toggle labels (Applet.name, via data migration 0014)
both retitled — a deliberate departure from the "My X" applet convention for
these two surfaces. Slugs (my-sky / my-sea) stay put.

Recolor (applet shells only — the standalone pages are tomorrow's todo):
• SkyDrive (uranium ramp): --sixU light-green title font, --priU deep-green
  shell bg; hover keeps the palette --ninUser highlight, glow takes the --sixU
  tint. Reverses to --priU font / --sixU bg on *-light palettes.
• Sea of Cards (neptunium ramp): --sixNp light-teal font, --priNp deep-teal shell;
  the conjunction "of" is a lowercase-italic span against the h2 uppercase
  transform. Reverses to --priNp / --sixNp on *-light palettes.

Mechanism: the shared applet title-link rule now reads --applet-title-fg (with a
--terUser fallback so every other applet is untouched); each applet sets that +
--applet-shell-bg, and a body[class*="-light"] override swaps the pair. No
specificity war.

Tests: data migration 0014 (reversible); FT/IT applet seeds + the My Sky heading
assertion updated to the new names; 531 dashboard/gameboard/applets ITs green.

[[feedback-applet-vs-page-naming-convention]] [[feedback-scss-id-context-specificity-trap]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:29:29 -04:00
Disco DeDisco
945d110171 Sea Select: keep the sky btn lit on a revisited felt (+ clean felt-swap); slow the glow-handoff ease-out — TDD
Symmetry (user-spec): Sky Select leaves the burger sea btn lit while the sky
felt is up; Sea Select now mirrors it — openSea disables ONLY id_text_btn, no
longer id_sky_btn, so a revisited Sea Select keeps the completed sky one click
away. An adversarial pass flagged that the two felts are equal-z (z-index:5)
siblings and neither open path drops the other's open-class, so leaving the sky
btn lit and clicking it from inside the sea felt would DOUBLE-OPEN (sea paints
over sky → confusing no-op) — the same latent stack already reachable in the
sky->sea direction on a both-complete reload. Fixed in BOTH directions: openSea
now closes the sky felt first + openSky closes the sea felt first (each exposes
its close on window: closeSeaFelt / closeSkyFelt), so clicking the other phase's
lit btn performs a clean SWAP. The text-btn disable/restore chain stays correct
across the swap (closeSky restores text, openSea re-disables + recaches it).

Glow: the glow-handoff pulse ease-OUT now runs ~2.8s (was ~1.4s) — moved the
cycle to 3.2s with the bright peak at 12.5%, keeping the ease-IN swell at ~0.4s
(user asked for a longer linger).

[[feedback-felt-aperture-fill-covers-felt]] [[feedback-inline-partial-script-defer-for-later-partial]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:57:24 -04:00
Disco DeDisco
82f4af9bcc Sea Select glow: defer sea-btn reopen + glow handoff past parse time; pulse the glow-handoff — TDD
room.html includes _sea_overlay.html (~L77) BEFORE _burger.html (~L167, which
holds #id_burger_btn + #id_sea_btn), so the overlay's two inline scripts
captured those btns at parse time → null → both bindings silently no-op'd:
the sea btn (active post-completion) did nothing on click + the burger stayed
stuck --priId because its glow-handoff transfer listener never bound. Defer both
the sea-btn REOPEN binding (_bindSeaReopen) & the burger→sea_btn→.sea-select
GLOW chain (_bindGlowHandoff) to DOMContentLoaded so the burger fan exists first.

Also make the glow-handoff halo PULSE (quick ease-in swell, slow ease-out decay
via per-segment timing fns + a lopsided 22/78 keystop split) instead of a flat
glow — the burger, then the sea btn after the handoff click, keep cueing
"click me to reopen your sea".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:39:38 -04:00
Disco DeDisco
0a6bfcf6cc Sea Select options: fixed-width chunks + 2-line spread names (add Rider-) — TDD
- The option chunks' width tracked the select label length (jumped between the
  two spreads). Fix the `.sea-form-col` to 19rem (a touch wider) so all three
  chunks share a standard width.
- Split the spread label onto two lines around the comma + add the "Rider-"
  prefix now there's room: "Celtic Cross," / "Rider-Waite-Smith" and "Celtic
  Cross," / "Escape Velocity". combobox.js now writes the current label via
  `innerHTML` (not textContent) so the `<br>` survives a selection — plain-text
  options (sky / my_sea) are unaffected (their innerHTML == textContent).
- ITs updated for the new gameroom labels (my_sea's single-line names untouched).
  953 epic+gameboard ITs + Jasmine green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:25:11 -04:00
Disco DeDisco
2bf439eab5 Sea Select options: disabled-btn contrast + AUTO DRAW scrolls back to the cross
- A disabled OK/DEL × inside a --priUser option chunk blended into it (the global
  `.btn-disabled` bg is also --priUser → no visible circle). Drop the disabled
  btns in `.sea-options-col` to the felt --duoUser so they read as a distinct
  disabled circle, like the deck-stack FLIP ×.
- AUTO DRAW now eases the felt back UP to the cross even when the user already
  OK'd the spread + scrolled DOWN to the options page — so he watches the cards
  land one-by-one. `_chooseSpread(slideIn)`: the OK reveal pins to the options
  (slide-in from above); AUTO DRAW (already chosen) skips the pin + just eases up
  to the cross. `_scrollToCross` now eases from the current scroll position.
- 12 PickSeaUnifiedFeltTest render ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:14:25 -04:00
Disco DeDisco
1fe257a7a9 Sea Select options: OK beside the select + --priUser chunk rects — TDD
Restyle the spread-options page (post the scroll-snap refactor):
- OK `.btn-confirm` moves UP beside the `.sea-select` combobox (a new
  `.sea-select-row`), off the AUTO DRAW / DEL action row.
- OK gains `.btn-disabled` + × the moment the first card is drawn — inverse to
  DEL (which loses them then), simultaneous with the combobox locking. So
  `_chooseSpread` (OK) no longer locks; the lock + both btn states flip together
  at the first draw via `_setHasDrawn` + `_lockSpread`. Server-renders OK
  disabled/× when `saved_by_position`.
- The three chunks (spread/select/OK, the mini preview, AUTO DRAW/DEL) each get
  the same --priUser rounded rectangle as the GAME POST lines / composer
  (`_base.scss` `.form-control`): --priUser fill + half-alpha --secUser border +
  rounded + padding. The `.sea-form-col`/`-main` go transparent flex columns so
  the felt shows between the chunks.
- IT: OK enabled / DEL disabled when fresh; flips once a card is drawn.
  953 epic+gameboard ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:02:14 -04:00
Disco DeDisco
edc9a49f06 Sea Select: refactor to scroll-snap options→cross (mirror Sky Select), drop the modal — TDD
The Gaussian spread modal couldn't hang off the burger #id_sea_btn anymore (that
button now also opens the felt). Mirror Sky Select's form→wheel scroll-snap
instead: the felt starts with the spread OPTIONS on the --duoUser felt; clicking
OK confirms the spread → the options shunt DOWN and the spread CROSS takes page 1
(scroll down to find the options again). No modal, no corner NVM.

- `_sea_overlay.html` restructured into `.sea-options-col` (the .sea-select
  combobox + mini preview + OK .btn-confirm + AUTO DRAW + DEL — NO deck stacks)
  and `.sea-cross-col` (the real .my-sea-cross + the Gravity/Levity deck stacks +
  the portaled stage). `#id_sea_overlay` is a `display:contents` passthrough so
  the two cols are the scroll-snap sections.
- OK (`#id_sea_confirm_spread`) → `_chooseSpread()`: adds `sea-spread-chosen` to
  the felt → SCSS engages `scroll-snap-type:y mandatory`, the cross-col gets
  `order:-1` (page 1), options shunt to page 2; locks the combobox; eases the
  scroller to the cross. AUTO DRAW also confirms first. A reload of an in-progress
  sea renders `sea-spread-chosen` (cross revealed) server-side.
- SCSS (`_sky.scss`): the sea felt is now a column scroller; `.sea-cross-col`
  `display:none` pre-confirm; the `sea-spread-chosen` scroll-snap block mirrors
  `body.sky-saved`. The options `.sea-form-col` goes transparent/content-sized
  (blends onto the felt, not the modal's --priUser card).
- Sea sub-btn: no longer activated by openSea; it's the POST-COMPLETION reopen
  affordance (cascade activates it + `sea_btn_active = hand_complete` ctx flag),
  an active click → `window.openSeaFelt()` (review the saved spread), like the
  sky btn. Removed the sea_btn open-modal IIFE + the corner NVM.
- IT: options-on-felt (combobox + OK + AUTO DRAW + DEL + preview) w. NO modal /
  NVM. 952 epic+gameboard ITs + Jasmine + PickSeaAsyncTransitionTest(3) green.

my_sea.html keeps its modal (untouched) — the gameroom intentionally diverges.

[[project-character-creation-spec]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:53:43 -04:00
Disco DeDisco
cf84fdc992 Sea Select: post-completion cascade — felt eases out → DRAW SEA → SEED MAP — TDD
Mirror CAST SKY's post-save cascade for the sea phase. When the 6-card spread
completes (live FLIP of the 6th card / AUTO DRAW finishing): linger ~3s on the
felt → the felt eases OUT (`.sea-page--cascade-out`, revealing the table-hex) →
DRAW SEA gives way to SEED MAP + the sea glow fires on the burger (handoff →
sea_btn) → +3s → SEED MAP eases IN. Same shape as CAST SKY → sky-btn glow →
DRAW SEA.

- `_room_hex_center.html`: SEED MAP joins the hex-phase-stack; DRAW SEA goes
  --out once `hand_complete`, SEED MAP --out until then (a reload of a complete
  sea lands on SEED MAP server-side = the cascade's end-state). SEED MAP → the
  Voronoi map (roadmap step 21) is a stub — it only needs to APPEAR here
- `_sea_overlay.html`: `_setComplete(on, live)` runs `_startSeaCascade()` on the
  LIVE completion (FLIP / AUTO DRAW pass `live=true`; init does not, so a reload
  doesn't re-animate). The completion-glow IIFE no longer self-starts on the
  data-state transition — the cascade adds `glow-handoff` to the burger; the IIFE
  keeps only the burger → sea_btn → .sea-select handoff
- `.sea-page--cascade-out` SCSS (mirrors `.sky-page--cascade-out`)
- ITs: SEED MAP --out pre-completion (DRAW SEA in); SEED MAP in + DRAW SEA --out
  when hand_complete. 952 epic+gameboard ITs + PickSeaAsyncTransitionTest(3) green

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:03:17 -04:00
Disco DeDisco
0f57cae50d Sea Select: spread center significator supplies the card-face image — TDD
The felt cross's CENTER significator was hardcoded to the corner-rank + suit-icon
text thumbnail, so an image deck (RWS / Minchiate) showed a bare "0"+icon card
instead of its face. Mirror my_sea: render the `.sig-stage-card-img` (image mode)
when the sig's deck has card images, else fall through to the text thumbnail. The
tray sig stays the simple thumbnail (user-spec). The sig's `deck_variant` is the
card's OWN deck — the Sig Select pick is drawn from the Role Select contributed
deck (`_room_deck_variant`), so this is the correct image source (no equipped_deck
bug, unlike the earlier FLIP back-img).

- IT: an image-deck significator renders the center `sea-sig-card sig-stage-card--image`
- 949+ epic ITs green

; FLIP tint tweak (parallel edit): flipped-back overlay alpha 0.6 → 0.3

[[project-deck-segment-model]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:47:54 -04:00
Disco DeDisco
d09dca56c0 Sky/Sea Select: ?seat-aware so a CARTE owner drives all 6 seats — TDD
A Carte Blanche gamer owns all 6 seats but could only cast sky / draw sea for ONE:
the sky/sea state IS per-seat (Character.seat), but the code resolved to the fixed
canonical (PC-first) seat, ignoring the ?seat switched to via the GATE VIEW pos-
circles — so the tray switched but the sky wheel / sea spread stuck on the
canonical seat + saves wrote back to it. Mirrors Sig Select's existing ?seat path.

- generalize `_acting_sig_seat` → `_acting_seat` (logic is sig-agnostic; 3 callers)
- `_role_select_context` SKY_SELECT branch keys off `selected_seat` (the ?seat-aware
  seat, already computed) instead of `_canonical_seat`: user_polarity,
  confirmed_char, user_seat_role, my_tray_sig, saved_by_position, saved_sea_spread,
  sea_default_spread, hand_complete, sea_back_image_url
- sky_save / sky_delete / sea_save / sea_delete / sea_deck resolve the acting seat
  via `_acting_seat(…, request.GET.get("seat"))`; sea_partial threads seat_param
- the sky + sea felts carry `?seat={{ current_slot }}` on their save/delete/deck/
  sea_partial action URLs so the POSTs target the switched-to seat
- single-seat flow unchanged (no ?seat → canonical fallback)
- ITs: CARTE owner — ?seat switches the displayed spread; sea_save/sky_save target
  the switched seat leaving the canonical seat's Character intact; felt URLs carry
  ?seat. 949 epic+gameboard ITs green.

; FLIP tint tweak (parallel edit): drop the polarity border, bump the flipped-back
overlay --quiUser/--terUser to 0.6 alpha

[[project-sig-select-seat-switch-open-problems]] [[project-deck-segment-model]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:44:06 -04:00
Disco DeDisco
de59cb7e69 Sea Select FLIP: source the card-back from the seat's deck, not equipped_deck — TDD
The sea-stage FLIP no-op'd in the gameroom because `_sea_stage.html` rendered
the back-img from `request.user.equipped_deck.back_image_url` — but
`select_role` NULLS OUT the user's equipped_deck once it's contributed to the
room's seats (views.py:1148), so in the Sea Select phase the deck was None →
no back-img element → sea.js's FLIP handler short-circuits on the missing
`.sig-stage-card-back-img` sibling. (my_sea worked: solo, no contribution.)

- `_sea_stage.html` now renders the back-img from a `sea_back_image_url` ctx var
  instead of `request.user.equipped_deck`
- gameroom: `_role_select_context` sets it from the SEAT's contributed deck
  (`_canonical_seat.deck_variant`, when it has card images)
- my_sea: the my_sea view sets it from the user's own equipped deck
- ITs: image seat-deck renders `.sig-stage-card-back-img`; text seat-deck omits it

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:03:19 -04:00
Disco DeDisco
ab00774a49 Sea Select: drawn-slot reopen after refresh + FLIP polarity-tinted back — TDD
- fix: drawn spread slots silently no-op'd on click AFTER a refresh — SeaDeal's
  in-memory `_seaHand` is only populated by openStage/register during the live
  session, so a reload left it empty + the overlay click handler short-circuited
  (`if (!_seaHand[pos]) return`). `_sea_overlay.html` now re-seeds `_seaHand`
  from the server-rendered saved slots once the deck fetch resolves (cards
  looked up by `data-card-id`; reversed/polarity DOM-sourced) — the same fix
  my_sea already carries
- FLIP card-back: the sea stage now renders the deck back-img for ANY image-
  equipped deck w. a back image (dropped the `not is_polarized` gate — it
  omitted the back for the room's polarized Gravity/Levity draw, so FLIP
  no-op'd). The back-art is identical across polarities, so `_card-deck.scss`
  tints the FLIPped card by the drawn polarity: gravity --quiUser fill @ 0.3 +
  --quaUser border; levity --terUser fill @ 0.3 + --ninUser border (scoped to
  `.sea-stage--*`, so my_sign / applet stages are untouched)
- IT: an image-deck sea stage renders `.sig-stage-card-back-img`

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:53:35 -04:00
Disco DeDisco
c037e876e2 Sea Select: rebuild as a felt + Gaussian spread modal, unify w. my_sea — TDD
Hollow out the gameroom DRAW SEA dark modal into the two surfaces my_sea
uses: a --duoUser felt (.sea-page--room) filling the hex pane where the
Celtic Cross deals, + a Gaussian spread modal (#id_sea_spread_modal: the
.sea-select combobox w. the two Celtic Cross 6-card opts ONLY, AUTO DRAW/DEL,
a mini preview, + a corner #id_sea_cancel NVM). Opened by DRAW SEA
(html.sea-open); the room gear's NVM (room-menu-sea) returns to the hex;
#id_text_btn + #id_sky_btn go inert while the felt is open.

- persist: epic:sea_save / sea_delete upsert the seat's Character.celtic_cross
  (none of my_sea's MySeaDraw quota/Brief machinery); room ctx adds
  saved_by_position + saved_sea_spread + sea_default_spread + hand_complete so
  a reload re-renders the filled cross. celtic_cross field already existed (no
  migration)
- mini spread preview (_sea_spread_preview.html) in BOTH the gameroom + my_sea
  modals — shape only, NEVER dealt to: SeaDeal scopes its slot queries to
  .sea-cross:not(.sea-cross--preview)
- always TWO deck stacks (Gravity + Levity) in the room Sea Select — the gamer
  draws from either populated half (sea_deck split), even a CARTE monodeck;
  unlike my_sea / Sig Select's polarization collapse
- glow coordination: the sky-saved glow is muted in the sky/sea phases
  (html.sky-open / sea-open / sea-entered); sea glow color --priYl -> --priId
  (distinct from sky's --priTk); the sea glow-machine fires at hand-COMPLETION
  (mirrors Sky Select), not during drawing
- guard copy "Auto deal cards?" -> "Auto Draw cards?" (match the AUTO DRAW btn)
- fix: drop the stale `html.sea-open #id_aperture_fill { opacity:1 }` — it
  painted the opaque z-90 fill over the z-5 felt so the spread flashed then
  vanished (same trap as the CAST SKY felt); removed the dead .sea-backdrop /
  .sea-overlay / .sea-modal-* SCSS
- tests: epic PickSeaPersistTest (7) + PickSeaUnifiedFeltTest (6) ITs; SeaDeal
  preview-scoping + BurgerSpec sky-glow-mute Jasmine specs; my_sea sig-card
  ITs scoped to .my-sea-cross (the preview adds a 2nd .sea-sig-card)

[[feedback-felt-aperture-fill-covers-felt]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:42:24 -04:00
Disco DeDisco
4322e1fc17 table-hex: DRY-lift the shared hex skeleton into core/_partials/_table_hex.html
The .room-table-scene → .table-hex-border → .table-hex → .table-center + seats
ring was byte-identical across room.html, my_sea.html, my_sea_visit.html &
billboard/my_sign.html. Lift the skeleton into one cross-app partial so every
surface — and the upcoming Sea Select felt rebuild — shares a single source.

- core/_partials/_table_hex.html: the 4-div skeleton. The varying center +
  seats are passed as partial NAMES (`hex_center` / `hex_seats`) since Django
  {% include %} has no slot; `{% include hex_center %}` resolves the string var
  + inherits the full page context (no `only`), so each fragment sees its page's
  vars unchanged → identical render.
- Per-surface center fragments (verbatim moves): _room_hex_center,
  _my_sea_hex_center, _my_sea_visit_hex_center (gameboard/_partials),
  _my_sign_hex_center (billboard/_partials).
- Seats fragments: _room_hex_seats (gate_positions); _table_seats — SHARED by
  my_sea + my_sea_visit (`seats` loop; the `--self` modifier is inert on my_sea);
  _my_sign_hex_seats (single chair).
- The page-specific outer wrappers stay put (room's .room-shell + ROLE_SELECT
  SCAN SIGS form; .my-sea-landing / .my-sign-landing; my_sign's
  {% if not current_significator %} gate).

The "felt" is deliberately NOT extracted — it's a --duoUser bg toggled by
phase/stage classes (CSS), already DRY; each phase's felt content is bespoke.

Markup-only, no behaviour change. Verified: 1170 epic+gameboard+billboard render
ITs green (the table-hex / table-seat / center-btn assertions are the gate) +
MySeaDrawSeaLandingTest FTs green (live hex render + FREE DRAW seats
.table-seat[data-slot="1"] through the shared seats partial).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:37:50 -04:00
Disco DeDisco
ce4cb03af7 DRAW SEA async-transition FT: reload fallback for non-felt-save sky confirm — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Running the corequisite channels FTs surfaced a real gap the cascade introduced:
a sky confirm that did NOT come through the felt's own save (a direct POST, or
another browser of the same seat) left _lastChartData null, so _onSkyConfirmed's
_activateSavedState returned early → no transition at all (the old code reloaded).

- _sky_overlay.html: _onSkyConfirmed now reloads when _lastChartData is absent
  (the pre-cascade behaviour, preserved for non-felt-save confirm paths); the
  felt-save path still eases to DRAW SEA via the cascade (no reload).
- test_game_room_select_sea.py: the async-transition assertion updated for the
  phase-stack — CAST SKY is now present-but-`--out` (hidden in the shared grid
  cell), not removed from the DOM, so assert the `hex-phase-btn--out` class on
  CAST SKY + its ABSENCE on DRAW SEA rather than `find_elements(...) == []`.

Corequisite FTs run green: select_sea async-transition (3) + deal (9) channels;
dash my_sky async-save + aperture-snap (3) — the shared body.sky-saved apparatus
is untouched by the room-scoped felt SCSS. select_sky FTs already green. The sig
SRG7 reveal FT is unaffected (it waits for the SIG_SELECT hidden CAST SKY, which
that branch still renders; it never clicks, so the _reload change isn't reached).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:36:05 -04:00
Disco DeDisco
d5e4fc53f0 CAST SKY cascade: felt eases out → glow → DRAW SEA eases in; burger handoff; reload-into-open — TDD
Post-save choreography + the polish asks from this session.

- Post-save cascade (_sky_overlay.html): after SAVE the gamer lingers on the
  wheel ~3s, then the felt eases OUT (fade, .sky-page--cascade-out) to reveal the
  table-hex; the burger fires its --priTk glow + #id_sky_btn goes active (both
  ride the cascade now, not the save instant); 3s later the DRAW SEA btn eases IN
  and the sea overlay is injected so it's live with NO reload.
- Hex phase-stack (room.html + _room.scss): CAST SKY + DRAW SEA share one grid
  cell (.hex-phase-stack) so they cross-fade in place (.hex-phase-btn--out); the
  server seeds --out on the inactive one, the cascade swaps them. A confirmed
  reload lands DRAW SEA visible / CAST SKY out, same as the cascade end.
- Seamless sea injection: _injectSeaOverlay fetches sea_partial (the URL already
  existed, unused) + re-creates its <script>s so the overlay's own init (openSea
  + SeaDeal.reinit) runs — DRAW SEA opens with no reload. (Bridge until Sea Select
  is itself hollowed into a felt.)
- Burger → Sky-btn handoff (burger-btn.js + _burger.scss): _pulseGlow generalized;
  once saved, the next burger-OPEN pulses #id_sky_btn --priTk ("now click me to
  reopen"). Jasmine specs added (BurgerSpec).
- #id_text_btn disabled while the felt is up (openSky/closeSky) — its swipe
  machine would otherwise half-load the scroll-locked reelhouse.
- Reload-into-open (sig-select.js + _sky_overlay.html): the SIG_SELECT→SKY_SELECT
  CAST SKY click crosses a server-render boundary (felt/phase-stack/sea-inject
  only exist in SKY_SELECT), so it still reloads — but drops a sessionStorage
  flag first so the felt OPENS on arrival. Kills the old click→reload→click-again
  double-take (the "first click reloads" report). DRAW SEA can inject in-place
  (stays within SKY_SELECT); CAST SKY can't, so this is the seamless equivalent.

Tests: BurgerSpec handoff specs + full Jasmine green; 32 render ITs + 930
epic+gameboard green. Cascade timing live-verified by the user.

[[feedback-scss-import-order-specificity]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:24:32 -04:00
Disco DeDisco
94cd9db3a4 CAST SKY felt: fix invisible form — (0,2,0) chain + drop the modal-era aperture-fill
Two live-only stacking/layout bugs in the in-room felt (caught via Claudezilla
DOM inspection — neither is reachable by IT/Jasmine):

- .sky-page--room (0,1,0) lost the source-order tie to the LATER base
  `.sky-page { position: relative }` (also 0,1,0), so the felt stayed
  position:relative + flex:1 and collapsed to width 0 as a flex child of the
  hex-pane — the form vanished onto a 0-wide column. Chained to
  `.sky-page.sky-page--room` (0,2,0) so it wins regardless of order.
  [[feedback-scss-import-order-specificity]]
- Dropped `html.sky-open { #id_aperture_fill { opacity: 1 } }` — that modal-era
  backdrop is a full-cover --duoUser div at z-90; the old dark modal sat above
  it (z-120), but the felt sits at z-5, so the fill painted an opaque green
  sheet OVER the felt + form. Its opacity transition is why the form "flashed
  then vanished after <0.5s". The felt is its own --duoUser surface + covers the
  hex on its own, so the fill stays transparent now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:29:04 -04:00
Disco DeDisco
1f874de459 CAST SKY: unify w. My Sky — inline --duoUser felt + scroll-snap wheel, gear NVM, burger reopen glow — TDD
Replaces the root-level dark Gaussian CAST SKY modal with the my_sky / sky.html
apparatus, mirroring the Sig Select unify (71c0069). Sky data stays seat-bound
(Character.seat), never a pos-circle.

- room.html: the sky overlay moves INTO .room-hex-pane on --duoUser felt
  (has-sky-stage), my_sea-style; rendered through the confirmed state too so
  the burger can reopen the saved wheel. Sky tooltips stay at root.
- _sky_overlay.html: drops .sky-backdrop / .sky-modal-wrap / .sky-modal / header
  + the in-felt NVM; reuses the shared .sky-page form/wheel (.sky-page--room).
  No live preview — the wheel only paints after SAVE SKY (my_sky parity); SAVE
  adds body.sky-saved → the felt flips to scroll-snap (form shunts to page 2,
  ease to the wheel on page 1). saved_sky_json primes the reopen draw. Inert
  STUB hook for the post-character-creation form lock (roadmap step 21).
- _sky.scss: in-room felt fill + open/close (html.sky-open); hides the position
  strip while the felt is up for a clean homogeneous surface.
- _room.scss: html.sky-open pins .room-aperture.is-scrollable (overflow hidden,
  snap none) so the ATLAS/SCROLL/YARN/POST/PULSE reelhouse is unreachable while
  casting; restored the instant the felt closes.
- _room_gear.html + room-views.js: NVM moves into a new .room-menu-sky gear pane
  (→ epic:room, which re-renders DRAW SEA if saved else CAST SKY); syncGear()
  shows it while sky-open.
- _burger.html + _burger.scss + burger-btn.js: the Sky sub-btn goes .active once
  saved (sky_btn_active = sky_confirmed) — concurrent w. a thrice --priTk burger
  pulse (.sky-saved-glow, rhymes w. .flash-inactive); an active click reopens
  the wheel via window.openSkyFelt.
- epic/views.py: sky_btn_active + saved_sky_json ctx off the seat's confirmed
  Character; the acting gamer's WS auto-reload is dropped (SAVE reveals the
  wheel in place; the gear NVM does the nav to DRAW SEA).
- Tests: PickSkyUnifiedFeltTest + PickSeaRenderingTest ITs (930 epic+gameboard
  green); BurgerSpec sky-glow/reopen Jasmine (full suite green); PickSky
  LocalStorageTest + PickSkyDelTest FTs reworked to the post-save flow.

[[project-deck-segment-model]] [[feedback-scss-import-order-specificity]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:54:33 -04:00
Disco DeDisco
75301ca84d Test runner: retry the SQLite teardown on Windows PermissionError (local-only nicety)
On local Windows + SQLite, DiscoverRunner's teardown ends in
os.remove(test_db.sqlite3), which raises PermissionError [WinError 32] when
another handle still holds the file (a concurrent run, a lingering connection,
AV / Search indexer) — crashing an otherwise-green run at the very end.

RobustCompressorTestRunner.teardown_databases now retries super() up to 10x with
a 0.1s sleep, then leaves the stale file for the next run to overwrite rather
than fail. Mirrors the _robust_save PermissionError retry already in the runner.

CI-neutral: CI is Postgres on Linux — teardown is DROP DATABASE (no file remove),
and Linux unlinks open files without error — so the loop never triggers there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:08:55 -04:00
Disco DeDisco
2c2ec16f08 Revert the Celery countdown migration — it broke local dev (no worker) — back to threading.Timer
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
a6ce207 moved the 12s polarity confirm to a Celery task (apply_async countdown).
That requires a running Celery worker to execute it — but local dev runs only
uvicorn (the dev-server skill starts no worker; the original tasks.py docstring
chose threading.Timer precisely "so no separate Celery worker is needed in
development"). So locally the confirm was queued and never ran: the countdown hit
0, no significators saved, and a refresh stayed in SIG_SELECT (no skip to the
table hex). A regression in the core flow.

Restore tasks.py + test_tasks.py to the faaa4ec threading.Timer version (still
in-process, with the {token, deadline} cache + countdown_remaining restore-on-
load intact) and drop the now-unneeded CELERY_BROKER_URL='memory://' test
override.

Kept from a6ce207: the room.js WebSocket auto-reconnect — that is the actual fix
for the dropped-socket delivery bug (the SigSelectSpec bisection proved the
client restarts the numeral fine on re-received events; the failure was delivery,
which a dead socket with no reconnect explains). Celery was a misdiagnosis of an
in-process broadcast that works fine for a single-process dev/staging server.

23 task UTs + CARTE sig ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:00:42 -04:00
Disco DeDisco
44bf4e626c Sig Select: regression spec — re-ready restarts the visual countdown (client bisection)
A diagnostic that replays the reported "cancel → re-ready doesn't restart the
flashing numeral" sequence purely through the client handlers (countdown_start →
countdown_cancel → countdown_start). It passes: _showCountdown re-renders the
numeral every time the event is received, so the client logic is sound.

This bisects the live bug to WS *delivery* (the re-ready countdown_start not
reaching the browser), not client state — consistent with a dropped room socket
that never reconnected (every subsequent live event lost until a refresh, which
matches the symptom exactly). The auto-reconnect added in a6ce207 is the
relevant mitigation; kept as a permanent guard on the client restart path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:54:26 -04:00
Disco DeDisco
a6ce20761b Sig countdown: run the post-countdown confirm as a Celery task + auto-reconnect the room WS — TDD
The flaky tray→thumbnail→hex animation after the 12s countdown.

Root cause: the confirm ran in a threading.Timer thread inside the web process,
and _fire broadcast polarity_room_done / pick_sky_available via
async_to_sync(group_send) from an ephemeral per-call event loop. With the Redis
channel layer that publish is unreliable across loops (the production analog of
the "broadcast must originate in daphne" test trap), so the live events reached
the client only sporadically — the server-side state (sig assignment,
SKY_SELECT) still committed, which is why a refresh always showed the concluded
hex but the animation usually didn't play.

Fix (chosen: migrate to Celery — the path the tasks.py docstring already called
for): _fire becomes the @shared_task confirm_polarity_room, enqueued by
schedule_polarity_confirm via apply_async(countdown=seconds). The worker is a
stable long-lived process whose channel-layer singleton is never shared with a
serving loop, so its group_send reaches daphne reliably; it also survives
web-worker restarts. No task revocation needed — cancellation/supersession ride
the existing cache token guard (cancel just deletes the token; a stale queued
task no-ops). Dropped threading.Timer + the _timers registry.

Test settings get CELERY_BROKER_URL='memory://' so apply_async queues without a
live Redis and without running the task (no worker) — mirrors the old timer that
was scheduled but never fired inside a sub-12s test. NOT eager: eager would
ignore the countdown and assign significators synchronously during the ready
POST. test_tasks rewritten: confirm_polarity_room called directly (task body),
schedule asserts the enqueue + countdown + fresh-token supersession; the
broadcast itself stays IT-uncoverable under InMemory (known channels limit).

Also: room.js now auto-reconnects the room WebSocket with capped exponential
backoff (1s→30s, reset on open, halted on beforeunload). A dropped socket (proxy
idle-timeout, blip, server restart) previously stayed dead until a manual
refresh, silently losing every live event — an independent reliability gap that
compounded the "sporadic" feel.

602 epic ITs + 18 task UTs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:52:21 -04:00
Disco DeDisco
f3f509a59a Sig Select countdown numeral: enlarge via a class so it doubles at every breakpoint — TDD
The 12s countdown numeral set its size with an inline style.fontSize='2em',
but .btn-primary carries `font-size: 0.625rem !important` inside its small
landscape/short-portrait down-size media query — and an !important declaration
beats an inline style. So on phones / short viewports the numeral stayed
button-sized instead of doubling.

Fix: sig-select.js now toggles a `.sig-take-sig-btn--counting` class instead of
the inline font-size (in _showCountdown / _hideCountdown / the unready path),
and a new `.sig-stage .sig-take-sig-btn.sig-take-sig-btn--counting` rule in
_card-deck.scss re-asserts `font-size: 2em !important` at (0,3,0) specificity —
strictly beating the (0,2,0) btn-primary media-query !important at all queries.
em stays parent-relative so the doubling tracks the stage font across sizes.

2 Jasmine specs (class present + no inline override on show; class cleared on
countdown_cancel) added to SigSelectSpec; SpecRunner green; SCSS compiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:39:52 -04:00
Disco DeDisco
faaa4ecfb0 Sig Select gate-view: CONT GAME/NVM keep the acting seat; restore the live countdown numeral on load — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Two Sig-Select seat-switch follow-ups from the 2026-06-05 countdown sprint.

(1) CONT GAME + gear NVM pos-1 shuttle.
The GATE VIEW (room_gate.html) CONT GAME button and the gear-menu NVM both
linked to epic:room with NO ?seat, so a CARTE multi-seat gamer acting at pos 4
got shuttled back to owned[0] (pos 1) instead of staying on his acting seat —
the same gap bedc489 fixed for the GATE VIEW nav buttons. room_gate now
computes a single table_url (epic:room + ?seat=<current_slot> when seated) and
feeds it to both the CONT GAME onclick and the NVM include. The table view
already reads ?seat, so the gamer lands on the seat he was viewing. Added
`reverse` import to epic/views.py. 2 ITs in CarteTrayFollowsSelectedSeatTest
(cont-game + nvm carry the acting seat; default targets lowest owned); updated
RoomGateViewTest.test_nvm_returns_to_room_hex to expect the seat-carrying href.

(2) Live countdown numeral not restored on a fresh seat view mid-countdown.
countdown_start is a ONE-SHOT WS broadcast: a gamer (esp. a CARTE owner
switching to an already-ready seat) who loads the view after it fired saw a
static WAIT NVM, never the 12s flashing numeral — the redirect still fired, so
it was a visual-restore-on-load gap. The cache entry now stores the absolute
deadline alongside the timer token ({token, deadline} dict, was a bare token
string); _fire's token guard reads either shape defensively so a stale string
from an older deploy can't crash the callback. New tasks.countdown_remaining()
derives the seconds-left from the deadline (None when no countdown / elapsed).
The room view seeds ctx["countdown_remaining"] for the acting polarity;
_sig_select_overlay.html carries data-countdown-remaining; sig-select.js's
_replayReservations restores _showCountdown(remaining) on load when a count is
live, else falls back to WAIT NVM. 4 unit tests (CountdownRemainingTest), 2 ITs
(SigSelectRenderingTest), 3 Jasmine specs (SigSelectSpec countdown-restore).

922 epic+gameboard ITs + 19 task UTs + Jasmine SpecRunner all green.
Trap [[feedback-ws-cursor-group-must-match-acting-seat]].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:25:54 -04:00