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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The 12s flashing #id_take_sig_btn never appeared for a solo CARTE gamer: the
countdown is broadcast only to cursors_<room>_<polarity>, but RoomConsumer
subscribed the WS to a single cursor group chosen from an arbitrary .first()
seat and ignored the acting ?seat. A CARTE owner holds seats in BOTH polarities,
so whenever he was completing the polarity his WS wasn't subscribed to, the
countdown_start event silently missed him (the sigs still committed server-side
via the timer — hence 'works but no visual').
Fix: carry the acting ?seat on the WS URL (room.js) and resolve the cursor
group from it — the ?seat owned-slot override, else the role-canonical seat
(PC-first), matching the overlay's seat resolution. Single-seat gamers are
unaffected (one seat → canonical == .first()).
New channels test CarteCursorGroupTest: acting a gravity seat (?seat) receives
the gravity countdown; no-seat receives the canonical levity countdown. Full
epic channels suite (7) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A CARTE gamer owning multiple seats was capped at one SigReservation per
room by the (room, gamer) unique constraint, and sig_reserve had a solo
immediate-commit shortcut that wrote seat.significator on OK and never ran a
countdown. Together they 409'd the second sig: after OK-ing one seat, switching
to another and clicking OK was blocked — a gamer could only ever pick one sig.
The countdown/confirm machinery (sig_confirm + the threading-timer task) was
already per-seat-row based — it iterates every ready reservation in a polarity
and seats each. So the fix is to let one gamer hold one reservation PER SEAT:
- Constraint (room, gamer) -> (room, gamer, seat) (migration 0018). The
(room, card, polarity) constraint still enforces a distinct sig per seat.
- Demolish the solo immediate-commit shortcut in sig_reserve — it predated the
countdown mechanism. Reserve now always just creates a provisional row.
- Scope the reserve guard, release, and idempotency lookups to the acting seat
via a new _acting_sig_seat(room, user, ?seat) helper; add a guard against
re-using a card already held for another of the gamer's seats.
- sig_ready resolves the acting seat the same way (?seat) and looks up the
per-seat reservation; data-ready-url + user_ready now carry/reflect the seat.
Now a solo CARTE tester reserves + readies all 3 sigs per polarity and each
room fires its own 12s countdown, then both confirm -> SKY_SELECT.
Tests: new SigReserveCarteMultiSeatTest (no-NVM per-seat reserve; same-seat 409
retained; 3-levity-ready fires one countdown); rewrote the model constraint
test + the CARTE FT to the new behaviour. Multi-gamer path unchanged (one row
per gamer) — epic 595 + gameboard 319 + epic channels 5 + jasmine all green;
full 1663-test suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GATE VIEW buttons (navbar + room.html lapsed-cost) linked to room_gate
with no ?seat, so _viewer_current_slot fell back to owned[0] (pos 1). On the
gate view a CARTE multi-seat gamer acting at pos 2+ saw pos 1 wrongly flagged
me-current AND href-less — the one circle you could never switch back to.
Fix (option a): _role_select_context + _gate_context now both expose
current_slot; both GATE VIEW buttons append ?seat={{ current_slot }} on
page-room. The gate view's current now matches the table → pos 1 becomes
me-also (switch href) when acting elsewhere, the occupied seat correctly
carries no href. _gate_context computes current_slot once and reuses it for
gate_positions.
3 ITs in CarteTrayFollowsSelectedSeatTest (button carries acting seat;
default targets lowest owned; gate page re-carries seat + pos 1 is me-also).
911 epic+gameboard ITs green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-seat (CARTE) gamer clicking a pos-circle switched the sig overlay
(via the ?seat=N override) but the tray stayed pinned to the role-canonical
PC seat — my_tray_role read assigned_seats[0] (role-sorted, always PC) and
my_tray_sig read _canonical_user_seat. So every circle put the PC icon in
the tray regardless of which one was clicked.
Re-point both tray keys to the seat occupying current_slot (the acting
seat). Single-seat gamers are unaffected — their lone slot IS current_slot,
so selected_seat == their canonical seat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RoomScrollLiveRefreshTest could never pass: ChannelsLiveServerTestCase runs
daphne in a SEPARATE PROCESS, and the test settings force the in-memory channel
layer, so a group_send issued from the test process (via record() → on_commit)
can't reach the consumer in the daphne process — the scroll_update nudge is
undeliverable no matter how correct the feature is. (Probed it: WS open, the
scroll-status endpoint returns the new row, but the browser logs 0 nudges.)
Production uses Redis, which is cross-process, so the live refresh works there.
Every link is already covered by ITs, so the E2E hop is the only gap:
record() schedules the broadcast on_commit (test_record_broadcasts_scroll_update_
on_commit), RoomConsumer relays it (test_receives_scroll_update_broadcast), and
the re-fetched feed partial renders the latest events (ScrollStatusViewTest).
Replaced the FT with a NOTE documenting why there's no FT + where the coverage
lives. Unblocks the channels CI stage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The qualifier (Elevated/Enlightened/Graven) now renders on the frozen stat-block
stat-face, not just the hover-only text card-face (which image-mode decks hide).
populateStatExtras takes opts.polarity and fills new .stat-face-qualifier--above
/--below slots, mirroring the card-face placement: non-major above the title,
major below it (title gets a trailing comma). Qualifier shares the title's style
per request ("same style as 'Jack of Crowns' below it").
The 3 theme FTs were asserting the qualifier on hover, but the stat-block is
display:none until a card is OK'd (.sig-stage--frozen) — only the card preview
shows on hover. Repointed them to select→OK→freeze, then read the stat-block.
Threaded polarity through the sig + my_sign callers; added 4 Jasmine specs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Earthman deck now renders the sig stage in image-mode (has_card_images
defaults True), so .fan-card-face — where .sig-qualifier-above/below live — is
display:none and Selenium reads "". The Elevated/Enlightened/Graven qualifier
belongs on the always-visible stat-block now (the card-face instance goes away
once every deck gets images). Skipped pending the repoint + green-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end coverage for this session's two shipped features.
SigStageUnifiedTest (FunctionalTest, no WS — both pass locally):
- the sig stage renders INSIDE .room-hex-pane on green --duoUser felt
(.has-sig-stage), the overlay is a descendant of the hex pane, the dark
.sig-backdrop is gone, and the overlay bg is not a translucent-black wash;
- OK'ing a card freezes the stage and reveals the DRY _stat_face.html —
.stat-face-title + .stat-chip-rank populate (the old reduced block had
neither; proves the populateStatExtras wiring).
RoomScrollLiveRefreshTest (ChannelsFunctionalTest, @tag channels):
- with the room open, a server-side record() of a new GameEvent grows the
feed (#id_drama_scroll .drama-event 1 → 2) WITHOUT a reload, via the
record() on_commit broadcast → RoomConsumer.scroll_update relay →
room-scroll.js re-fetch+swap. Validated in the CI channels stage (needs a
cross-process channel layer); the plumbing is already green via the
consumer-relay + record-hook + scroll_status ITs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The room's scroll-of-events feed only updated on refresh — a gamer
watching the SCROLL view never saw a co-player's deposit / role pick /
sig appear. Now every recorded GameEvent nudges all open room sockets to
re-fetch the feed.
- drama.models.record() broadcasts a `scroll_update` to the `room_<id>`
group via transaction.on_commit — so the live re-fetch sees the
committed row, and a rolled-back TestCase never fires it (zero overhead
/ channel-layer traffic for the plain IT suite). _broadcast_scroll_update
is fully guarded: a missing/unreachable channel layer must NEVER break
event recording (falls back to refresh-to-update). One central hook
covers every event writer, current + future.
- RoomConsumer gains a `scroll_update` relay handler (same one-liner shape
as gate_update / turn_changed).
- New `scroll_status` view + url (epic:scroll_status,
room/<id>/scroll/status) renders JUST core/_partials/_scroll.html with
the same events/viewer/scroll_position context as room_view's inline
paint, so the swapped feed is identical.
- room-scroll.js listens for `room:scroll_update`, fetches the partial,
swaps #id_drama_scroll, then re-applies the saved Frame/Redact filter +
restarts the buffer dots on the fresh nodes. URL comes from
.room-page[data-scroll-status-url]. Refactored the dots + filter into
re-runnable helpers; existing behavior (title reel IO, filter form,
localStorage) preserved.
TDD:
- drama RecordBroadcast ITs: record() schedules the broadcast on commit
(captureOnCommitCallbacks execute=True) and NOT before commit.
- RoomConsumer relays scroll_update (InMemory layer, WebsocketCommunicator).
- ScrollStatusViewTest: endpoint renders the feed section, reflects the
latest events, is the bare partial (no navbar/aperture chrome).
544 drama+epic ITs green — the on_commit hook is inert under TestCase, so
no existing event-writer test regressed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The in-room SIG_SELECT stage diverged from the polished GAME SIGN page:
a fixed dark-Gaussian modal over the hex, a stale label-only stat-block,
and no card imagery. This brings it in line with my_sign / my_sea.
A — Stat-block DRY: _sig_select_overlay.html now renders the shared
core/_partials/_stat_face.html (rank-chip + title + arcana + keywords)
instead of a reduced label-only copy; sig-select.js's updateStage() now
calls StageCard.populateStatExtras (the missing call that left those
fields blank). data-arcana-key added per card for title color-keying.
B — Per-card stage image: the stage card gains a .sig-stage-card-img
slot + data-image-url per thumbnail, so an image-equipped seat deck
(RWS / Minchiate) shows real card art on the preview. Thumbnails stay
glyph-only (rank + suit) at every deck — only the stage shows the image.
Keyed off each card's OWN deck_variant, so it auto-upgrades to mixed art
when the dubbodeck assembly lands. No backend change (cards already
carry a deck_variant via _room_deck_variant).
C — Felt-in-aperture: the stage renders INSIDE .room-hex-pane on edge-to-
edge green --duoUser felt (my_sea-style), replacing the hex content; the
old .sig-backdrop blur is gone. .sig-overlay absolute-fills the pane
(.room-hex-pane.has-sig-stage = positioning context); dismissing it
reveals the hex + waiting message behind. Scroll-down still reaches the
reelhouse carousel (untouched scroll pane).
Polishes:
- Image-mode bg escape: the levity 0,3,0 polarity rule
(.sig-overlay/.my-sign-page[data-polarity="levity"] .sig-stage-card)
hard-set a --secUser background that re-clothed image cards behind the
transparent PNG. Added the &.sig-stage-card--image { background:
transparent; border:0; overflow:visible } escape (parity w. the base +
my-sea rules). Latent my_sign bug too. Monodeck-era assumption.
- FLIP .btn-reveal: non-polarized image decks get a FLIP that turns the
preview to the deck card-back (my_sign parity) — back-img + reused
.my-sign-flip-btn (shared positioning/hide/counter-position rules
already cover .sig-stage-card) + a frozen-gated reveal scoped to
.sig-overlay + sig-select.js _flipToBack (500ms Y-rotate, midpoint
swap). SPIN now sets data-spinning so the btn hides mid-rotate.
- Reserved thumbs-up / hover cursors portal to a body-root fixed
container, so they hung over the reelhouse on scroll. sig-select.js now
toggles .cursors-hidden off the aperture scrollTop: instant hide the
moment the scroll leaves the hex, 0.5s opacity ease-in on the full
return. Tray intentionally kept.
TDD: SigSelectUnifiedStageTest (6 ITs) — DRY stat-face present, per-card
data-image-url + data-arcana-key, .sig-stage-card-img slot, image deck
non-empty face URL / text deck empty, has-sig-stage felt + overlay inside
the hex pane. 319 epic test_views ITs green; user-verified live on an RWS
room (no rect, FLIP works, thumb timing). Jasmine for the JS wiring +
the dubbodeck cross-deck assembly (per-seat segment cards, CARTE-solo
both-polarity case, per-card backs) are the tracked follow-on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gatekeeper's GATE VIEW is the canonical "who holds which position"
surface, but it reused _table_positions.html, which paints the
role-assigned fade-out class (opacity:0 / scale .5) server-side from
pos.role_assigned (slot <= assigned_count). So as gamers got seated
during Role Select, the gate-view circles vanished one-by-one in
lockstep with the table-hex — and by the time SCAN SIGS appeared (all
six roles assigned) the gate showed an empty hex. The disappear-as-
seated animation is meant as a TABLE-HEX-only cue.
Fix: _table_positions.html now suppresses role-assigned when its new
persist_circles flag is set; room_gate.html includes the partial with
persist_circles=True. room.html passes no flag (→ falsy), so the
table-hex keeps the fade animation untouched. No JS reads role-assigned
in the gate view (role-select.js isn't loaded there), so the server-side
guard is sufficient.
TDD: PositionTooltipRenderTest.test_gate_view_circles_never_fade_when_roles_assigned
— assigns all six roles, asserts the gate view keeps six .gate-slot
circles with NO role-assigned. Verified live via Claudezilla on a
SIG_SELECT setup_sig_session room. 47 render/gate ITs green
(RoleSelectRenderingTest still asserts the table-hex DOES fade).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- In landscape the wordmark is `writing-mode: vertical-rl` + `rotate(180deg)`. The rotation runs every reel child's translate in the rotated frame (inverting motion vs portrait), and the vertical-rl forces the flex MAIN axis onto the vertical, so the five view-word cells stack vertically.
- Vertical reel (hex⇄views): negate the translateY so ROOM exits UP + the view reel rises from BELOW (was dropping ROOM below the rising view).
- Lateral reel (view-to-view): the cells stack vertically, so the portrait `translateX` slid that stack sideways off the slot — only one word rendered (desktop showed PULSE, mobile ATLAS; `row`/`column`/`order`/`row-reverse` all collapse). Traverse with `translateY` instead: the words now slide up-down ALONG the rotated wordmark and each view lands its own word, consistently across desktop + mobile. (True left-right needs a track writing-mode override + reopens the rotate inversion — deferred; up-down is the working result.)
- `overflow: hidden` on the view reel clips it to its one-slot box so the neighbouring cell can't bleed over ROOM (the lateral reel now shares the vertical axis with the hex⇄views reel).
- Pure SCSS, verified visually across desktop + mobile landscape; portrait untouched (all rules inside the landscape media query).
[[feedback-vertical-rl-flex-axis]] [[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The reelhouse POST card now takes the billboard New Post applet's look: a --duoUser green felt bg with the rotated room-name `> h2` given the SAME green-tinted strip — three translucent 0,0,0/0.125 layers over the felt, NO opaque base, so the felt shows through (matches #id_applet_new_post, which lands there via a %applet-box > h2 specificity quirk; we do it on purpose).
- Each .post-line is restyled to LOOK LIKE the "Enter a post line" composer input below it (mirrors .form-control, _base.scss): a --priUser fill (0.8 alpha), a 0.1rem --secUser border at the same border-radius, full width, an up/down margin + content-driven (dynamic) height — so the thread reads as a stack of input-style pills on the felt. #id_post_table left/right padding zeroed so pills span the full card content width (= the composer row).
- OK button wrapped in .applet-btn-panel (--priUser fill + faint --terUser border) so the green .btn-confirm reads against the felt, mirroring the New Post composer.
- All scoped to .room-view--post — post.html (.post-page), the billboard New Post applet, and MY POSTS (.applet-list-entry) stay untouched (verified live: no .post-line bleed).
- Verified: GameViewsCarouselTest.test_post_view_is_room_thread_with_working_composer + test_atlas_aggregates_provenance_and_posts green (composer still works with the OK-panel wrap); billboard + GAME POST visually confirmed via Claudezilla.
[[feedback-scss-id-context-specificity-trap]] — %applet-box > h2's background-color inherits the applets-container ID via the @extend chain, out-specifying a plain #id_applet > h2 override, so an "opaque base" mask silently renders translucent (the felt bleeds through). New Post hit this by accident; GAME POST replicates it deliberately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bug (staging, touch only): a finger-swipe onto ATLAS landed blank ("The atlas gathers . . .") though SCROLL + POST had content; opening the ATLAS gear & clicking OK (no option change) made it appear. Desktop never reproduced.
- Cause: the ATLAS feed is a client-side merge of the live SCROLL + POST DOM. buildAtlasFeed ran in placeView (initial land) + goToView (icon-click / horizontal wheel) only. A native swipe is scroll-snap → it reaches the carousel solely through the IntersectionObserver, which called setActiveView('atlas') w. NO build. The gear-OK was the only other path that re-ran the merge.
- Fix: centralise the build in setActiveView — the single chokepoint placeView, goToView, AND the IO all share — so every activation path (incl. swipe) rebuilds; placeView/goToView no longer call buildAtlasFeed directly.
- Removed the empty-state (template + JS): ATLAS is never reached before SCROLL's game-creation "Welcome to <game>!" event, so rows is never bare; an empty render beats a stale placeholder lingering.
- Jasmine: new spec stubs window.IntersectionObserver, fires a synthetic atlas-intersect entry, asserts #id_room_atlas fills from the SCROLL/POST DOM w.o. any goToView or gear-OK (headless can't fire a real intersection — see the swipe-machine note). 473 specs green; the 3 ATLAS carousel FTs green (icon-click path + empty-state removal).
[[feedback-client-view-rebuild-on-io-swipe-path]] [[project-room-game-views-carousel]] [[feedback-headless-delayed-scroll-dropped]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unifies the Post applet across post.html and the room game-views POST chat:
- Extract _post_recipient.html (the @handle chip: post-recipient + post-attribution) + _post_header.html (title + 'shared between … & me' / 'just me' prose). post.html's owner branch + the reelhouse POST view both render via _post_header; the invitee branch stays bespoke but reuses the chip.
- Async-chip styling bug (post.html): the bud-invite append built a bare <span class=post-recipient> with the raw display name, so the recipient rendered without the --quaUser key + the @ until a refresh. Now billboard:share_post returns recipient_chip_html (the server-rendered _post_recipient.html) and the bud panel splices THAT in — identical classes + @handle. Also fixed the 'just me'→'& me' flip to mutate only the leading text node so the self line's own .post-attribution span survives.
- Reelhouse POST chat: gains the full .post-header — title hardcoded to 'Gamer Introduction' (dynamic template later) + recipients = the gamers OCCUPYING SEATS (room.table_seats, deduped, viewer excluded), NOT gate-slot/token depositors. And room_post ACCESS now requires a TableSeat, not a filled gate slot: a depositor who never took a seat can retract + leave, so they must not have R/W access to the private chat.
Tests: header IT (seatmate listed, transient gate-slot-only depositor not — scoped to the recipients paragraph since the position strip carries every gate-slot handle elsewhere); room_post seat-access ITs (seated OK; non-seated + gate-slot-only → 403); share_post recipient_chip_html IT; carousel FT setUp now seats disco/amigo/bud (pal/dude/bro stay transient). All green: 255 ITs, 11 carousel FTs, 29 bud-btn/composer FTs.
[[project-room-game-views-carousel]] [[feedback-at-handle-for-usernames]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each reelhouse view now has its own gear. The single #id_room_menu swaps panes by the active view (room-views.js owns this now — removed from room-scroll.js, which keeps only the title reel):
- hex → default NVM/DEL/BYE; SCROLL → the Frame/Redact log filter; ATLAS → a new source-checkbox pane (.room-menu-atlas / #id_atlas_source_form).
- YARN/POST/PULSE → no menu yet: the gear goes .gear-disabled (opacity 0.6) and an active click is swallowed by a capture-phase listener that flashes a --priRd fa-ban (the burger inactive-flash cadence) instead of opening anything.
ATLAS gear: a checkbox per other reelhouse view (Scroll/Post wired + checked; Yarn/Pulse disabled — struck label, an ✗ in a custom box matching the enabled ✓; starting-majuscule labels, capslock stays reel-only). OK persists to localStorage + re-runs buildAtlasFeed, which now gates each source on atlasSources() (scroll→provenance, post→post).
Also: the reelhouse POST composer drops its bespoke 'Speak at the table' placeholder for the canonical 'Enter a post line' (same as the billboard New Post applet's _form.html + post.html).
Verified: 11 carousel FTs (incl. the new per-view-gear FT) + 310 epic ITs (incl. the atlas-menu IT; room_gate shares _room_gear, unaffected) + the scroll-gear regression FTs + Jasmine, all green.
[[project-room-game-views-carousel]] [[feedback-applet-menu-needs-extend]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The merged ATLAS feed now reads its provenance rows the way SCROLL does:
- Struck (retracted/redacted) logs carry the strikethrough — buildAtlasFeed captures the source .drama-event-body.struck state and renderAtlasRow re-applies it as .atlas-row-body.struck (styled to match .drama-event-body.struck).
- A log hidden by the SCROLL Frame/Redact gear filter (display:none) is skipped in the ATLAS merge too — buildAtlasFeed checks getComputedStyle(ev).display, so unchecking Redact on SCROLL keeps those rows out of ATLAS.
- Also lays the source-toggle seam: atlasSources() reads the (forthcoming) ATLAS gear's view-checkboxes (scroll→provenance, post→post), defaulting to both when absent.
Verified: Jasmine renderAtlasRow struck spec + two FTs (struck row shows struck in ATLAS; redact-filtered rows absent from ATLAS) + the existing atlas aggregate FT, all green.
[[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CHAT conceptually overlapped POST, so it's gone — replaced by a distinct YARN view (fa-route) shifted one slot left, between SCROLL and POST. Reelhouse order is now ATLAS | SCROLL | YARN | POST | PULSE. YARN is a stub (the shared [Feature forthcoming] partial in its .applet-scroll, like PULSE).
Touched: the carousel + strip partials, the h2 reel words, the _base.scss data-active-view translateX reindex (yarn=-200%, post now -300%), room-views.js VIEW_ORDER, the Jasmine spec VIEWS, and the FT/IT order + stub assertions. The horizontal-wheel FT now expects scroll's neighbour to be YARN.
ATLAS timestamps: the merged rows carry the ORIGINAL <time> from their source (.drama-event-time from SCROLL, .post-line-time from POST), but those source rules are feed/thread-scoped so the atlas copies rendered full-size inline. Made .atlas-row a flex row and restated the shared small / dim / right-aligned look on both source time classes so each timestamp reads the same as in the view it came from.
Verified: 8 carousel FTs + carousel ITs + Jasmine (atlas merge + swipe machine) green.
[[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three things on the carousel:
- CHAT/PULSE stub: the shared [Feature forthcoming] partial centres itself absolutely, landing on top of the flex-centred watermark icon. Override it to flow (position:static) inside .room-view-stub so the icon keeps the top slot and the label rests below — FT asserts the icon's bottom clears the label's top (no clip).
- Adopt 'reelhouse' as the collective term for the fivefold applet-scroll carousel (class on #id_room_views + comments).
- Text sub-btn swipe machine: when already reel'd down onto the reelhouse, slide straight over to POST (plain goToView); when starting up in the room (the hex), run smooth DOWN to the reelhouse, HOLD 0.5s, then OVER to POST unless already there — the hold beats the two motions apart (DOWN-then-OVER, never diagonal).
Test rework: the from-hex OVER beat is a DELAYED (post-descent + hold) programmatic scroll, which headless Selenium drops/resets (works fine in a real browser), so the end-to-end land-on-POST can't be FT'd. Split it: the FT now asserts the reliable DESCENT beat (Text from the hex reveals the reelhouse + icon strip), and a new Jasmine swipe-machine spec pins the nav DECISION (from hex → POST after the descent+hold; inactive btn → no-op) against a fixture + mocked clock. room-views.js exposes init() so the spec can bind to the fixture.
Verified: 8 carousel FTs + Jasmine (atlas merge + swipe machine) green.
[[project-room-game-views-carousel]] [[feedback-ft-run-discipline]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The carousel's lateral nav animated wrong: the h2 reel slid VERTICALLY (old word down, new word up — both visible at once), and a first attempt that put translateX+translateY on one transform produced a DIAGONAL blend on hex->views (ROOM+ATLAS+SCROLL all flashing, always landing on ATLAS).
Fix — split the two axes onto NESTED elements so they never blend:
- OUTER .gr-views-reel does translateY ONLY, gated on .is-scroll (the hex<->views vertical reel, in lockstep with ROOM sliding up/out).
- INNER .gr-views-track does translateX -idx*100% (VIEW_ORDER atlas|scroll|post|chat|pulse), gated on data-active-view ALONE — so the active view's cell sits in the slot at ALL times, including at the hex. Default (pre-JS/unset) = SCROLL.
Result: hex<->views is a pure vertical reel that lands on whatever view you left off on (POST returns to POST, no diagonal); lateral nav is a pure horizontal slide — old word out one side, new in from the other, direction from the translateX sign — same rusty linear() sequence as ROOM<->SCROLL, just left-right.
Cards: goToView now smooth-scrolls (scrollTo behavior:smooth) instead of jumping scrollLeft, so the five .applet-scroll panes visibly SLIDE; an IO-suppression flag during the programmatic snap keeps the icon glow + reel from jittering through passed-over views (native touch-drag still updates via the IO). Initial land uses an instant placeView (no slide on arrival).
Verified: 8 carousel FTs + the GAME ROOM<->SCROLL vertical-reel FT green; a 3-lens adversarial audit (vertical axis / horizontal axis+clipping / repo-wide regression sweep) returned holds with no findings.
[[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Deepen .room-view bottom padding to 3.5rem so the .applet-scroll card ends above the absolute icon strip (bottom 0.85rem, ~2.1rem tall) — the strip now stands on its own in a reserved lane beneath the card instead of overlapping its bottom edge / the WHAT HAPPENS NEXT buffer.
- CHAT + PULSE stub bodies swap the ad-hoc 'Chat/Pulse opens soon.' line for the shared core/_partials/_forthcoming.html ([Feature forthcoming]); the identity icon + .room-view-stub wrapper stay, so the stub FT + styling are unaffected.
[[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups to the carousel sprint:
- The footer's billboard nav icon was fa-scroll, which now collides semantically with the carousel's SCROLL view; FA's billboard glyph is paywalled, so the billboard nav uses fa-receipt (ledger/scroll) instead. The footer FT selects by href, so navigation is unaffected.
- #id_room_views_strip was position:fixed (viewport bottom) and rendered down in the fixed footer/burger zone. Switched to position:absolute so it anchors to .room-page's bottom — and since the aperture fills .room-page via inset:0, that IS the bottom of the views pane, clear of the fixed footer. Still a sibling of the aperture (escapes the scroll-card fade mask + scroll clip); .room-page's overflow:hidden doesn't clip it (stays in bounds). Strip-visibility + landscape-within-viewport FTs still green.
[[project-room-game-views-carousel]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>