2026-05-12 22:48:32 -04:00
|
|
|
// ── Shared scrollable applet list (.applet-list) ───────────────────────────
|
|
|
|
|
// In-grid applet partials (My Posts, My Buds, My Notes, My Scrolls, My Games)
|
|
|
|
|
// + the dedicated applet-list pages (billbuds, billposts) all wrap items in a
|
|
|
|
|
// `<ul class="applet-list">`. The list rules live at top level so the same
|
|
|
|
|
// item-entry styling applies in both surfaces; per-context wrappers below
|
|
|
|
|
// handle flex sizing so the list scrolls inside its parent's aperture.
|
|
|
|
|
|
|
|
|
|
.applet-list {
|
|
|
|
|
list-style: none;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0 0.75rem 0 0;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
// Flex-column lets the empty-state entry fill the aperture so it can
|
|
|
|
|
// centre vertically. Fires only when the list is entirely empty —
|
|
|
|
|
// as soon as a real .applet-list-entry lands, layout reverts to the
|
|
|
|
|
// default left-aligned vertical stack.
|
|
|
|
|
&:has(> .applet-list-entry--empty) {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Empty-state filler (`No <X> yet.` rows). Centres in any flex-column
|
|
|
|
|
// parent — the .applet-list above OR a `display: flex; flex-direction:
|
|
|
|
|
// column` applet section directly (e.g. #id_applet_most_recent_scroll
|
|
|
|
|
// when no Room has events yet).
|
|
|
|
|
.applet-list-entry--empty {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
text-align: center;
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
font-style: italic;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
applet rows: 3-col grid `<title> | <body> | <ts>` mirroring post.html's `.post-line` shape — `_my_posts_applet_item / _my_buds_applet_item / _my_notes_item / _my_scrolls_item / _my_games_item` all gain a `.applet-list-entry.row-3col` w. `<a class="row-title">` (clickable, 35c/32+... server-side truncated via new `lyric_extras.truncate_title` filter) + `<span class="row-body">` (most-recent activity excerpt, dimmed 0.6 opacity, CSS-`text-overflow: ellipsis` clipped to whatever space remains — no server-side trunc here so the full line lives in the DOM for inspectors) + `<time class="row-ts">` (`relative_ts` formatted, same `minmax(3rem,auto)` rightward column allocation post.html's `.post-line-time` uses, font-size 0.75rem + opacity 0.5 + right-aligned + nowrap); SCSS grid `minmax(4rem,auto) 1fr minmax(3rem,auto)` lifted from `.post-line`'s template so the timestamp column lines up across post.html / scroll.html / every applet list; per-applet data shapes — `_recent_posts` annotates each Post w. `latest_line` (Line FK ordered by -id, None for empty Note-unlock posts); `_recent_buds` `select_related('to_user__active_title')` warms the bud's donned-Note FK in one query for the buds row body ("the {{ bud.active_title_display }}" + "since {{ bud.active_title.earned_at|relative_ts }}" — the "since " prefix is unique to this row since the ts is "when they donned it", not the row's own creation); `_recent_notes` attaches `description` from `_NOTE_META` per slug; `annotate_latest_event(rooms)` helper added to `apps.epic.utils` (next to `rooms_for_user`) — attaches `room.latest_event` per Room w. one `.events.order_by('-timestamp').first()` per item, used by `_billboard_context` for `my_rooms` (My Scrolls applet) AND by `apps.gameboard.views.gameboard` + `toggle_game_applets` for `my_games` (My Games applet), keeping the My Scrolls + My Games shapes symmetric; `_billboard_context.my_rooms = annotate_latest_event(...)` swaps `rooms_for_user(...).order_by("-created_at")` materialisation point — bud row's "no active title" branch silently drops body + ts cells so unrecognised buds still surface but don't fabricate a "since None" line; new `truncate_title` filter is the existing `_truncate_post_title` view helper hoisted into the template namespace (literal `...` past 35 chars, None-safe); 5 ITs in BillboardViewTest cover row content / row absence on missing activity / "since" prefix uniquely on the buds row + 1 in GameboardViewTest for My Games row event prose; deferred row-prose body content cap on `<span class="row-body">` purely to CSS `text-overflow: ellipsis` per user's "middle col should take up the remaining space" steer (initial pass also server-side trunc'd the body to 35c; removed) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:06:55 -04:00
|
|
|
// 3-col applet row — `<title> | <body> | <ts>`. Mirrors post.html's
|
|
|
|
|
// `.post-line` grid (`minmax(4rem,auto) 1fr minmax(3rem,auto)`) so the
|
|
|
|
|
// rightward ts column lines up across post.html, scroll.html, and every
|
|
|
|
|
// applet list. Title gets the project-wide 35char/32+... truncation via
|
|
|
|
|
// the `truncate_title` template filter; body is dimmed to 0.6.
|
|
|
|
|
.applet-list-entry.row-3col {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(4rem, auto) 1fr minmax(3rem, auto);
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: 0.5rem;
|
2026-05-13 00:27:39 -04:00
|
|
|
padding-left: 0.5rem;
|
|
|
|
|
padding-right: 0.5rem;
|
|
|
|
|
border-radius: 0.25rem;
|
|
|
|
|
transition: background-color 0.12s ease, color 0.12s ease;
|
applet rows: 3-col grid `<title> | <body> | <ts>` mirroring post.html's `.post-line` shape — `_my_posts_applet_item / _my_buds_applet_item / _my_notes_item / _my_scrolls_item / _my_games_item` all gain a `.applet-list-entry.row-3col` w. `<a class="row-title">` (clickable, 35c/32+... server-side truncated via new `lyric_extras.truncate_title` filter) + `<span class="row-body">` (most-recent activity excerpt, dimmed 0.6 opacity, CSS-`text-overflow: ellipsis` clipped to whatever space remains — no server-side trunc here so the full line lives in the DOM for inspectors) + `<time class="row-ts">` (`relative_ts` formatted, same `minmax(3rem,auto)` rightward column allocation post.html's `.post-line-time` uses, font-size 0.75rem + opacity 0.5 + right-aligned + nowrap); SCSS grid `minmax(4rem,auto) 1fr minmax(3rem,auto)` lifted from `.post-line`'s template so the timestamp column lines up across post.html / scroll.html / every applet list; per-applet data shapes — `_recent_posts` annotates each Post w. `latest_line` (Line FK ordered by -id, None for empty Note-unlock posts); `_recent_buds` `select_related('to_user__active_title')` warms the bud's donned-Note FK in one query for the buds row body ("the {{ bud.active_title_display }}" + "since {{ bud.active_title.earned_at|relative_ts }}" — the "since " prefix is unique to this row since the ts is "when they donned it", not the row's own creation); `_recent_notes` attaches `description` from `_NOTE_META` per slug; `annotate_latest_event(rooms)` helper added to `apps.epic.utils` (next to `rooms_for_user`) — attaches `room.latest_event` per Room w. one `.events.order_by('-timestamp').first()` per item, used by `_billboard_context` for `my_rooms` (My Scrolls applet) AND by `apps.gameboard.views.gameboard` + `toggle_game_applets` for `my_games` (My Games applet), keeping the My Scrolls + My Games shapes symmetric; `_billboard_context.my_rooms = annotate_latest_event(...)` swaps `rooms_for_user(...).order_by("-created_at")` materialisation point — bud row's "no active title" branch silently drops body + ts cells so unrecognised buds still surface but don't fabricate a "since None" line; new `truncate_title` filter is the existing `_truncate_post_title` view helper hoisted into the template namespace (literal `...` past 35 chars, None-safe); 5 ITs in BillboardViewTest cover row content / row absence on missing activity / "since" prefix uniquely on the buds row + 1 in GameboardViewTest for My Games row event prose; deferred row-prose body content cap on `<span class="row-body">` purely to CSS `text-overflow: ellipsis` per user's "middle col should take up the remaining space" steer (initial pass also server-side trunc'd the body to 35c; removed) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:06:55 -04:00
|
|
|
|
|
|
|
|
.row-title {
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.row-body {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.row-ts {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
text-align: right;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
2026-05-13 00:27:39 -04:00
|
|
|
|
|
|
|
|
// Hover (mouse) / click-lock (touch + click persistence) — both states
|
|
|
|
|
// share the highlight. JS in `apps/applets/row-lock.js` toggles
|
|
|
|
|
// `.row-locked` on click; `:hover` is pure CSS. Background to --secUser,
|
|
|
|
|
// title to --quaUser, dimmed body + ts brought to full opacity in
|
|
|
|
|
// --priUser (the back-of-card colour, readable against the --secUser
|
|
|
|
|
// fill).
|
|
|
|
|
&:hover,
|
|
|
|
|
&.row-locked {
|
|
|
|
|
background-color: rgba(var(--secUser), 1);
|
|
|
|
|
|
|
|
|
|
.row-title,
|
|
|
|
|
a.row-title { color: rgba(var(--quiUser), 1); text-shadow: none; }
|
|
|
|
|
|
|
|
|
|
.row-body,
|
|
|
|
|
.row-ts { color: rgba(var(--priUser), 1); opacity: 1; }
|
|
|
|
|
}
|
applet rows: 3-col grid `<title> | <body> | <ts>` mirroring post.html's `.post-line` shape — `_my_posts_applet_item / _my_buds_applet_item / _my_notes_item / _my_scrolls_item / _my_games_item` all gain a `.applet-list-entry.row-3col` w. `<a class="row-title">` (clickable, 35c/32+... server-side truncated via new `lyric_extras.truncate_title` filter) + `<span class="row-body">` (most-recent activity excerpt, dimmed 0.6 opacity, CSS-`text-overflow: ellipsis` clipped to whatever space remains — no server-side trunc here so the full line lives in the DOM for inspectors) + `<time class="row-ts">` (`relative_ts` formatted, same `minmax(3rem,auto)` rightward column allocation post.html's `.post-line-time` uses, font-size 0.75rem + opacity 0.5 + right-aligned + nowrap); SCSS grid `minmax(4rem,auto) 1fr minmax(3rem,auto)` lifted from `.post-line`'s template so the timestamp column lines up across post.html / scroll.html / every applet list; per-applet data shapes — `_recent_posts` annotates each Post w. `latest_line` (Line FK ordered by -id, None for empty Note-unlock posts); `_recent_buds` `select_related('to_user__active_title')` warms the bud's donned-Note FK in one query for the buds row body ("the {{ bud.active_title_display }}" + "since {{ bud.active_title.earned_at|relative_ts }}" — the "since " prefix is unique to this row since the ts is "when they donned it", not the row's own creation); `_recent_notes` attaches `description` from `_NOTE_META` per slug; `annotate_latest_event(rooms)` helper added to `apps.epic.utils` (next to `rooms_for_user`) — attaches `room.latest_event` per Room w. one `.events.order_by('-timestamp').first()` per item, used by `_billboard_context` for `my_rooms` (My Scrolls applet) AND by `apps.gameboard.views.gameboard` + `toggle_game_applets` for `my_games` (My Games applet), keeping the My Scrolls + My Games shapes symmetric; `_billboard_context.my_rooms = annotate_latest_event(...)` swaps `rooms_for_user(...).order_by("-created_at")` materialisation point — bud row's "no active title" branch silently drops body + ts cells so unrecognised buds still surface but don't fabricate a "since None" line; new `truncate_title` filter is the existing `_truncate_post_title` view helper hoisted into the template namespace (literal `...` past 35 chars, None-safe); 5 ITs in BillboardViewTest cover row content / row absence on missing activity / "since" prefix uniquely on the buds row + 1 in GameboardViewTest for My Games row event prose; deferred row-prose body content cap on `<span class="row-body">` purely to CSS `text-overflow: ellipsis` per user's "middle col should take up the remaining space" steer (initial pass also server-side trunc'd the body to 35c; removed) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 23:06:55 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 22:48:32 -04:00
|
|
|
.applet-list-entry {
|
|
|
|
|
padding: 0.4rem 0;
|
|
|
|
|
|
|
|
|
|
.bud-name { font-weight: bold; opacity: 0.85; }
|
|
|
|
|
|
|
|
|
|
a {
|
|
|
|
|
color: rgba(var(--terUser), 1);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
transition: text-shadow 0.15s ease;
|
rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests
- new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts
- drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette
- recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-*
- _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes)
- NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py
- 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:32:34 -04:00
|
|
|
|
2026-05-12 22:48:32 -04:00
|
|
|
&:hover,
|
|
|
|
|
&:active {
|
|
|
|
|
color: rgba(var(--ninUser), 1);
|
|
|
|
|
text-shadow: 0 0 0.55rem rgba(var(--terUser), 0.7);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.applet-list-buffer {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
height: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In-grid applet sections: flex-column so the .applet-list can flex:1
|
|
|
|
|
// and scroll within the applet box. Left-aligned items across the
|
|
|
|
|
// board (My Games used to centre — symmetrised w. the rest 2026-05-12).
|
|
|
|
|
|
|
|
|
|
#id_applet_my_posts,
|
|
|
|
|
#id_applet_my_buds,
|
|
|
|
|
#id_applet_my_scrolls,
|
|
|
|
|
#id_applet_notes {
|
rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests
- new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts
- drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette
- recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-*
- _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes)
- NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py
- 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:32:34 -04:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
2026-05-12 22:48:32 -04:00
|
|
|
.applet-list {
|
rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests
- new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts
- drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette
- recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-*
- _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes)
- NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py
- 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:32:34 -04:00
|
|
|
flex: 1;
|
2026-05-12 22:48:32 -04:00
|
|
|
padding-top: 0.25rem;
|
rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests
- new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts
- drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette
- recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-*
- _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes)
- NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py
- 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:32:34 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 16:46:46 -04:00
|
|
|
// ── Shared aperture fill for both billboard pages ──────────────────────────
|
aperture architecture: lift the page-locking foundation (html/body/.container overflow:hidden + flex-column + min-height:0; .row flex-shrink:0) from 5 per-page SCSS files into _base.scss — was opt-in per page via `body.page-billboard` / `page-dashboard` / `page-gameboard` / `page-sky` / `page-wallet` etc., with 5 near-identical `html:has(body.page-X) { overflow: hidden }` + `body.page-X { … }` blocks duplicating the same rules; any page that forgot to set `page_class` in its view context (e.g. `epic.tarot_deck` — never set) rendered without the aperture, letting applet borders + titles clip past the fixed navbar/footer sidebars at narrower viewports; foundation now universal, page-specific overrides stay scoped — gameboard keeps `.container { overflow: clip }` (Firefox seat-tooltip scroll-anchoring quirk) + billboard/dashboard/gameboard keep `.row { margin-bottom: -1rem }` (h2-row tightening); page_class context vars + body class hooks preserved (FTs at test_bud_btn.py:370 / :379 still assert on them); regression gate: 60 layout-sensitive FTs (billboard, my_buds, bud_btn, applet_my_posts, dashboard, wallet, gameboard, layout_and_styling, jasmine) + 43 room FTs (gatekeeper_bud_btn, room_gatekeeper, room_sky_select, sharing) all green
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:16:12 -04:00
|
|
|
//
|
|
|
|
|
// Aperture foundation (html/body/.container overflow + flex-column +
|
|
|
|
|
// .row flex-shrink: 0) now lives universally in _base.scss. Only the
|
|
|
|
|
// page-specific `.row { margin-bottom: -1rem }` pull (tightening the
|
|
|
|
|
// h2 row against subsequent applet content) stays here, since wallet
|
|
|
|
|
// + sky pages deliberately don't carry that pull.
|
2026-03-24 16:46:46 -04:00
|
|
|
|
|
|
|
|
%billboard-page-base {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body.page-billboard,
|
post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD
- schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner).
- lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed).
- drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default.
- billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title.
- post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio).
- SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square.
- _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self).
- FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors.
- rootvars.scss: minor plutonium + fuschia tweaks (pre-existing).
- 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
|
|
|
body.page-billscroll,
|
buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial)
- lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design.
- applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts).
- billboard.views.my_buddies + add_buddy:
• my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all().
• add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup).
- templates:
• _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/).
• my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet."
• _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add.
- SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement).
- tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op.
- 841 ITs (+14) + 4 my_buddies FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:31:42 -04:00
|
|
|
body.page-billpost,
|
buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:33 -04:00
|
|
|
body.page-billbuds,
|
|
|
|
|
body.page-billposts {
|
2026-03-23 01:06:14 -04:00
|
|
|
.row {
|
|
|
|
|
margin-bottom: -1rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 16:46:46 -04:00
|
|
|
// ── Billboard page (three-applet grid) ─────────────────────────────────────
|
|
|
|
|
|
2026-03-23 01:06:14 -04:00
|
|
|
.billboard-page {
|
2026-03-24 16:46:46 -04:00
|
|
|
@extend %billboard-page-base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Billscroll page (single full-aperture applet) ──────────────────────────
|
|
|
|
|
|
|
|
|
|
.billscroll-page {
|
|
|
|
|
@extend %billboard-page-base;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2026-03-23 01:06:14 -04:00
|
|
|
padding: 0.75rem;
|
2026-03-24 16:46:46 -04:00
|
|
|
|
|
|
|
|
// The single scroll applet stretches to fill the remaining aperture
|
2026-05-03 23:22:01 -04:00
|
|
|
.applet-scroll {
|
2026-03-24 17:31:51 -04:00
|
|
|
@extend %applet-box;
|
2026-03-24 16:46:46 -04:00
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
#id_drama_scroll {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow-y: auto;
|
pronouns: per-user pronouns ideology + Pronouns applet on Game Kit; provenance prose uses actor.pronouns at render time — TDD
- User.pronouns CharField w. choices=[pluralism (default), bawlmorese, misogyny, misandry, misanthropy] + pronoun_subj/obj/poss properties; PRONOUN_TABLE single source of truth in apps.lyric.models; mig 0002_user_pronouns
- drama.GameEvent.to_prose() drops module-level PRONOUN_* constants; SIG_READY/SIG_UNREADY/ROLE_SELECTED now resolve poss/subj from self.actor.pronouns at render time, so flipping a user's preference rewrites all their existing scroll prose; default actor → "their"
- SIG_READY prose strips a leading "The " from card_name so "the The Wanderer" reads "the Wanderer" and "the Engraven The Nomad" reads "the Engraven Nomad"; minor arcana ("Maid of Brands") untouched
- new applets/0005 seeds 'pronouns' applet (3x3, game-kit, default visible); _game_kit_sections.html grows a #id_gk_pronouns block w. 5 .gk-pronoun-card items labeled by ideology slug (italic) and tagged data-pronoun + data-trio
- card click → window.showGuard(card, "Set pronoun preference?<span class='guard-pronoun-trio'>{trio}</span>", commitCb); on OK fetches POST /dashboard/set-pronouns w. CSRF cookie + reloads so .active class moves and provenance prose re-renders; NVM dismisses
- dashboard.set_pronouns view (POST-only, login_required, 204/400/405) at /dashboard/set-pronouns; rejects choices not in PRONOUN_TABLE
- _game-kit.scss extends shared card rule to .gk-pronoun-card w. .active fill state + italic ideology label; #id_guard_portal .guard-pronoun-trio styled small/dim/centered under the question
- billscroll aperture: padding-right 0.75rem on #id_drama_scroll inside .applet-scroll so the timestamp column no longer sits beneath the scrollbar
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 01:11:40 -04:00
|
|
|
padding-right: 0.75rem;
|
2026-03-24 19:02:29 -04:00
|
|
|
|
|
|
|
|
.scroll-buffer {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
padding: 2rem 0 1rem;
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
|
|
|
|
.scroll-buffer-text {
|
|
|
|
|
letter-spacing: 0.33em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scroll-buffer-dots {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
letter-spacing: 0;
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 0.7em;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-24 16:46:46 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD
- schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner).
- lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed).
- drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default.
- billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title.
- post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio).
- SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square.
- _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self).
- FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors.
- rootvars.scss: minor plutonium + fuschia tweaks (pre-existing).
- 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
|
|
|
// ── Dashpost page (bottom-anchored thread + composer) ─────────────────────
|
|
|
|
|
// Mirrors billscroll's flex-column / overflow-y / scroll-buffer pattern,
|
|
|
|
|
// with the composer pinned at the bottom (flex-shrink: 0) so the thread
|
|
|
|
|
// breathes against the viewport bottom and the input stays in reach.
|
|
|
|
|
|
|
|
|
|
.post-page {
|
|
|
|
|
@extend %billboard-page-base;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
2026-05-08 23:42:51 -04:00
|
|
|
// Username + title attribution spans — line author column, self/shared
|
|
|
|
|
// header lines, server-rendered grant prose. --quaUser palette key
|
|
|
|
|
// unifies them across the page; placed at .post-page scope so it
|
|
|
|
|
// applies in BOTH .post-header and #id_post_table descendants.
|
|
|
|
|
.post-attribution {
|
|
|
|
|
color: rgba(var(--quaUser), 1);
|
|
|
|
|
}
|
|
|
|
|
|
post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD
- schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner).
- lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed).
- drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default.
- billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title.
- post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio).
- SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square.
- _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self).
- FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors.
- rootvars.scss: minor plutonium + fuschia tweaks (pre-existing).
- 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
|
|
|
.post-header {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
.post-title {
|
|
|
|
|
margin: 0 0 0.25rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.post-shared-recipients,
|
|
|
|
|
.post-shared-self {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
opacity: 0.75;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#id_post_table {
|
|
|
|
|
list-style: none;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0 0.75rem 0 0;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
// Bottom-anchor: scroll buffer above the lines pushes them down
|
|
|
|
|
// until they fill from the bottom; once content exceeds the
|
|
|
|
|
// aperture, normal scrolling kicks in.
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
|
|
|
|
.post-line {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(4rem, auto) 1fr minmax(3rem, auto);
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
padding: 0.25rem 0;
|
|
|
|
|
|
|
|
|
|
.post-line-author {
|
|
|
|
|
font-weight: bold;
|
2026-05-08 23:42:51 -04:00
|
|
|
color: rgba(var(--quaUser), 1);
|
post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD
- schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner).
- lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed).
- drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default.
- billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title.
- post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio).
- SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square.
- _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self).
- FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors.
- rootvars.scss: minor plutonium + fuschia tweaks (pre-existing).
- 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
|
|
|
white-space: nowrap;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.post-line-text {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.post-line-time {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
text-align: right;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// System-authored Lines (adman) get a subtler typographic key
|
|
|
|
|
// — the inline `<a class="note-ref">` carries the emphasis.
|
|
|
|
|
&.post-line--system .post-line-text {
|
|
|
|
|
font-style: italic;
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.post-line-buffer {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
height: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.post-line-form {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding-top: 0.25rem;
|
|
|
|
|
|
|
|
|
|
input.form-control {
|
|
|
|
|
width: 100%;
|
2026-05-08 21:52:34 -04:00
|
|
|
|
|
|
|
|
// Admin-Post readonly input — no response is invited, so the
|
|
|
|
|
// focus halo softens to --secUser (cooler than the regular
|
|
|
|
|
// --terUser glow used on user-Post composers).
|
|
|
|
|
&[readonly]:focus {
|
|
|
|
|
border-color: rgba(var(--secUser), 0.6);
|
|
|
|
|
box-shadow: 0 0 0.75rem rgba(var(--secUser), 0.4);
|
|
|
|
|
}
|
post aperture refactor (May-8b): Post.title field; Line.author PROTECT FK + created_at; Note.grant_if_new admin-vs-Look! format dispatch w. note-ref anchor; bottom-anchored aperture w. shared-between header + per-Line user/timestamp; dotted-? Brief square; reserved adman seed — TDD
- schema: billboard/0004 adds Post.title (CharField 35) + Line.author (PROTECT FK, related_name=authored_lines) + Line.created_at (auto_now_add); RunPython backfill stamps existing rows (note_unlock → "Notes & recognitions" + author=adman; user_post → first-line glean + author=Post.owner).
- lyric/0003 seeds adman User (system author for note unlock + share invite Lines); apps.lyric.models gains RESERVED_USERNAMES = {"adman"}, is_reserved_username() guard in dashboard.set_profile, get_or_create_adman() lazy fetch (TransactionTestCase flushes the seed).
- drama: Note.grant_if_new dispatches via _ADMIN_NOTE_SLUGS = {"super-schizo","super-nomad"} — admin slugs use "The administration recognizes…" prose; everyone else uses "Look!—new Note unlocked." Both wrap Note name in `<a class="note-ref">`. Header Line dropped (test_two_different_grants_share_one_post asserts 2 lines, not 3). Note.display_name property added (slug.title() default — "super-schizo" → "Super-Schizo"). User.active_title_display returns donned recognition title or "Earthman" default.
- billboard models: Post.name property removed → my_posts.html, _applet-my-posts.html, PostSerializer switched to Post.title. LineForm.save(for_post, author) + ExistingPostLineForm.save(author) signature + all callers (api.views, billboard.views.new_post + view_post + share_post). billboard.views.share_post authors via get_or_create_adman; new_post truncates first line for Post.title via _truncate_post_title.
- post.html: <h3> post title heading; .post-shared-recipients (commas only) + .post-shared-self lines ("just me, X the Earthman" / "& me, X the Y" 0/≥1 split); #id_post_table is now a <ul> w. justify-content: flex-end + per-Line 3-col grid (author/text/time); adman Lines render |safe + .post-line--system italic; #id_text → #id_post_line_text rename (post.html only — /billboard/ new-post applet keeps #id_text); page_class page-billpost (joins billboard+billscroll body-class trio).
- SCSS _billboard.scss: .post-page extends %billboard-page-base, adds bottom-anchored flex-column scroll + 3-col .post-line grid + .post-line-form pinned at bottom. _note.scss: a.note-banner__image picks up .note-item__image-box dashed-? styling for the Brief square.
- _buddy_panel.html JS rewired for new layout: _appendLine builds <li class="post-line post-line--system"> w. adman+timestamp; _appendRecipientChip handles 0→1+ transition (rewrites "just me," → "& me,", inserts .post-shared-recipients line above self).
- FT post_page.py: get_table_rows queries .post-line; wait_for_row_in_post_table matches by text containment (line_number arg ignored — kept for backwards compat); get_line_input_box probes #id_post_line_text first, falls back to #id_text; get_post_owner reads textContent (hidden span). test_applet_new_post_line_validation switched to input[name="text"]:invalid/:valid for cross-page selectors.
- rootvars.scss: minor plutonium + fuschia tweaks (pre-existing).
- 818 ITs + 35 FTs (buddy/new-post/sharing/validation/layout/jasmine/my-notes/my-posts) green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:21 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:33 -04:00
|
|
|
// ── Applet-list page (Billbuds, Billposts) ───────────────────────────────
|
|
|
|
|
// Shared shell for pages built around _applet-list-shell.html — vertical
|
|
|
|
|
// title rotated on the left of an .applet-scroll card + scrollable <ul>
|
|
|
|
|
// aperture. `--single` hosts one section (My Buds); `--two-up` stacks
|
|
|
|
|
// two sections in portrait, places them side-by-side in landscape (My
|
|
|
|
|
// Posts: own + shared).
|
buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial)
- lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design.
- applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts).
- billboard.views.my_buddies + add_buddy:
• my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all().
• add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup).
- templates:
• _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/).
• my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet."
• _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add.
- SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement).
- tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op.
- 841 ITs (+14) + 4 my_buddies FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:31:42 -04:00
|
|
|
|
buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:33 -04:00
|
|
|
.applet-list-page {
|
buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial)
- lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design.
- applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts).
- billboard.views.my_buddies + add_buddy:
• my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all().
• add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup).
- templates:
• _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/).
• my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet."
• _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add.
- SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement).
- tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op.
- 841 ITs (+14) + 4 my_buddies FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:31:42 -04:00
|
|
|
@extend %billboard-page-base;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
padding: 0.75rem;
|
buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:33 -04:00
|
|
|
gap: 0.75rem;
|
buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial)
- lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design.
- applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts).
- billboard.views.my_buddies + add_buddy:
• my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all().
• add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup).
- templates:
• _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/).
• my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet."
• _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add.
- SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement).
- tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op.
- 841 ITs (+14) + 4 my_buddies FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:31:42 -04:00
|
|
|
|
buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:33 -04:00
|
|
|
.applet-scroll {
|
|
|
|
|
@extend %applet-box;
|
buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial)
- lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design.
- applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts).
- billboard.views.my_buddies + add_buddy:
• my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all().
• add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup).
- templates:
• _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/).
• my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet."
• _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add.
- SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement).
- tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op.
- 841 ITs (+14) + 4 my_buddies FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:31:42 -04:00
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
2026-05-12 22:48:32 -04:00
|
|
|
// .applet-list / .applet-list-entry / .applet-list-buffer rules
|
|
|
|
|
// live at top level (above) so they apply to in-grid applets too.
|
|
|
|
|
.applet-list { flex: 1; }
|
buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Side-by-side in landscape; stacked in portrait (default).
|
|
|
|
|
&--two-up {
|
|
|
|
|
@media (orientation: landscape) {
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
.applet-scroll { flex: 1; }
|
buddies sprint phase 1: User.buddies M2M(self,symm=False) + my_buddies aperture page + add_buddy JSON endpoint + buddy btn slide-out — TDD; My Contacts applet renamed → My Buddies (slug + name + partial)
- lyric/0004 adds User.buddies = ManyToManyField('self', symmetrical=False, blank=True, related_name='added_as_buddy'). Asymmetric one-way add: A.buddies.add(B) doesn't reciprocate. Reverse via B.added_as_buddy.all() — load-bearing for the future "buddy changed username" snapshot-accept flow noted in design.
- applets/0006 renames slug my-contacts → my-buddies + name 'Contacts' → 'My Buddies'. Existing migrations 0003/0004 untouched (historical artifacts).
- billboard.views.my_buddies + add_buddy:
• my_buddies: GET /billboard/my-buddies/ → renders the aperture page with request.user.buddies.all().
• add_buddy: POST /billboard/buddies/add → JSON {buddy: {id, username, email}|null}. Privacy: returns null when email isn't a registered User OR is the requester's own; never leaks membership. Idempotent on re-add (M2M dedup).
- templates:
• _applet-my-contacts.html → _applet-my-buddies.html (heading + link to /billboard/my-buddies/).
• my_buddies.html — bottom-anchored aperture list of buddies w. {% empty %} fallback "No buddies yet."
• _buddy_add_panel.html — bottom-left handshake btn + slide-out, mirrors _buddy_panel.html (post share) but POSTs to add_buddy and appends to #id_buddies_list. Skips append if data-buddy-id already in DOM (race-safe). Drops the .buddy-entry--empty row on first add.
- SCSS: page-billbuddies joins the body-class aperture trio; .buddies-page extends %billboard-page-base + flex-column + bottom-anchor for #id_buddies_list. id_applet_my_contacts → id_applet_my_buddies (test references + grid placement).
- tests: new test_buddies.py — 14 ITs covering UserBuddiesM2MTest (asymmetric, idempotent), MyBuddiesViewTest (lists own buddies only, anon redirect), AddBuddyViewTest (registered/unregistered/self/idempotent/email-fallback/405). Existing test_views/test_billboard/test_game_kit references swapped to my-buddies. New test_my_buddies.py FT — 4 tests: pre-existing buddies render, empty state, add via panel appends entry w. username, unregistered silent no-op.
- 841 ITs (+14) + 4 my_buddies FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:31:42 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 17:19:09 -04:00
|
|
|
// ── Billboard applet placement ─────────────────────────────────────────────
|
rename: Note→Post/Line (dashboard); Recognition→Note (drama); new-post/my-posts to billboard
- dashboard: Note→Post, Item→Line across models, forms, views, API, urls & tests
- new-post (9×3) & my-posts (3×3) applets migrate from dashboard→billboard context; billboard view passes form & recent_posts
- drama: Recognition→Note, related_name notes; billboard URL /recognition/→/my-notes/, set-palette at /note/<slug>/set-palette
- recognition.js→note.js (module Note, data.note key); recognition-page.js→note-page.js; .recog-*→.note-*
- _recognition.scss→_note.scss; BillNotes page header; applet slug billboard-recognition→billboard-notes (My Notes)
- NoteSpec.js replaces RecognitionSpec.js; test_recognition.py→test_applet_my_notes.py
- 4 migrations applied: dashboard 0004, applets 0011+0012, drama 0005; 683 ITs green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:32:34 -04:00
|
|
|
// Left column (4-wide): My Scrolls → Contacts → Notes stacked.
|
2026-05-03 23:22:01 -04:00
|
|
|
// Right column (8-wide): Most Recent Scroll spans full height.
|
2026-03-24 17:19:09 -04:00
|
|
|
// Portrait override (container query) restores stacked full-width layout.
|
|
|
|
|
|
|
|
|
|
#id_billboard_applets_container {
|
2026-05-03 23:22:01 -04:00
|
|
|
#id_applet_my_scrolls { grid-column: 1 / span 4; grid-row: 1 / span 3; }
|
buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:33 -04:00
|
|
|
#id_applet_my_buds { grid-column: 1 / span 4; grid-row: 4 / span 3; }
|
2026-05-03 23:22:01 -04:00
|
|
|
#id_applet_notes { grid-column: 1 / span 4; grid-row: 7 / span 4; }
|
|
|
|
|
#id_applet_most_recent_scroll { grid-column: 5 / span 8; grid-row: 1 / span 10; }
|
2026-03-24 17:19:09 -04:00
|
|
|
|
|
|
|
|
@container (max-width: 550px) {
|
2026-05-03 23:22:01 -04:00
|
|
|
#id_applet_my_scrolls,
|
buds rename + applet-list shell — Buddies → Buds everywhere (model field, slug, URL, view, DOM, CSS); my_buds.html + my_posts.html share new _applet-list-shell.html partial — vertical-title applet-scroll card; my_posts hosts two side-by-side in landscape, stacked in portrait — TDD
- lyric/0005 RemoveField+AddField (RenameField doesn't rename the implicit M2M through table; field was new in 0004 so no data loss). Lyric.User.buddies → User.buds; related_name added_as_buddy → added_as_bud.
- applets/0007 renames Applet slug my-buddies → my-buds + name 'My Buddies' → 'My Buds'. UI rationale: BILLBUDDIES overflowed the page-header band; in-game term collapses to BILLBUDS.
- billboard/0006 alter Line.Meta.ordering = ('created_at', 'id') — was already in models.py, just generates the corresponding migration (formalizing the ordering decision from the May-8b refactor).
- global rename via sed: buddies → buds, buddy → bud across 16 files (templates, SCSS, JS, ITs, FTs, page object, view code). 4 file renames via git mv: my_buddies.html → my_buds.html, _applet-my-buddies.html → _applet-my-buds.html, _buddy_panel.html → _bud_panel.html, _buddy_add_panel.html → _bud_add_panel.html, _buddy.scss → _bud.scss. Test files renamed too: test_buddies.py → test_buds.py, test_my_buddies.py → test_my_buds.py, test_buddy_btn.py → test_bud_btn.py. core.scss @import 'buddy' → 'bud'.
- new shared partial templates/apps/applets/_partials/_applet-list-shell.html — vertical-rotated <h2> + scrollable <ul> aperture, parameterised via {% include %} so a single page can invoke it more than once. Params: shell_title, shell_items, shell_item_template, shell_list_id, shell_empty.
- my_buds.html: single shell invocation w. add-bud panel below (page_class page-billbuds).
- my_posts.html: two shell invocations (own posts + posts shared with me) inside .applet-list-page--two-up — portrait stacks them; landscape lays side-by-side via @media (orientation: landscape) flex-direction: row (page_class page-billposts).
- SCSS: drop the bottom-anchored .buds-page block; new shared .applet-list-page (extends %billboard-page-base, flex-column + padding) w. .applet-scroll inside (extends %applet-box) and .applet-list inside that (flex: 1, overflow-y: auto). .applet-list-page--two-up flips to row layout in landscape. Body class trio gains page-billposts.
- 841 ITs + 5 my_buds/my_posts FTs green.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:33 -04:00
|
|
|
#id_applet_my_buds,
|
2026-05-03 23:22:01 -04:00
|
|
|
#id_applet_notes,
|
|
|
|
|
#id_applet_most_recent_scroll {
|
2026-03-24 17:19:09 -04:00
|
|
|
grid-column: 1 / span 12;
|
|
|
|
|
grid-row: span var(--applet-rows, 3);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 23:22:01 -04:00
|
|
|
// ── Most Recent Scroll applet — scrollable drama feed ─────────────────────
|
2026-03-24 17:19:09 -04:00
|
|
|
|
2026-05-03 23:22:01 -04:00
|
|
|
#id_applet_most_recent_scroll {
|
2026-03-24 17:19:09 -04:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
.most-recent-room-link {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#id_drama_scroll {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.most-recent-load-more {
|
|
|
|
|
display: block;
|
|
|
|
|
padding-bottom: 0.5rem;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 14:51:08 -04:00
|
|
|
// ── Drama event entries: 90 / 10 column split ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
.drama-event {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
|
|
|
|
.drama-event-body {
|
|
|
|
|
flex: 0 0 80%;
|
2026-04-13 00:34:05 -04:00
|
|
|
|
|
|
|
|
&.struck {
|
|
|
|
|
text-decoration: line-through;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
2026-04-02 14:51:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drama-event-time {
|
|
|
|
|
flex: 0 0 20%;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 22:48:32 -04:00
|
|
|
// My Scrolls now rides the shared `.applet-list` rule above (lifted out of
|
|
|
|
|
// `.applet-list-page .applet-scroll`). Old `.scroll-list` styling removed.
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
|
|
|
|
|
// ── My Sign applet (billboard) ────────────────────────────────────────────
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
// Saved-sig preview — mirrors the `.sig-stage-card` layout (corner top-
|
|
|
|
|
// left + face w. name + arcana + mirror corner bottom-right) but sized
|
|
|
|
|
// to fill the applet's vertical aperture rather than a fixed 5rem.
|
|
|
|
|
// Container queries on `.my-sign-applet-body` lift `--applet-card-w` to
|
|
|
|
|
// `min(100cqi, 62.5cqh)` — the card grows to fill whichever axis is
|
|
|
|
|
// constraining (62.5cqh = `100cqh * 5/8` keeps the 5:8 aspect inside
|
|
|
|
|
// the container height). All child font sizes calc off --applet-card-w
|
|
|
|
|
// so the typography scales w. the card without per-applet tuning.
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
#id_applet_my_sign {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
// Anchor for #id_applet_sky_delete_btn's absolute centering.
|
|
|
|
|
position: relative;
|
|
|
|
|
background-color: rgba(var(--duoUser), 1) !important;
|
|
|
|
|
|
|
|
|
|
.my-sign-applet-empty {
|
|
|
|
|
opacity: 1 !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h2 {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
background-color: rgba(var(--priUser), 1);
|
|
|
|
|
box-shadow: rgba(0, 0, 0, 1) !important;
|
|
|
|
|
}
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
|
|
|
|
|
.my-sign-applet-body {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
flex-direction: row;
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
gap: 0.5rem;
|
|
|
|
|
padding: 0.3rem;
|
|
|
|
|
container-type: size;
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.my-sign-applet-card {
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
// Width-cap shrinks by half the row (card + gap + stat-block) so the
|
|
|
|
|
// pair centres without horizontal overflow. `1.0cqi` keeps the
|
|
|
|
|
// gap/padding allowance — anything left after the stat-block grows
|
|
|
|
|
// to fill, capped by the card's natural 5:8 aspect.
|
|
|
|
|
--applet-card-w: min(48cqi, 62.5cqh);
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
width: var(--applet-card-w);
|
|
|
|
|
aspect-ratio: 5 / 8;
|
|
|
|
|
border-radius: 0.4rem;
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
// Gravity default — `--priUser` bg + `--terUser` ink. `--levity`
|
|
|
|
|
// modifier below inverts to `--secUser` bg + `--quiUser` ink,
|
|
|
|
|
// matching the page stage card's polarity convention (cf
|
|
|
|
|
// `_card-deck.scss:1002-1019` for levity, :1039-1057 for gravity).
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
background: rgba(var(--priUser), 1);
|
|
|
|
|
border: 0.12rem solid rgba(var(--secUser), 0.6);
|
A.6 + A.7 billboard My Sign applet + gameboard My Sea applet image-rendering + applet-level FLIP-to-back — TDD. Sprints A.6 + A.7 of [[project-image-based-deck-face-rendering]]: rolls image-mode out to the two card-rendering applets (My Sign on /billboard/, My Sea on /gameboard/). Both reuse the shared `.sig-stage-card.sig-stage-card--image` SCSS contract via a comma-list selector extension covering the parallel container classes (`.my-sign-applet-card.my-sign-applet-card--image` + `.my-sea-slot.my-sea-slot--image`) — single source of truth for the contour-stroke drop-shadow chain + tray-card silhouette black depth shadow + .is-flipped-to-back visibility toggle + the `--img-stroke-color` arcana-keyed CSS prop. Templates branch server-side on `card.deck_variant.has_card_images`: image-mode renders `<img class="sig-stage-card-img" src="{{ card.image_url }}">` w. the marker class + `data-arcana-key` attr; text mode keeps the existing fan-card-corner + fan-card-face scaffold unchanged. SCSS import-order quirk: `_card-deck.scss` imports BEFORE both `_billboard.scss` (which nests `.my-sign-applet-card` inside `.my-sign-applet-body` for container queries) and `_gameboard.scss` (which nests `.my-sea-slot--filled.--gravity/--levity` inside `#id_applet_my_sea` w. specificity 1,2,0). The shared top-level image-mode rule at 0,2,0 loses on bg/border/padding to those nested base rules, so each app's stylesheet gets a parallel `&.--image { background: transparent; border: 0; padding: 0 }` override inside its own nest. The filter-chain rules on `.sig-stage-card-img` (descendant selector inside the shared rule) DO win since the apps don't restyle that class — only the outer container needs the parallel override. Sprint A.6 bonus: applet-level FLIP btn for non-polarized image-equipped decks (Minchiate today). Mirrors the my_sign.html main page A.5-polish-2 FLIP-to-back contract — `.my-sign-applet-flip-btn` nested inside the .--image card so absolute positioning anchors to the card bounds; inline `<script>` IIFE (gated inside the sig-present {% with card %} scope to keep `card` in lexical reach + prevent the JS selector string leaking into the no-sig DOM where `assertNotContains "my-sign-applet-card"` ITs catch it) attaches a click handler that runs the same rotateY 0→90→0 animation, toggles `.is-flipped-to-back` at the halfway point, and clears `data-flipping` at end; SCSS `.my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn { opacity: 0; pointer-events: none }` hides the btn mid-spin. Critical scope bug caught + fixed during browser verify: initial draft had the script BLOCK + its `{% if card.deck_variant.has_card_images %}` gate placed AFTER the `{% endwith %}` closing tag — `card` was out of scope at the `{% if %}` evaluation, Django treats undefined vars as empty string, the gate evaluated falsy, and the script NEVER rendered (the FLIP btn rendered fine since it was inside the with block, but no JS handler → click did nothing but the CSS depress animation). Fix: move `{% endwith %}` to AFTER the script gate so `card` is still in scope. 7 new ITs total: 2 in `BillboardAppletMySignTest` (image-equipped Minchiate renders `--image` class + img + correct asset URL + lacks text scaffold; Earthman keeps the text scaffold + lacks `--image`); 3 in `BillboardMySignViewTest` (data-deck-polarized attr present; back-img element renders for non-polarized image deck; polarized deck omits it); 1 in `GameboardViewTest` (image-equipped Minchiate slot renders `--image` + img + lacks text scaffold); plus regression coverage on the no-sig empty-state assertion that originally caught the script-scope bug (assertNotContains validates the script doesn't leak in the no-sig case). Tests: 6 new ITs green; 1306/1306 IT+UT total green (72s; +6 from bdf6a25's 1303 — minus 3 dups since some ITs were counted across both A.6 + A.5-polish-2 runs). Visual verify by user 2026-05-25 PM: stage card image renders cleanly; FLIP cycles to back image + back via animation; FLIP btn hides during 500ms spin; placeholder dim styling correctly distinguishes no-deck state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:58:36 -04:00
|
|
|
|
|
|
|
|
// Sprint A.6 — image-mode override. `_card-deck.scss` imports before
|
|
|
|
|
// `_billboard.scss`, so the shared `.my-sign-applet-card--image` rule
|
|
|
|
|
// there gets out-cascaded by the base bg/border above (same specificity,
|
|
|
|
|
// later declaration wins). Re-state the transparency here AFTER the
|
|
|
|
|
// base. The contour stroke + depth shadow on the <img> still come
|
|
|
|
|
// from the shared `_card-deck.scss` rule, which only loses on `bg` +
|
|
|
|
|
// `border` properties — not on the filter chain.
|
|
|
|
|
&.my-sign-applet-card--image {
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: 0;
|
|
|
|
|
position: relative; // anchor for the absolute FLIP btn
|
|
|
|
|
}
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
color: rgba(var(--terUser), 1);
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
padding: 0.35rem;
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
transition: transform 0.4s ease;
|
|
|
|
|
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
// Top-left + bottom-right corners — rank + suit icon stacked.
|
|
|
|
|
// br is rotated 180° (mirror) so the card reads as "completed"
|
|
|
|
|
// from both edges, matching the stage card pattern.
|
|
|
|
|
.fan-card-corner--tl,
|
|
|
|
|
.fan-card-corner--br {
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
line-height: 1.05;
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
gap: 0.05rem;
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
|
|
|
|
.fan-corner-rank {
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
font-size: calc(var(--applet-card-w) * 0.16);
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
i { font-size: calc(var(--applet-card-w) * 0.13); }
|
|
|
|
|
}
|
|
|
|
|
.fan-card-corner--tl { top: 0.25rem; left: 0.3rem; }
|
|
|
|
|
.fan-card-corner--br {
|
|
|
|
|
bottom: 0.25rem; right: 0.3rem;
|
|
|
|
|
transform: rotate(180deg);
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
}
|
|
|
|
|
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
// Card face — qualifier + title + arcana stacked, centred in the
|
|
|
|
|
// remaining vertical space between the two corners. `gap: 0` so
|
|
|
|
|
// qualifier sits directly above the title at the title's own
|
|
|
|
|
// line-height; `.fan-card-arcana` carries its own margin-top to
|
|
|
|
|
// restore breathing room between title block and arcana label.
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
.fan-card-face {
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
flex-direction: column;
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
gap: 0;
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
text-align: center;
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
padding: 0 0.2rem;
|
|
|
|
|
}
|
|
|
|
|
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
// Qualifier + title share the same typography (per `_card-deck.scss`
|
|
|
|
|
// convention at lines 568-572 / 1821-1823) — both bold, same size,
|
|
|
|
|
// same wrap, same line-height. Polarity color (gravity → --terUser,
|
|
|
|
|
// levity → --quiUser) lives on the parent — both elements inherit.
|
|
|
|
|
.fan-card-qualifier,
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
.fan-card-name {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: calc(var(--applet-card-w) * 0.11);
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
line-height: 1.15;
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
text-wrap: balance;
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
color: inherit;
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
}
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
.fan-card-qualifier:empty { display: none; }
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
.fan-card-arcana {
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
margin: calc(var(--applet-card-w) * 0.05) 0 0;
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
font-size: calc(var(--applet-card-w) * 0.075);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
}
|
|
|
|
|
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
// Levity inversion — `--secUser` bg + `--quiUser` ink + `--priUser`
|
|
|
|
|
// border, mirroring `.sig-overlay[data-polarity="levity"]
|
|
|
|
|
// .sig-stage-card` at `_card-deck.scss:1002-1010`.
|
|
|
|
|
&.my-sign-applet-card--levity {
|
|
|
|
|
background: rgba(var(--secUser), 1);
|
|
|
|
|
border-color: rgba(var(--priUser), 0.6);
|
|
|
|
|
color: rgba(var(--quiUser), 1);
|
|
|
|
|
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
|
|
|
|
|
.fan-card-arcana { color: rgba(var(--priUser), 1); }
|
|
|
|
|
}
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
}
|
|
|
|
|
|
A.6 + A.7 billboard My Sign applet + gameboard My Sea applet image-rendering + applet-level FLIP-to-back — TDD. Sprints A.6 + A.7 of [[project-image-based-deck-face-rendering]]: rolls image-mode out to the two card-rendering applets (My Sign on /billboard/, My Sea on /gameboard/). Both reuse the shared `.sig-stage-card.sig-stage-card--image` SCSS contract via a comma-list selector extension covering the parallel container classes (`.my-sign-applet-card.my-sign-applet-card--image` + `.my-sea-slot.my-sea-slot--image`) — single source of truth for the contour-stroke drop-shadow chain + tray-card silhouette black depth shadow + .is-flipped-to-back visibility toggle + the `--img-stroke-color` arcana-keyed CSS prop. Templates branch server-side on `card.deck_variant.has_card_images`: image-mode renders `<img class="sig-stage-card-img" src="{{ card.image_url }}">` w. the marker class + `data-arcana-key` attr; text mode keeps the existing fan-card-corner + fan-card-face scaffold unchanged. SCSS import-order quirk: `_card-deck.scss` imports BEFORE both `_billboard.scss` (which nests `.my-sign-applet-card` inside `.my-sign-applet-body` for container queries) and `_gameboard.scss` (which nests `.my-sea-slot--filled.--gravity/--levity` inside `#id_applet_my_sea` w. specificity 1,2,0). The shared top-level image-mode rule at 0,2,0 loses on bg/border/padding to those nested base rules, so each app's stylesheet gets a parallel `&.--image { background: transparent; border: 0; padding: 0 }` override inside its own nest. The filter-chain rules on `.sig-stage-card-img` (descendant selector inside the shared rule) DO win since the apps don't restyle that class — only the outer container needs the parallel override. Sprint A.6 bonus: applet-level FLIP btn for non-polarized image-equipped decks (Minchiate today). Mirrors the my_sign.html main page A.5-polish-2 FLIP-to-back contract — `.my-sign-applet-flip-btn` nested inside the .--image card so absolute positioning anchors to the card bounds; inline `<script>` IIFE (gated inside the sig-present {% with card %} scope to keep `card` in lexical reach + prevent the JS selector string leaking into the no-sig DOM where `assertNotContains "my-sign-applet-card"` ITs catch it) attaches a click handler that runs the same rotateY 0→90→0 animation, toggles `.is-flipped-to-back` at the halfway point, and clears `data-flipping` at end; SCSS `.my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn { opacity: 0; pointer-events: none }` hides the btn mid-spin. Critical scope bug caught + fixed during browser verify: initial draft had the script BLOCK + its `{% if card.deck_variant.has_card_images %}` gate placed AFTER the `{% endwith %}` closing tag — `card` was out of scope at the `{% if %}` evaluation, Django treats undefined vars as empty string, the gate evaluated falsy, and the script NEVER rendered (the FLIP btn rendered fine since it was inside the with block, but no JS handler → click did nothing but the CSS depress animation). Fix: move `{% endwith %}` to AFTER the script gate so `card` is still in scope. 7 new ITs total: 2 in `BillboardAppletMySignTest` (image-equipped Minchiate renders `--image` class + img + correct asset URL + lacks text scaffold; Earthman keeps the text scaffold + lacks `--image`); 3 in `BillboardMySignViewTest` (data-deck-polarized attr present; back-img element renders for non-polarized image deck; polarized deck omits it); 1 in `GameboardViewTest` (image-equipped Minchiate slot renders `--image` + img + lacks text scaffold); plus regression coverage on the no-sig empty-state assertion that originally caught the script-scope bug (assertNotContains validates the script doesn't leak in the no-sig case). Tests: 6 new ITs green; 1306/1306 IT+UT total green (72s; +6 from bdf6a25's 1303 — minus 3 dups since some ITs were counted across both A.6 + A.5-polish-2 runs). Visual verify by user 2026-05-25 PM: stage card image renders cleanly; FLIP cycles to back image + back via animation; FLIP btn hides during 500ms spin; placeholder dim styling correctly distinguishes no-deck state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:58:36 -04:00
|
|
|
// Sprint A.6 — FLIP btn for non-polarized image-equipped decks in the
|
|
|
|
|
// applet. Nested INSIDE the .my-sign-applet-card.--image (which has
|
|
|
|
|
// position: relative) so absolute positioning anchors to the card bounds.
|
|
|
|
|
// Hidden during the rotateY animation via the [data-flipping] hook on
|
|
|
|
|
// the parent card — same pattern as the my_sign page (`_card-deck.scss:889`)
|
|
|
|
|
// and the tarot-fan view (`_card-deck.scss:459`).
|
|
|
|
|
.my-sign-applet-card .my-sign-applet-flip-btn {
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
bottom: 0.6rem;
|
|
|
|
|
left: 0.6rem;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
.my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
feat: My Sea applet dynamic population + lay/leave POSITION_LABELS swap fix + My Sign applet stat-block + Brief-fied sign-gate + --duoUser olive on all four personal-data surfaces. Six visual+structural items batched across the dashboard/billboard/gameboard.
(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:19:34 -04:00
|
|
|
// Stat block — mirrors the stage card's footprint (same 5:8 aspect +
|
|
|
|
|
// height) so the pair reads as a balanced 2-tile composition centred
|
|
|
|
|
// in the applet aperture. Styling cribbed from `.sig-stat-block` in
|
|
|
|
|
// `_card-deck.scss:595-607` (priUser-translucent bg + terUser border)
|
|
|
|
|
// minus the SPIN/FYI button apparatus — applet is read-only, no
|
|
|
|
|
// interaction needed. `--applet-card-w` is reused as the sizing knob
|
|
|
|
|
// so stat-face-label + stat-keywords typography scales w. the card.
|
|
|
|
|
.my-sign-applet-stat-block {
|
|
|
|
|
--applet-card-w: min(48cqi, 62.5cqh);
|
|
|
|
|
width: var(--applet-card-w);
|
|
|
|
|
aspect-ratio: 5 / 8;
|
|
|
|
|
align-self: center;
|
|
|
|
|
background: rgba(var(--priUser), 0.8);
|
|
|
|
|
border-radius: 0.4rem;
|
|
|
|
|
border: 0.1rem solid rgba(var(--terUser), 0.15);
|
|
|
|
|
padding: calc(var(--applet-card-w) * 0.08);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
.stat-face-label {
|
|
|
|
|
font-size: calc(var(--applet-card-w) * 0.08);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.09em;
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
color: rgba(var(--terUser), 1);
|
|
|
|
|
margin: 0 0 calc(var(--applet-card-w) * 0.06);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-keywords {
|
|
|
|
|
list-style: none;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
|
|
li {
|
|
|
|
|
font-size: calc(var(--applet-card-w) * 0.1);
|
|
|
|
|
padding: calc(var(--applet-card-w) * 0.04) 0;
|
|
|
|
|
color: rgba(var(--quiUser), 1);
|
|
|
|
|
border-bottom: 0.05rem solid rgba(var(--terUser), 0.18);
|
|
|
|
|
&:last-child { border-bottom: none; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: significator_reversed=polarity bug + Pattern B name-swap rendering + qualifier-aware applet faces + sticky PAID DRAW + cooldown anchor on User + stat-block polarity unification across Sig/Sea/Fan/applets
Five-thread sprint atop 53cd7af; all 1238 IT/UT green (no FTs run per [[feedback-ft-run-discipline]]).
**Thread 1 — User.significator_reversed is the POLARITY axis, not orientation.** The saved sig was rendering as a gravity reversal when the user saved a levity emanation. Root cause: `my_sign.html` JS post-save load called `_toggleOrientation()` whenever `revInput.value==='1'` (SPIN-ing a card whose flag only meant "polarity=levity"); `_applet-my-sign.html` applied `.stage-card--reversed` + `keywords_reversed` for the same flag. Fix: JS drops the `_toggleOrientation()` call (saved sigs are always upright in their polarity, never spun); the applet drops the rotation class, swaps to `my-sign-applet-card--{levity,gravity}` modifier, and always renders `keywords_upright` / "Emanation". `data-polarity` cascades correctly. Memory: [[feedback-significator-reversed-is-polarity]].
**Thread 2 — qualifier rendering on the My Sign + My Sea applets.** Both applets were rendering name only — no qualifier word. Added `TarotCard.applet_face(polarity, reversed)` (model method) + `User.sig_face` (delegator for the saved sig) returning `{title, qualifier, qualifier_first}` payload that mirrors `populateCard` in `stage-card.js`. `latest_draw_slots()` augments each slot dict w. `face`. Templates render `.fan-card-qualifier` + `.fan-card-name` in the order the payload dictates (non-Major: qualifier-above-title; Major+qualifier: title-with-trailing-comma above qualifier; polarity-split: single-line title). Typography matched to title (same bold, same size, same color via `color: inherit` w. polarity-pin at 0,3,0 specificity to beat `_card-deck.scss:376-383`'s 0,2,0 `.fan-card-face .fan-card-name` rule that out-cascades when loaded after gameboard).
**Thread 3 — My Sea cooldown bugs.** Two: (a) PAID DRAW button reverted to FREE DRAW after one navigation cycle because `my_sea_paid_draw` deleted the row at commit time — without a row, `quota_spent=False` on next render. (b) Brief's "next free draw at" was anchored to the most recent paid draw, not the original free draw. Fix: new `User.last_free_draw_at` field (set in `my_sea_lock` when a fresh row lands AND user wasn't already in cooldown — i.e., this is a tokenless free draw); paid draws NEVER touch it. New `MySeaDraw.paid_through_at` field stamped at commit time + cleared in `my_sea_lock` when the first card of the paid session lands (one-shot credit per user-spec: "each redraw needs a new token"). `my_sea_paid_draw` no longer deletes the row — clears hand+deposit, sets `paid_through_at`, redirects to `?phase=picker`. View's landing button uses `show_paid_draw` (`deposit_reserved OR paid_through_at`) so PAID DRAW persists across navigation until the paid session's first card lands. Brief reads `user.next_free_draw_at` (= `last_free_draw_at + 24h`) w. row-fallback for legacy test fixtures. 11 new ITs (`MySeaCooldownAnchoredToFreeDrawTest`, `UserFreeDrawCooldownPropertyTest`, expanded `MySeaPhasePickerQueryParamTest`, expanded `my_sea_lock` tests). Existing `test_paid_draw_deletes_active_draw_row` rewritten as `test_paid_draw_preserves_row_and_sets_paid_through_at`. 1 new FT pinning the navigation-persistence regression. Memory: [[feedback-my-sea-cooldown-design]].
**Thread 4 — Pattern B / B' Major reversal name-swap.** Card 34's My Sea applet rendered the reversal as "Animal Powers, Patrilineage" (Patrilineage treated as a qualifier). User-locked semantics: for Majors w. BOTH polarity qualifiers AND a `reversal_qualifier`, the `reversal_qualifier` field carries the NAME SWAP for the reversal face; the polarity qualifier persists across both faces. Affected cards: 2-5 (Pope/Horseman), 10-15 (Elements), 22-33 (Zodiac → Houses), 34-35 (Lunars), 41 (Asteroid Belt). Pattern B': cards 16-18 (Realms — Disco Inferno → Shame etc.) reversal face drops the qualifier entirely; new `TarotCard.reversal_drops_qualifier` BooleanField marks these (set True on 16-18 via `epic/0010_set_reversal_drops_qualifier_realms.py` data migration). `applet_face()` + `stage-card.js::populateCard` both branch on `arcana==MAJOR AND reversal_qualifier AND polarity_qualifier` → Pattern B/B' rendering. Non-Major `reversal_qualifier` semantics unchanged (middle court: "Queen of Crowns" stays as title, "Vacant" renders as the reversal-face qualifier). New data attr `data-reversal-drops-qualifier` added to `my_sign.html`, `_sig_select_overlay.html`, `_tarot_fan.html` so stage-card.js can read it via dataset. `card_dict()` extended w. the same field. 3 new UTs (`TarotCardAppletFaceTest`: Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin). Old `test_reversed_uses_reversal_qualifier_with_comma_for_major` deleted (it pinned the conflated old behavior).
**Thread 5 — unified card + stat-block polarity convention across all 6 surfaces** (Sig Select, Sea Select stage modal, Game Kit fan, My Sign applet, My Sea applet, room.html). User-locked: card and adjacent stat block always carry OPPOSITE-polarity bgs (gravity card --priUser → stat block --secUser; levity card --secUser → stat block --priUser). `.is-reversed` (SPIN) is preview-only — never shifts bg. Per-card scoping (NOT page-wide) — drawn sea cards each carry their own polarity from the deck stack; `.sea-stage--{gravity,levity}` parent rules + `.tarot-fan-wrap[data-polarity=...]` parent rules cascade to their respective stat blocks. `game-kit.js` `_populateStage` + `_flipActive` mirror `_polarity` onto `.tarot-fan-wrap` so SCSS can pick it up without touching the stat block directly. Sea-stat-block was previously stuck at --priUser regardless of polarity; fan-stage-block ditto. Both inverted now. Memory: [[feedback-card-polarity-convention]].
**Bundled polish across the same surfaces** (each one a small visible item the user spotted during the sprint):
- My Sign applet card: levity polarity flips bg to --secUser + border to --priUser + ink to --quiUser (matches page stage card at `_card-deck.scss:1002-1019`). Gravity stat block flips to --secUser bg w. --quiUser label ink + --priUser keyword ink (matches `_card-deck.scss:1042-1046`).
- Qualifier + title share typography (font-size, weight, polarity-color, text-wrap). `.fan-card-face { gap: 0 }` + `line-height: 1.15` so qualifier sits directly above title at the title's own line-height. `.fan-card-arcana { margin-top }` reserves breathing room below.
- `.fan-card-qualifier:empty { display: none }` collapses polarity-split / Major-no-qualifier cards cleanly.
**Memory recorded**:
1. [[feedback-ft-run-discipline]] — re-pinned 2026-05-23 after I burned a multi-minute full-FT-suite run mid-task. Default loop is IT/UT only. FT runs must be ONE test method by full dotted path; never a whole file; never re-run an already-green FT.
2. [[feedback-significator-reversed-is-polarity]] — the flag is polarity (FLIP), not orientation (SPIN); SPIN never persisted; saved sigs always upright in their polarity.
3. [[feedback-card-polarity-convention]] — opposite-polarity stat-block bg, per-card scoping, SPIN never shifts bg, the full color table.
4. [[feedback-my-sea-cooldown-design]] — cooldown anchored to User.last_free_draw_at, paid draws never reset it, paid_through_at is a sticky one-shot credit, button state machine.
**Files** (every uncommitted file folded in — session work + pre-existing modifications):
Models / migrations:
- `apps/epic/models.py` — `applet_face()` extended w. Pattern B/B' branches; new `reversal_drops_qualifier` BooleanField.
- `apps/epic/migrations/0009_reversal_drops_qualifier.py` — schema.
- `apps/epic/migrations/0010_set_reversal_drops_qualifier_realms.py` — data migration setting flag True on cards 16-18.
- `apps/epic/utils.py` — `card_dict` carries `reversal_drops_qualifier`.
- `apps/gameboard/models.py` — `paid_through_at` field; `latest_draw_slots()` attaches `face` payload per slot; `active_draw_for` docstring refreshed.
- `apps/gameboard/migrations/0003_myseadraw_paid_through_at.py` — schema.
- `apps/lyric/models.py` — `last_free_draw_at` field; `free_draw_cooldown_active` + `next_free_draw_at` props; `sig_face` delegator.
- `apps/lyric/migrations/0013_user_last_free_draw_at.py` — schema.
Views:
- `apps/gameboard/views.py` — `my_sea` view button state machine (`show_paid_draw` / `show_gate_view` / `show_picker`); `my_sea_lock` sets `last_free_draw_at` on free-draw + clears `paid_through_at` on paid-session first card; `my_sea_paid_draw` preserves row + stamps `paid_through_at`.
JS:
- `apps/epic/static/apps/epic/stage-card.js` — `fromDataset` reads `reversal_drops_qualifier`; `populateCard` branches Pattern B / B' for the reversal face.
- `apps/gameboard/static/apps/gameboard/game-kit.js` — mirrors `_polarity` onto `.tarot-fan-wrap` so SCSS can invert the fan-stage-block bg per active card.
Templates:
- `templates/apps/billboard/my_sign.html` — JS drops `_toggleOrientation()` on saved-sig load; sig-card grid carries `data-reversal-drops-qualifier`.
- `templates/apps/billboard/_partials/_applet-my-sign.html` — drops `stage-card--reversed`, adds polarity modifier, renders qualifier via `sig_face` payload, always shows Emanation keywords + label.
- `templates/apps/gameboard/_partials/_applet-my-sea.html` — renders qualifier via `slot.face` payload (Pattern B/B' aware).
- `templates/apps/gameboard/_partials/_sig_select_overlay.html` + `_tarot_fan.html` — `data-reversal-drops-qualifier` added to sig-card grid + fan cards.
- `templates/apps/gameboard/my_sea.html` — landing button form swaps to `show_paid_draw` / `show_gate_view` flags.
SCSS:
- `static_src/scss/_billboard.scss` — My Sign applet card polarity inversion (levity bg + ink), polarity stat-block inversion (gravity → --secUser bg), qualifier+title shared typography, polarity-aware ink via `color: inherit`.
- `static_src/scss/_card-deck.scss` — sea-stat-block polarity rules (`.sea-stage--gravity/levity .sea-stat-block`), fan-stage-block polarity rules (`.tarot-fan-wrap[data-polarity] .fan-stage-block`), comments documenting fallback bgs.
- `static_src/scss/_gameboard.scss` — `.my-sea-slot--filled.--gravity/--levity` pin `color: inherit` on `.fan-card-corner`, `.fan-card-qualifier`, `.fan-card-name`, `.fan-card-arcana` (0,3,0 beats global 0,2,0). Slot label keeps original wrap-sibling placement w. `z-index: 2` to render above the dotted bottom border on empty slots.
Tests:
- `apps/billboard/tests/integrated/test_views.py` — updated `test_my_sign_applet_renders_card_when_sig_set` to assert polarity modifier + qualifier text + Emanation-only; new `test_my_sign_applet_renders_gravity_qualifier_when_not_reversed`.
- `apps/epic/tests/unit/test_models.py` — `TarotCardAppletFaceTest` (Pattern B name swap, Pattern B' qualifier drop, non-Major regression pin, polarity-split, reversal qualifier fallback).
- `apps/gameboard/tests/integrated/test_views.py` — `MySeaCooldownAnchoredToFreeDrawTest` (5 tests pinning cooldown anchor on User, sticky PAID DRAW, paid-through credit consumption); `UserFreeDrawCooldownPropertyTest` (4 tests); expanded `MySeaPhasePickerQueryParamTest` w. paid-through-shows-PAID-DRAW-btn assertion; expanded `my_sea_lock` tests (free-draw-anchors-last_free_draw_at, paid-draw-leaves-anchor-alone, first-paid-card-consumes-credit); My Sea applet qualifier IT (Major comma format end-to-end).
- `functional_tests/test_game_my_sea.py` — `test_paid_draw_commits_token_and_redirects_to_picker` updated to assert row preservation + paid_through_at stamping; new `test_paid_draw_btn_persists_after_navigation_without_card_draw` pinning the user-reported regression.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:06:35 -04:00
|
|
|
// Polarity inversion of the stat block — mirrors the page convention
|
|
|
|
|
// where the stat block always carries the OPPOSITE-polarity colors of
|
|
|
|
|
// its sibling card (`_card-deck.scss:1042-1046` for the gravity case,
|
|
|
|
|
// levity inherits the default --priUser bg). Gravity polarity card →
|
|
|
|
|
// --secUser stat block (light), w. --quiUser label + --priUser
|
|
|
|
|
// keywords for contrast against the light bg.
|
|
|
|
|
.my-sign-applet-body[data-polarity="gravity"] .my-sign-applet-stat-block {
|
|
|
|
|
background: rgba(var(--secUser), 0.8);
|
|
|
|
|
border-color: rgba(var(--priUser), 0.15);
|
|
|
|
|
.stat-face-label { color: rgba(var(--quiUser), 1); }
|
|
|
|
|
.stat-keywords li {
|
|
|
|
|
color: rgba(var(--priUser), 1);
|
|
|
|
|
border-bottom-color: rgba(var(--priUser), 0.18);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix: shop tooltip price flex-pinned right (cross-file `#id_tooltip_portal .tt-title { display: block }` was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.
(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.
(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.
(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.
(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.
(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.
**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
|
|
|
.my-sign-applet-empty {
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
text-align: center;
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
font-style: italic;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|