Commit Graph

363 Commits

Author SHA1 Message Date
Disco DeDisco
4aee5016c1 dubbodeck per-card FLIP back: the sig stage FLIPs each card to its OWN deck's back, not the seat's — finishes the cross-deck story — TDD
Pairs with the dubbodeck assembly: a mixed pile can now hold cards from up to three decks per polarity, so the single server-rendered back (the viewer's seat deck) was wrong for any cross-deck card. Now per-card: each .sig-card thumbnail carries data-back-image-url (its OWN deck back, only for non-polarized image decks; empty otherwise); stage-card.js fromDataset reads back_image_url + _setImageMode repoints the stage .sig-stage-card-back-img at the FOCUSED card's back and shows/hides the FLIP affordance per card (a backless card in the pile hides FLIP + drops any flipped state).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[[project-position-circle-tooltips]]

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

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

[[project-deck-segment-model]]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:39:52 -04:00
Disco DeDisco
9a00f96fe5 Sig Select qualifier on the stat-block — green the 3 theme FTs
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
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>
2026-06-03 14:28:45 -04:00
Disco DeDisco
71c00699a1 room Sig Select: unify with the my_sign card-stage apparatus (DRY stat-block, per-card stage image, --duoUser felt)
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>
2026-06-03 02:37:13 -04:00
Disco DeDisco
c4279c5515 room title reel: fix the landscape view-to-view + hex⇄views motion (vertical-rl axis)
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
- 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>
2026-06-03 01:13:09 -04:00
Disco DeDisco
fc49ca2c74 game post: New Post applet treatment — --duoUser felt, green-tinted title strip + input-pill .post-line rows
- 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>
2026-06-02 22:52:33 -04:00
Disco DeDisco
73644e226b game-views: per-view gear menus (SCROLL filter / ATLAS sources / disabled stubs); reelhouse POST placeholder DRY
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
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>
2026-06-02 15:34:34 -04:00
Disco DeDisco
9754f6a54c game-views ATLAS: mirror SCROLL log styling (strikethrough) + honour the redact filter
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>
2026-06-02 15:19:23 -04:00
Disco DeDisco
ced324081f game-views: replace CHAT with YARN (.fa-route) between SCROLL & POST; ATLAS timestamps match their source views
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>
2026-06-02 14:45:25 -04:00
Disco DeDisco
6f5927083c game-views: forthcoming watermark no-overlap; reelhouse term; Text swipe-machine DOWN-hold-OVER + Jasmine-tested nav
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>
2026-06-02 14:31:35 -04:00
Disco DeDisco
1c7f7d0adf game-views: horizontal direction-aware title reel + smooth card slide for lateral nav
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>
2026-06-02 14:00:20 -04:00
Disco DeDisco
be7a8c17f0 game-views: card ends above the icon strip; CHAT/PULSE use the [Feature forthcoming] partial
- 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>
2026-06-02 13:34:18 -04:00
Disco DeDisco
28d1d126bb game-views: footer billboard icon fa-scroll → fa-receipt; pin the views strip to the aperture bottom
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>
2026-06-02 13:25:04 -04:00
Disco DeDisco
f036c8f461 game-views carousel: ATLAS/SCROLL/POST/CHAT/PULSE views in the room scroll pane — TDD
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>
2026-06-02 13:05:36 -04:00
Disco DeDisco
62743aabd0 post composer: restore validation-error reveal broken by the .composer-row wrap; CI FT cleanup — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
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>
2026-06-02 02:54:31 -04:00
Disco DeDisco
cf1965c439 game room title: GAME ROOM ⇄ GAME SCROLL reel on the scroll aperture — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
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>
2026-06-02 01:05:00 -04:00
Disco DeDisco
114f0fd0db composer applets: .applet-btn-panel behind the OK btn for contrast on the --duoUser felt — TDD
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>
2026-06-02 00:15:11 -04:00
Disco DeDisco
22fc38b92b billboard applets view: stop the top-fade mask clipping the first applet
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>
2026-06-01 21:39:35 -04:00
Disco DeDisco
430c3bf141 composer applets: --duoUser felt + masked h2 (matching My Sky / My Sea / My Sign)
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>
2026-06-01 21:24:42 -04:00
Disco DeDisco
91f48384ff post composer: OK btn + orientation-aware right clamp; #id_post_line_text styling — TDD
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>
2026-06-01 21:06:51 -04:00
Disco DeDisco
d66d898f4d light palettes: --duoUser = --undUser felt so input/aperture fills blend with the page
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>
2026-06-01 20:47:12 -04:00
Disco DeDisco
39a2a49d7e New Post applet: kill #id_text remnant, add OK btn, bolden composer inputs — TDD
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>
2026-06-01 20:37:52 -04:00
Disco DeDisco
4a4e60f668 room scroll-of-events: applet-box card styling + scroll-driven gear menu (Frame/Redact filter) — TDD
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>
2026-06-01 20:03:48 -04:00
Disco DeDisco
a4adf9664b room scroll-of-events: table-hex aperture binary scroll-snaps to the room provenance feed — TDD
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>
2026-06-01 18:32:57 -04:00
Disco DeDisco
5229b9f96a slot/token tooltips: 'expires <relative>' (lowercase, .row-ts timescale) + per-slot token_cost on GateSlot + '+ <Token>' deposited-token list — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
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>
2026-06-01 17:39:15 -04:00