Third attempt at fixing the dashboard's right-column alignment, this
time with the structural cause identified explicitly.
Previous attempts (a55168d, 8347c90) changed align-self on #log-panel
to control how the panel filled its grid area. They got the box
edges aligned, but the underlying problem was a different one:
CSS Grid auto-sizes each row by MAX(intrinsic content height across
items in that row). When the log content is taller than indicators +
portfolio combined, the grid grows rows 2-3 to fit it; portfolio
ends up in a stretched row with empty space below the actual content.
The fix is to stop the log's content from contributing to the grid
row sizing at all. `contain: size` on the log panel declares "my
contents do not affect my intrinsic size" — the grid then sizes rows
2-3 from indicators + portfolio alone, and the log stretches to
inhabit that combined height. A flex column inside the panel
(min-height: 0 on every level of the chain) lets .panel-body fill
the remaining height below the header and scroll instead of
overflowing.
The 1100px mobile breakpoint undoes the constraint: at that width
the grid restructures to a single column, the log no longer shares
a row with indicators + portfolio, and `contain: size` would just
collapse the panel to zero. There the log expands naturally and
page scroll handles it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two small fixes to the dashboard right column based on user feedback:
1. layout.css — drop align-self:start from #log-panel.
The panel previously shrank to its content, leaving the right-hand
column visually shorter than the indicators+portfolio stack on the
left. Removing the override lets the grid stretch the panel to the
full row span so the two columns now bottom-align. The log content
still sits at the top of the panel; any extra height is empty
padding inside the box.
2. portfolio.js — re-hydrate AI analysis expanded.
The 60s auto-refresh rebuilds the portfolio mount and re-attaches
the previously-generated analysis from localStorage, but the
<details> element was re-attached with open:false — collapsing it
under the user's cursor every minute. Users reasonably perceived
that as "the analysis disappeared". Hydrate as open:true so the
body stays visible; the user can still click the summary to
collapse manually within a refresh window.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reverses the polarity of 71155a6 to match the actual semantics:
- "Novice" stays labelled "Novice" → glossary tooltips, plainer prose.
- "Intermediate" is relabelled "Pro" → terse, assumes fluency, no
hand-holding. This is the mode an expert reader wants, so the "Pro"
badge actually fits.
Backend tone values (NOVICE, INTERMEDIATE) are unchanged — no API,
prompt, or stored-preference impact. Only the display strings flip.
Also drops the .tone-toggle button min-width: 10em override added in
71155a6. With "Intermediate" gone from the visible label, the longest
remaining label is "Novice" (6 chars), which fits the shared 5.5em
just like the theme and language toggles.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-visible relabel only. Backend tone value stays NOVICE — no API
contract change, no migration on stored user.digest_tone, the
glossary/plain-prose depth of analysis is unchanged. The marketing
intent is that "Pro" reads better than "Novice" on the dashboard
header; landing/pricing/privacy copy still uses the word "Novice" in
flowing prose, so leaving those alone keeps the existing explanations
coherent until they get a copy pass.
Toggle width: the popup expansion (positioned left:0/right:0) is
sized by the container, which previously sized to the active button.
When "Pro" was active the popup was too narrow to fit "Intermediate".
Bumped .tone-toggle button min-width to 10em so both buttons reserve
enough room for the longest label regardless of which one is active.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hovering a toggle (tone, theme, language) previously revealed the
non-active option inline next to the active one, which widened the
toggle and pushed its neighbours sideways. Now the non-active option
appears as a popup ABSOLUTELY POSITIONED below the active one — the
toggle's in-flow footprint stays exactly one button wide and tall, so
the other two toggles next to it never move when the user mouses over
one of them.
Mechanism: inside @media (hover: hover) the container becomes
position:relative and every button defaults to display:none. The
:hover/:focus-within rule renders all options as position:absolute
under the container. Specificity (.X[data=Y] btn[data=Y]) on the
active-button rule then pins the active option back into the static
flow at the top, so only the non-active end(s) up absolute — popup
grows downward only. margin-top:-1px makes the popup's top border
overlap the container's bottom border for a single shared edge.
z-index:60 sits above the markets bar (z-50). Touch devices keep
both options side-by-side (the @media gate); the mobile drawer keeps
both visible too.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Header layout was visibly broken on desktop after the mobile-drawer
change: flex space-between distributed brand, BETA, tone-toggle, nav
and header-right across the bar, so BETA drifted away from the brand
wordmark and the tone-toggle landed in the middle of the row.
Markup: brand + BETA are now wrapped in .header-left so they ride
together. The tone-toggle moves back inside .header-right next to
theme + lang where it logically belongs. CSS: the header switches to
grid (1fr auto 1fr) on desktop, which truly centres the nav regardless
of side-group widths. The mobile @media block reverts to flex so the
hamburger + slide-out drawer still work.
Toggle redesign (tone, theme, language):
- The single-button theme widget becomes a Light | Dark segmented
control matching the other two so all three read as one cluster.
cassandraToggleTheme is replaced by cassandraSetTheme(theme), the
toggle's data-theme attribute is synced on page load.
- All three share one CSS rule set: same padding, font, border, and
a min-width so the active-only width matches the expanded width
(no layout jump on hover).
- On hover-capable devices each toggle collapses to just the active
option; hovering (or keyboard focus-within) reveals both. Touch
devices keep both visible — the @media (hover: hover) gate handles
that and the mobile-drawer block overrides it explicitly so the
drawer-stacked controls remain full-width with both options shown.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three pieces of phone-side feedback:
1. Indicator group tabs wrap onto multiple rows instead of
horizontal-scrolling — every group is visible at a glance. Each
button keeps its own bottom border so wrapped rows stay
visually delimited; the container's bottom border is removed.
2. Portfolio holdings table hides Qty and Avg columns on mobile via
the mobile-hide class (same mechanism as the indicator table).
Remaining columns are the actionable ones: Ticker, Name, Last,
P/L, %.
3. Markets bar at the bottom compacts to one row per chip —
dot + code + change% only. The state word ("open" / "closed")
is implied by the dot colour; the index label, price, and
until-time are dropped on mobile. Grid columns drop their 220px
floor so the full set fits the viewport without horizontal
scroll (previously the bar scrolled within itself).
User reported the page rendering at ~3x viewport width on Android
Chrome with overflow-x:hidden clipping off most of the content.
Root cause: CSS grid items default to min-width:min-content, and the
indicator table inside the indicators panel has white-space:nowrap
cells. A long Symbol/Label value forces the table wider than its
panel; the panel propagates that minimum width up the grid; the grid
expands the .app-main; .app-main pushes the page wider than the
viewport. overflow-x:hidden then just chops the right portion off.
Fix has three parts:
1. .app and .app-main get min-width:0 and max-width:100vw so the
shell can't be wider than the viewport regardless of descendants.
2. Every direct child of .app-main (each panel) gets min-width:0
on mobile so individual panels can shrink past their min-content.
3. table.dense drops white-space:nowrap on text cells at ≤480px —
long symbols wrap to two lines instead of forcing the table wide.
Numeric cells keep nowrap (negative percentages reading as
"−12\n.34%" would be unreadable).
Also adds an overflow-x:auto fallback on .panel-body pre/code so
any code block in AI output scrolls within the panel instead of
blowing the page out.
Two related bugs reported on phone:
1. Drawer was unclickable — backdrop covered it. Root cause: the
.app-header (position:sticky, z-index:50) creates a stacking
context, so the drawer inside it had its z-index:100 clamped to
"above other things inside the header" but NOT above siblings of
the header. The backdrop at root-level z:90 then sat over the
drawer subtree.
Fix: when body.drawer-open, raise .app-header z-index to 110
so its entire descendant tree (drawer included) draws above the
z:90 backdrop. The page body under the header stays dimmed.
2. Horizontal scrolling on the dashboard. Root cause: the bottom
markets bar used `grid-template-columns: repeat(auto-fit,
minmax(220px, 1fr))`, which at 4+ markets blows out to 880px+ and
forces the page wider than the viewport.
Fix: on ≤480px the markets bar becomes a horizontally scrolling
flex strip with min-width:160px per chip — page stays narrow,
user swipes the bar to see more markets.
Also added overflow-x:hidden to html/body as a defensive net against
the fixed off-screen drawer creating overflow on Safari iOS.
≤480px gets a hamburger button in the topbar and a fixed slide-out
panel from the right edge (width min(82vw, 320px)). The topbar keeps
only brand + tone toggle + hamburger visible; nav and the
header-right widgets (theme, lang, user menu, version meta) move
into the drawer.
Markup change: nav and .header-right are now wrapped in
.mobile-drawer, which is display:contents on desktop (no layout
effect) and a fixed translateX panel on mobile. The user-menu
dropdown chip hides on mobile and its links surface flat inside the
drawer.
JS: ~50 lines of vanilla. Tap hamburger / backdrop / ESC / swipe-
right-on-drawer all close. Clicking a nav link inside the drawer
closes it after the navigation kicks off so the panel doesn't linger
on the next page.
CSS: per-file @media block at the bottom of layout.css per the
agreed-upon organisation.
Splits the 2571-line cassandra.css into ten focused stylesheets:
tokens (palette + fonts), layout (chrome), panels, dashboard,
portfolio, log-chat, auth, settings, news, public. base.html and
public_base.html load only what they need; auth pages (login,
verify, unsubscribe confirm) load tokens + layout + auth.
Brand drift-detection test repointed at tokens.css (where the
palette now lives). 291 tests still pass.