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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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 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>
- 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>
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>
Unskips the 8 RED FTs from the prior commit (test_game_room_views.py) and lands the feature beneath them — the room's 2nd vertical snap pane becomes a horizontal scroll-snap carousel of five views, landing on SCROLL (2nd).
Carousel core: _room_views.html (5 .room-view panes) + _room_views_strip.html (root-level icon strip, outside the aperture so it clears the scroll card's fade mask + scroll clip); room-views.js owns the horizontal axis — goToView (authoritative active-state) + an IntersectionObserver backing native swipe; horizontal wheel (deltaX / shift+wheel) advances a view while vertical wheel stays for feed scroll; icon-click snaps; the strip shows only while the views pane is on screen (vertical IO mirrors room-scroll.js). SCROLL still wraps _room_scroll.html, so the existing binary y-snap + provenance feed + GAME ROOM ⇄ GAME SCROLL title reel behave unchanged.
Title reel: the .gr-swap reel gains the four extra view words; the active word is driven by data-active-view on the h2 (set by room-views.js), gated by .is-scroll (room-scroll.js) so ROOM shows at the hex.
POST view: a room-scoped game-table thread. New Post.room FK + KIND_ROOM_THREAD (mirrors Brief.room) + Room.get_thread_post(); epic:room_post AJAX endpoint appends a Line (seated-gamer-gated, dup-rejected) and returns the rendered line partial. _post_line.html extracted from post.html and shared by both surfaces + the endpoint. The composer appends OPTIMISTICALLY (synchronous line so the POST + ATLAS views reflect it the instant OK is clicked, no dependence on the round-trip), then reconciles with the server's authoritative @handle/timestamp render; a rejection rolls the optimistic line back.
ATLAS view: a live client-side merge of the SCROLL provenance rows + the POST thread rows, time-ordered, each row tagged data-source=provenance|post for end-of-sprint per-type styling. Rebuilds from the live DOM on activation + on every composer append. CHAT/PULSE are .room-view-stub placeholders (no backing model yet).
Burger Text sub-btn lights .active on the table (text_btn_active from epic.room_view, unset on every other _burger.html surface) → room-views.js binds its active click to the swipe machine: DOWN to the views pane, RIGHT to Post.
Coverage: 8 carousel FTs green; Jasmine RoomViewsSpec (atlas merge order/stability + row data-source); epic ITs (Room.get_thread_post, carousel markup, room_post endpoint 200/403/400/GET); 1636 ITs/UTs + the existing scroll FT green (no regression).
Gotcha logged: build FormData(form) BEFORE clearing the input on optimistic submit — clearing first captures an empty text field → 400 → the line silently rolls back.
[[project-room-game-views-carousel]] [[project-room-scroll-of-events]] [[project-room-title-scroll-reel-jun02]] [[feedback-jsonfield-exclude-sqlite-null]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OK-button composer redesign wrapped the line input in .composer-row, so the .form-control.is-invalid input is no longer a general SIBLING of its .invalid-feedback — the `&.is-invalid ~ .invalid-feedback` reveal (\_base.scss) silently stopped matching, so post-line validation errors rendered in the DOM but stayed display:none (invisible to users). Reveal via `.composer-row:has(.form-control.is-invalid) ~ .invalid-feedback`. Greens test_cannot_add_duplicate_lines + test_error_messages_are_cleared_on_input (both were catching this real regression, not flaky).
Harden WalletShopFreeDeckTest: the .tt-micro is briefly detached mid-HTMX-swap, so get_attribute('innerHTML') returns None and a bare assertIn raises TypeError — which wait_for does NOT retry. Coalesce to '' so it polls until the swap settles (explains the local-pass / CI-fail).
Delete test_core_styling.test_layout_and_styling: a Percival-era assertion that the post input is horizontally CENTRED in its section. The responsive .composer-row (input + OK btn) + the orientation-aware right-margin clamp intentionally removed that invariant (the input now lands in different spots per viewport). Zero behavioural coverage lost — the composer is covered by LineValidationTest + PostComposerOkButtonTest.
Skip GameViewsCarouselTest (red planning contract for the unbuilt Game-views carousel — see project-room-game-views-carousel).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The h2's second slot becomes a two-word vertical reel: GAME stays put, ROOM rests in view, SCROLL is parked one notch below in the slot's bottom fade. room-scroll.js toggles `.is-scroll` on the h2 from the SAME IntersectionObserver that already watches the table-hex aperture's scroll pane — ROOM slides up & out under the navbar line while SCROLL rises out of the page-aperture gradient (reverses on scroll-up). Table-phase only; the gate phase stays a plain GAME ROOM.
One translateY drives both orientations. Portrait: the word is a short horizontal row in a short slot. Landscape: writing-mode: vertical-rl (inherited from the rotated gutter wordmark) makes the word a tall letter-column, so the same translateY slides it ALONG the wordmark — the user-chosen landscape behaviour for free. Landscape uses a shallower --gr-fade + a letter inset so the space-between end-letters parked at the slot edges aren't dimmed by the dissolve.
Motion is deliberately old & rusty: a single cubic-bezier can overshoot at most once and can't oscillate, so the easing is a CSS linear() curve — stall against the grime, jerk free, clunk PAST the mark, then a damped end-wobble into place. Exposed as --gr-ease / --gr-dur / --gr-fade knobs on .gr-swap.
base.html's letter-splitter now also splits the two .gr-word words; the .gr-swap window ships data-letters-split="1" so the splitter skips it (no 'roomscroll' run). Reel SCSS is scoped to .gr-swap/.gr-word; `> span.gr-swap` ties `> span:last-child` at (0,4,3) and wins on later source order [[feedback-scss-import-order-specificity]].
TRAP: libsass does NOT strip `//` comments INSIDE a CSS custom-property value — they leak into the compiled output, making the linear() (hence the whole `transition` shorthand) invalid-at-computed-value-time, which silently resets to 0s/ease (no animation). Keep every annotation OUTSIDE the linear(). [[feedback-libsass-comment-in-custom-property]]
Reusable .gr-swap seam: my_sea gets GAME SEA → GAME SCROLL via a one-line header swap once its sea-scroll pane is built (deferred — the sea scroll doesn't exist yet).
Tests: 2 ITs (RoomScrollOfEventsTest) — reel markup renders in the table phase, stays plain in the gate phase; 1 FT (test_scroll_swaps_room_title_to_scroll) — scrolling the aperture toggles GAME ROOM ⇄ GAME SCROLL both ways. collectstatic'd room-scroll.js for the FT [[feedback-collectstatic-before-ft]].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Giving New Game / New Post the --duoUser green felt made the green OK .btn-confirm hard to see. Wrap it in an .applet-btn-panel — a --priUser fill + faint --terUser border, mirroring .gate-roles-panel (_room.scss) with tighter padding so it stays snug beside the line input — so the button reads clearly against the felt.
Applied in _applet-new-game.html and the shared _form.html (New Post now; room.html's composer inherits it when ported).
Tests: GameboardViewTest.test_new_game_ok_button_in_applet_btn_panel (OK btn inside #id_applet_new_game .applet-btn-panel); NewPostTest.test_new_post_applet_has_ok_confirm_button extended to assert the panel wrap. The New Game OK btn stays clickable inside the panel (GatekeeperTest.test_founder_creates_room_and_sees_gatekeeper FT green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The billboard applets view (.billboard-page) was a plain page-level scroller, unlike .gameboard-page / .dashboard-page (flex columns whose applet grid fills the aperture and scrolls internally). So #id_billboard_applets_container (flex:1, from %applets-grid) had no flex parent — it grew to its full content height, and the shared %applets-grid `transparent 0% -> black 2%` top-fade became 2% of that tall box, fading away the top of the first applet (newly visible now that New Post carries the --duoUser felt).
Fix — match the other two boards:
- .billboard-page: display:flex; flex-direction:column; overflow:hidden (overrides %billboard-page-base's overflow-y:auto).
- #id_billboard_applets_wrapper (the extra hx-swap wrapper the other boards don't have): display:flex; flex-direction:column; flex:1; min-height:0 — passes the flex height through so the grid is aperture-constrained with an internal scroll. Its top-fade is now the same small fade as gameboard/dashboard.
Verified: BillboardAppletsTest.test_billboard_shows_three_applets + test_toggling_applets_keeps_content_and_persists_per_applet green (the pair-run flake was the known Selenium memory-pressure NoSuchWindow, not a regression — both pass in isolation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The New Game and New Post composer applets (#id_applet_new_game / #id_applet_new_post) now take the same treatment as the My Sky / My Sea / My Sign applets: a --duoUser green-felt content bg with the rotated >h2 title bar masked back to the default dark.
The h2 mask is an opaque --priUser base under the same two 0,0,0/0.125 overlays the %applet-box + its >h2 give every normal applet — so the title bars read identically dark instead of green-tinted, and it stays palette-correct on *-light palettes (no flat 0,0,0). The same mask was applied to the three #id_applet_my_* shells, whose h2 had been left as pure --priUser (lighter than the other applets).
Composer line-inputs (#id_new_game_name / #id_new_post_text / #id_post_line_text) keep a --priUser fill + faint --secUser placeholder + 700 weight + --terUser focus, standing out against the felt like My Sky's form fields.
Bundled: palette felt retunes (rootvars --terFor / --terPer).
[[project-room-scroll-of-events]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Billpost composer (#id_post_line_text) now matches the other composers and prepares the pattern for room.html.
1. OK .btn-confirm (#id_post_line_btn) added to the editable post-line form, in a flex .composer-row beside the input (read-only system Posts — note_unlock / tax_ledger / mail_acceptance — invite no response, so get no OK). #id_post_line_text also joins the composer-input styling selector in _applets.scss (700 weight + duoUser fill + terUser focus + priUser placeholder).
2. Orientation-aware right clamp on .post-line-form so the OK btn clears the bottom-right corner button:
- portrait: margin-right 3.5rem — clears the gear (.post-page > .gear-btn, right:0.5rem, 3rem wide → 3.5rem slot).
- landscape: margin-right 7.2rem — clears the burger slot (#id_burger_btn lands at right:4.2rem, 3rem wide → its left edge sits 7.2rem from the viewport's right edge; this also clears the bud at right:0.5rem). post.html carries no burger (room.html will) — the slot is reserved for parity.
.composer-row is flex (input flex:1 + OK); the read-only input keeps width:100%.
Bundled: rootvars --terPer felt retune (82,71,138).
Tests: PostViewTest ITs (editable post renders the OK .btn-confirm in .composer-row; read-only system post does not); functional_tests/test_bill_post_composer FTs (portrait OK right edge <= gear left edge; landscape OK right edge >= 7.2rem in from the viewport edge). 12 PostViewTest ITs + 442 dashboard/billboard ITs + 2 composer FTs + test_bill_new_post FT (ENTER-submit through the composer-row) green.
[[project-room-scroll-of-events]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On *-light palettes the dark-olive --duoUser felt (terFor) read too heavy as an input / aperture fill against the light page. The light-palette override block (body[class*="-light"]) now sets --duoUser: var(--undUser), so every --duoUser surface — e.g. oblivion-light's New Game / New Post composer inputs — uses the lighter forest felt and blends in. Overrides the :root --duoUser for every *-light palette (body[class*="-light"] out-specifies :root).
Composer polish (_applets.scss): the New Game / New Post line inputs gain an explicit --duoUser background fill + a --priUser placeholder colour, alongside the 700 weight + terUser focus shift already added. Forest felt (priFor) retuned in rootvars to suit the lighter light-palette fill.
[[project-room-scroll-of-events]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prep for porting a post composer into room.html. Three changes on the New Game / New Post applets:
1. Renamed the old Percival #id_text → #id_new_post_text. _form.html (the shared post composer) is now parameterized on input_id (default id_new_post_text) + submit_id (default id_new_post_btn); the feedback div + aria-describedby track {{ input_id }}_feedback. _applet-new-post.html includes it with input_id="id_new_post_text". room.html will reuse the same partial with input_id="id_room_post_text". Refs updated: _scripts.html (initialize("#id_new_post_text")), Jasmine Spec.js, FT post_page.py. _form.html has exactly one includer.
2. Added an OK .btn-confirm submit to _form.html (flex .composer-row: line input + OK, validation feedback below), mirroring the New Game applet. ENTER still submits, so the existing add_post_line FTs stay green.
3. Composer-input styling in _applets.scss: #id_new_game_name, #id_new_post_text { font-weight: 700; &:focus { color: --terUser } } — mirrors .sky-form-col input. The duoUser fill / secUser default text / terUser border+glow on focus already come from .form-control; this adds the 700 weight + the focus text-colour shift. room's #id_room_post_text joins this selector list when it lands.
Tests: NewPostTest ITs (conventional id + aria-describedby, OK .btn-confirm). 440 dashboard+billboard ITs green; Jasmine spec green with the renamed id; test_bill_new_post FT green (renamed input + OK btn, ENTER submit).
[[project-room-scroll-of-events]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The room scroll pane now matches scroll.html: the feed sits in a .applet-scroll %applet-box card with the rotated room-name title; dropped the special --duoUser pane bg (the dark card sits on the room-page bg).
Gear menu is now view-aware. #id_room_menu carries two panes: .room-menu-default (the existing NVM/DEL/BYE) + .room-menu-scroll (a Frame/Redact #id_scroll_filter_form, rendered only when the gear include gets scroll_filter — room.html passes scroll_filter=room.table_status). room-scroll.js (NEW) runs an IntersectionObserver on .room-scroll-pane (root=#id_room_aperture): scrolled to the feed -> show the filter pane; back on the hex -> show the default. The filter mirrors scroll.html (per-room localStorage, toggles .drama-event[data-label] display). Buffer-dots animation moved from the inline partial script into room-scroll.js.
Other views keep their own menus, as asked: GATE VIEW (room_gate.html) includes _room_gear.html with nvm_url only (no scroll_filter, no room-scroll.js) -> NVM(->hex)/DEL/BYE; the cross/spread phase is a modal over the hex (scrollTop 0) -> default pane.
Traps: applets.js caches gear.dataset.menuTarget at bind time, so you can't swap a gear's target to a 2nd menu — both panes live in ONE #id_room_menu and JS toggles visibility. .room-menu-default is display:contents so wrapping the existing controls doesn't change their layout (JS toggles none<->contents, not '').
Tests: +3 ITs (RoomScrollOfEventsTest — .applet-scroll card + room-name title, filter pane renders in table phase, filter absent in gate phase); +2 FTs (test_game_room_scroll — gear swaps to filter when scrolled to feed, unchecking Redact+OK hides struck rows). 8 scroll ITs + 4 scroll FTs green; 554 epic ITs/UTs green; gatekeeper DEL+BYE gear FTs green (the .room-menu-default wrap is layout-neutral).
[[project-room-scroll-of-events]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From Role Select onwards, scrolling DOWN in the table-hex aperture swaps the entire hex view for the room's GameEvent feed (mirrors my_sky's wheel<->form; scroll-snap-stop:always = no partial scroll). Reuses the Billscroll query + core/_partials/_scroll.html so the feed renders identically.
room.html: #id_room_aperture wraps .room-hex-pane (existing .room-shell + the _table_positions strip moved INSIDE so the circles scroll away with the hex) + .room-scroll-pane (includes new _room_scroll.html); 'is-scrollable' added iff table_status set.
_room_scroll.html (new) = the DRY seam for my_sea; includes the shared scroll partial + a tiny dots-animation script (no scroll-position persistence). room_view adds events/viewer/scroll_position (same query as billboard.views.scroll).
_room.scss: .room-aperture + .room-pane (height:100%, not min-height); .is-scrollable engages scroll-snap-type:y mandatory + per-pane scroll-snap-align:start & scroll-snap-stop:always; .room-scroll-pane styles #id_drama_scroll + .scroll-buffer { margin-top:auto } (pure-CSS bottom-pin).
Trap: the aperture & panes set NO z-index/transform/opacity/filter -> NO stacking context, so the position strip's z-130 still resolves in the root context, above the gate/sig overlays (z-100/120). Verified by gatekeeper FTs (token drop + circle/modal layering).
Deferred INDEFINITELY (user): rising-game-cost + max room membership + max simultaneous CARTE slots + in-slot token combinations — until CARTE is anything but a secret type of Trinket.
Tests: RoomScrollOfEventsTest (5 ITs — aperture wraps hex+scroll panes, is-scrollable from Role Select, feed renders + scoped to room, no scroll pane in gate phase); functional_tests/test_game_room_scroll.py (2 FTs — computed scroll-snap props + scroll-down reveals the feed). 551 epic ITs/UTs green; 2 new FTs green; gatekeeper (token-drop + circle layering) + role-select (card fan) FTs green.
[[project-room-scroll-of-events]] [[project-position-circle-tooltips]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three pieces of housekeeping on the token tooltips:
1. Expiry format. relative_ts is now distance-based (abs gap from now), so it formats FUTURE expiries with the same timescale rules it already uses for past .row-ts timestamps (<24h time, <7d weekday, <1y 'dd Mon', else +year) — past behaviour unchanged (abs is a no-op for past). The FREE-token wallet tooltip (Token.tooltip_expiry) and the position-circle .tt-expiry both read 'expires <when>' (lowercase, no majuscule); the position tooltip is server-formatted via the filter (JS just copies the attr — no JS date logic).
2. Per-slot token count moved off the user's CARTE token onto the slot model. New GateSlot.token_cost (PositiveSmallIntegerField default 1) — the per-seat expenditure count. _gate_positions reads slot.token_cost instead of the CARTE Token.slots_claimed high-water mark, which wrongly showed '6' on every CARTE-covered seat. Every slot now reads 1 (a CARTE covers each seat at cost 1, like any token); the field only rises above 1 when the rising-game-cost feature lands.
3. Per-slot deposited-token list. Under the '<n> Token(s) deposited' header the tooltip now lists a '+ <Token name>' bulleted <ul> — one entry today (a slot ejects its token on any re-deposit, so combinations aren't yet possible). Derived from the slot's debited_token_type (e.g. 'carte' -> 'Carte Blanche', 'Free' -> 'Free Token'); a CARTE across all six seats shows '+ Carte Blanche' on each. token_types is a list, future-ready for token combinations + elevated per-slot cost.
Rising-game-cost is NOT built (recon-confirmed), so the per-slot count is always 1 and the 2-token-slot FT is intentionally skipped per user.
Tests: relative_ts future-date unit tests; FreeTokenTooltipTest rewritten for the relative format (real datetime, no MagicMock/strftime); wallet FT + the two CARTE token-count tests updated to per-slot semantics (1 + 'Carte Blanche'); FREE-slot IT asserts the token-list + 'expires '. Full suite 1606 green; 11 position-tooltip FTs + wallet tooltip FT green.
[[project-position-circle-tooltips]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>