(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>
514 lines
15 KiB
SCSS
514 lines
15 KiB
SCSS
// ── 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;
|
||
}
|
||
}
|