Files
python-tdd/src/static_src/scss/_billboard.scss
Disco DeDisco f6093136f1 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

514 lines
15 KiB
SCSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── 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;
}
// 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;
padding-left: 0.5rem;
padding-right: 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.12s ease, color 0.12s ease;
.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;
}
// 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-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;
&: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 {
display: flex;
flex-direction: column;
.applet-list {
flex: 1;
padding-top: 0.25rem;
}
}
// ── Shared aperture fill for both billboard pages ──────────────────────────
//
// 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.
%billboard-page-base {
flex: 1;
min-width: 0;
min-height: 0;
overflow-y: auto;
position: relative;
}
body.page-billboard,
body.page-billscroll,
body.page-billpost,
body.page-billbuds,
body.page-billposts {
.row {
margin-bottom: -1rem;
}
}
// ── Billboard page (three-applet grid) ─────────────────────────────────────
.billboard-page {
@extend %billboard-page-base;
}
// ── Billscroll page (single full-aperture applet) ──────────────────────────
.billscroll-page {
@extend %billboard-page-base;
display: flex;
flex-direction: column;
padding: 0.75rem;
// The single scroll applet stretches to fill the remaining aperture
.applet-scroll {
@extend %applet-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
#id_drama_scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 0.75rem;
.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;
}
}
}
}
}
}
// ── 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;
// 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-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;
color: rgba(var(--quaUser), 1);
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%;
// 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);
}
}
}
}
// ── 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).
.applet-list-page {
@extend %billboard-page-base;
display: flex;
flex-direction: column;
padding: 0.75rem;
gap: 0.75rem;
.applet-scroll {
@extend %applet-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
// .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; }
}
// Side-by-side in landscape; stacked in portrait (default).
&--two-up {
@media (orientation: landscape) {
flex-direction: row;
.applet-scroll { flex: 1; }
}
}
}
// ── Billboard applet placement ─────────────────────────────────────────────
// Left column (4-wide): My Scrolls → Contacts → Notes stacked.
// Right column (8-wide): Most Recent Scroll spans full height.
// Portrait override (container query) restores stacked full-width layout.
#id_billboard_applets_container {
#id_applet_my_scrolls { grid-column: 1 / span 4; grid-row: 1 / span 3; }
#id_applet_my_buds { grid-column: 1 / span 4; grid-row: 4 / span 3; }
#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; }
@container (max-width: 550px) {
#id_applet_my_scrolls,
#id_applet_my_buds,
#id_applet_notes,
#id_applet_most_recent_scroll {
grid-column: 1 / span 12;
grid-row: span var(--applet-rows, 3);
}
}
}
// ── Most Recent Scroll applet — scrollable drama feed ─────────────────────
#id_applet_most_recent_scroll {
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;
}
}
// ── Drama event entries: 90 / 10 column split ─────────────────────────────
.drama-event {
display: flex;
align-items: baseline;
.drama-event-body {
flex: 0 0 80%;
&.struck {
text-decoration: line-through;
opacity: 0.5;
}
}
.drama-event-time {
flex: 0 0 20%;
font-size: 0.75rem;
opacity: 0.5;
text-align: right;
}
}
// My Scrolls now rides the shared `.applet-list` rule above (lifted out of
// `.applet-list-page .applet-scroll`). Old `.scroll-list` styling removed.
// ── My Sign applet (billboard) ────────────────────────────────────────────
// Saved-sig preview thumbnail. Same 5:8 card shell + corner/name layout as
// `.sig-stage-card` (see `_card-deck.scss`) but scaled down to fit the 4×6
// applet aperture. Without these rules the markup collapses bg-less to the
// applet's top-left corner. `--applet-card-w` drives all child font sizing
// off a single knob (same calc-fractions as `.sig-stage-card` w. --sig-card-w).
#id_applet_my_sign {
display: flex;
flex-direction: column;
.my-sign-applet-body {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
}
.my-sign-applet-card {
--applet-card-w: 5rem;
width: var(--applet-card-w);
aspect-ratio: 5 / 8;
border-radius: 0.4rem;
background: rgba(var(--priUser), 1);
border: 0.12rem solid rgba(var(--secUser), 0.6);
color: rgba(var(--secUser), 1);
padding: 0.25rem;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
transition: transform 0.4s ease;
.fan-card-corner--tl {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.05rem;
position: absolute;
top: 0.2rem;
left: 0.2rem;
.fan-corner-rank {
font-size: calc(var(--applet-card-w) * 0.18);
font-weight: 700;
}
i { font-size: calc(var(--applet-card-w) * 0.14); }
}
.fan-card-name {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
text-align: center;
font-size: calc(var(--applet-card-w) * 0.13);
font-weight: 600;
text-wrap: balance;
padding: 0 0.15rem;
color: rgba(var(--quiUser), 1);
}
&.stage-card--reversed { transform: rotate(180deg); }
}
.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;
}
}