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.