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).
430 lines
13 KiB
CSS
430 lines
13 KiB
CSS
/* Cassandra — structural layout: html/body, app shell, header, main grid,
|
|
* sticky markets bar, scrollbar. */
|
|
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
font-variant-numeric: tabular-nums;
|
|
/* Prevents the off-screen fixed mobile drawer (translateX(100%))
|
|
from forcing horizontal scroll on Safari iOS, and provides a
|
|
safety net for any cell/grid that would otherwise overflow. */
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
a { color: var(--accent); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
|
|
/* --- Layout ---------------------------------------------------------- */
|
|
|
|
.app {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: auto 1fr auto;
|
|
min-height: 100vh;
|
|
/* Grid items default to min-content min-width which can blow past
|
|
the viewport when a descendant table or flex row is wide. min-width:0
|
|
lets the cell shrink below intrinsic min-content, and max-width:100vw
|
|
caps the whole shell against the viewport so we never need to rely on
|
|
overflow:hidden clipping. */
|
|
min-width: 0;
|
|
max-width: 100vw;
|
|
}
|
|
|
|
.app-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 10px 18px;
|
|
background: var(--surface);
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 50;
|
|
}
|
|
.app-header .brand {
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
text-decoration: none;
|
|
}
|
|
.app-header .brand:hover { color: var(--text); }
|
|
.app-header .brand::before { content: "▰ "; opacity: 0.6; }
|
|
.app-header nav a {
|
|
margin-left: 18px;
|
|
color: var(--muted);
|
|
}
|
|
.app-header nav a.active { color: var(--text); }
|
|
.app-header .meta { color: var(--muted); font-size: 11px; }
|
|
|
|
/* On desktop the mobile-drawer wrapper has no layout effect — its
|
|
* children (nav, header-right) flow as if it weren't there. On mobile
|
|
* the @media block at the bottom converts it to a fixed slide-out. */
|
|
.mobile-drawer { display: contents; }
|
|
|
|
.app-header .header-right { display: flex; align-items: center; gap: 14px; }
|
|
|
|
/* Hamburger button — only visible at ≤480px (rule in the mobile block).
|
|
* Three thin bars; uses the same border/muted treatment as the other
|
|
* header buttons so the visual rhythm matches. */
|
|
.drawer-toggle {
|
|
display: none;
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
cursor: pointer;
|
|
padding: 6px 8px;
|
|
width: 36px;
|
|
height: 32px;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
align-items: stretch;
|
|
}
|
|
.drawer-toggle:hover { border-color: var(--accent); }
|
|
.drawer-toggle__bar {
|
|
display: block;
|
|
height: 2px;
|
|
background: var(--muted);
|
|
width: 100%;
|
|
}
|
|
.drawer-toggle:hover .drawer-toggle__bar { background: var(--accent); }
|
|
|
|
.drawer-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.4);
|
|
z-index: 90;
|
|
opacity: 0;
|
|
transition: opacity 120ms ease-out;
|
|
}
|
|
body.drawer-open .drawer-backdrop { opacity: 1; }
|
|
.theme-toggle {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--muted);
|
|
padding: 3px 8px;
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
letter-spacing: 0.08em;
|
|
cursor: pointer;
|
|
text-transform: lowercase;
|
|
}
|
|
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }
|
|
.theme-toggle__label::before { content: "◐ light"; }
|
|
[data-theme="dark"] .theme-toggle__label::before { content: "◐ dark"; }
|
|
|
|
/* Tone toggle (segmented control: Novice | Intermediate) */
|
|
.tone-toggle {
|
|
display: inline-flex;
|
|
border: 1px solid var(--border);
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
.tone-toggle button {
|
|
background: transparent;
|
|
color: var(--muted);
|
|
border: 0;
|
|
padding: 4px 10px;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
letter-spacing: inherit;
|
|
text-transform: inherit;
|
|
}
|
|
.tone-toggle button + button { border-left: 1px solid var(--border); }
|
|
.tone-toggle button:hover { color: var(--accent); }
|
|
.tone-toggle[data-tone="NOVICE"] button[data-value="NOVICE"],
|
|
.tone-toggle[data-tone="INTERMEDIATE"] button[data-value="INTERMEDIATE"] {
|
|
background: var(--accent);
|
|
color: var(--bg);
|
|
}
|
|
|
|
/* Language toggle in the topbar — same visual rhythm as the tone
|
|
* toggle so the two controls read as a pair. Only EN and IT are
|
|
* visible here; the WIP languages (ES/FR/DE) live in /settings. */
|
|
.lang-toggle {
|
|
display: inline-flex;
|
|
border: 1px solid var(--border);
|
|
font-family: var(--font-mono);
|
|
font-size: 10.5px;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
.lang-toggle button {
|
|
background: transparent;
|
|
color: var(--muted);
|
|
border: 0;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
letter-spacing: inherit;
|
|
text-transform: inherit;
|
|
}
|
|
.lang-toggle button + button { border-left: 1px solid var(--border); }
|
|
.lang-toggle button:hover { color: var(--accent); }
|
|
.lang-toggle[data-lang="en"] button[data-value="en"],
|
|
.lang-toggle[data-lang="it"] button[data-value="it"] {
|
|
background: var(--accent);
|
|
color: var(--bg);
|
|
}
|
|
|
|
.app-main {
|
|
padding: 14px;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
|
grid-template-rows: auto auto auto auto;
|
|
grid-template-areas:
|
|
"header header"
|
|
"indicators log"
|
|
"portfolio log"
|
|
"news news";
|
|
gap: 14px;
|
|
}
|
|
@media (max-width: 1100px) {
|
|
.app-main {
|
|
grid-template-columns: 1fr;
|
|
grid-template-areas: "header" "indicators" "portfolio" "log" "news";
|
|
}
|
|
}
|
|
|
|
#dash-header-container { grid-area: header; }
|
|
#indicators-panel { grid-area: indicators; }
|
|
#portfolio-panel { grid-area: portfolio; }
|
|
#log-panel {
|
|
grid-area: log;
|
|
/* Don't stretch to fill both grid rows; if the log is shorter than
|
|
the portfolio next to it, the surplus below would render as a big
|
|
empty white box. Aligning to the start makes the panel shrink to
|
|
its content and the dashboard background fills any gap. */
|
|
align-self: start;
|
|
}
|
|
#news-panel { grid-area: news; }
|
|
|
|
|
|
/* Sticky bottom markets bar — uses the same .mkt chip styling as the
|
|
old dashboard header, extended with each market's headline index. */
|
|
.markets-bar {
|
|
position: sticky;
|
|
bottom: 0;
|
|
z-index: 50;
|
|
background: var(--surface);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.markets-bar__inner {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
gap: 1px;
|
|
background: var(--border);
|
|
border: 0;
|
|
}
|
|
.markets-bar .mkt {
|
|
border: 0;
|
|
border-radius: 0;
|
|
}
|
|
|
|
/* --- Scrollbar -------------------------------------------------------- */
|
|
|
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
::-webkit-scrollbar-track { background: var(--bg); }
|
|
::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 0; }
|
|
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
|
|
|
|
|
|
/* --- Mobile (≤480px) -------------------------------------------------- */
|
|
|
|
@media (max-width: 480px) {
|
|
/* Tighten the topbar so the brand, tone toggle and hamburger all fit
|
|
on a 360px phone. Drop the letter-spacing because at 11px tracking
|
|
eats horizontal space the brand cannot spare. */
|
|
.app-header {
|
|
padding: 8px 12px;
|
|
gap: 8px;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
/* When the drawer is open the header (which contains the drawer)
|
|
needs to draw above the backdrop. The header is a sticky element
|
|
with its own stacking context at z-index 50, so the drawer's
|
|
local z-index 100 is clamped to z-50 in the root context — the
|
|
backdrop at z-90 then sits OVER it. Raise the whole header above
|
|
the backdrop while the drawer is open. */
|
|
body.drawer-open .app-header { z-index: 110; }
|
|
.app-header .brand {
|
|
font-size: 12px;
|
|
/* Shrink the leading glyph but don't remove it — keeps brand identity. */
|
|
}
|
|
.beta-chip { display: none; }
|
|
|
|
/* Show the hamburger; the rest of the header widgets collapse into
|
|
the drawer (the .mobile-drawer block below). */
|
|
.drawer-toggle { display: flex; margin-left: auto; }
|
|
|
|
/* Keep the tone toggle visible but trim it: just N / I letters so it
|
|
fits next to brand + hamburger. */
|
|
.tone-toggle--header {
|
|
font-size: 9.5px;
|
|
}
|
|
.tone-toggle--header button {
|
|
padding: 4px 7px;
|
|
}
|
|
|
|
/* The drawer wrapper: full-height slide-out from the right. The
|
|
content inside (nav + header-right) becomes a vertical stack
|
|
with comfortable touch targets. */
|
|
.mobile-drawer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: min(82vw, 320px);
|
|
background: var(--surface);
|
|
border-left: 1px solid var(--border);
|
|
box-shadow: -2px 0 12px rgba(0, 0, 0, 0.18);
|
|
transform: translateX(100%);
|
|
transition: transform 180ms ease-out;
|
|
z-index: 100;
|
|
overflow-y: auto;
|
|
padding: 56px 18px 24px;
|
|
text-transform: none;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
body.drawer-open .mobile-drawer { transform: translateX(0); }
|
|
|
|
/* Vertical nav inside the drawer — links become big-tap rows, no
|
|
leading margin like the desktop horizontal nav. */
|
|
.mobile-drawer nav { display: flex; flex-direction: column; }
|
|
.mobile-drawer nav a {
|
|
margin-left: 0;
|
|
padding: 12px 4px;
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 14px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
.mobile-drawer nav a.active {
|
|
color: var(--accent);
|
|
border-left: 2px solid var(--accent);
|
|
padding-left: 10px;
|
|
}
|
|
|
|
/* header-right widgets vertically stacked inside the drawer. */
|
|
.mobile-drawer .header-right {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 14px;
|
|
margin-top: 20px;
|
|
}
|
|
.mobile-drawer .theme-toggle,
|
|
.mobile-drawer .lang-toggle {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
.mobile-drawer .lang-toggle { display: inline-flex; }
|
|
.mobile-drawer .lang-toggle button,
|
|
.mobile-drawer .theme-toggle { padding: 10px; font-size: 11.5px; }
|
|
|
|
/* The user-menu's dropdown becomes redundant inside the drawer —
|
|
surface its links flat as a list, and hide the chip button. */
|
|
.mobile-drawer .user-menu { width: 100%; }
|
|
.mobile-drawer .user-chip { display: none; }
|
|
.mobile-drawer .user-menu__panel {
|
|
display: block !important; /* override the hidden attribute */
|
|
position: static;
|
|
border: 0;
|
|
padding: 0;
|
|
margin-top: 4px;
|
|
}
|
|
.mobile-drawer .user-menu__panel[hidden] { display: block !important; }
|
|
.mobile-drawer .user-menu__item {
|
|
display: block;
|
|
padding: 10px 4px;
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 13px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
.mobile-drawer .meta {
|
|
margin-top: auto;
|
|
padding-top: 18px;
|
|
text-align: center;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* The drawer container itself sits above the topbar in z-stacking;
|
|
we still want the close button accessible while it's open, so push
|
|
a close target into the top-right corner of the drawer via a
|
|
repurposed pseudo-element. (Simpler than adding new markup.) */
|
|
.mobile-drawer::before {
|
|
content: "✕";
|
|
position: absolute;
|
|
top: 14px;
|
|
right: 18px;
|
|
font-size: 18px;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
pointer-events: none; /* tap handled by the backdrop / hamburger */
|
|
}
|
|
|
|
/* Body-level layout: tighten main padding too — saves another 16px
|
|
of horizontal real estate which the indicator table and chat
|
|
bubbles all benefit from. Also force min-width:0 on the grid
|
|
container and every grid item, otherwise a wide table inside
|
|
a panel forces the whole grid (and the page) wider than the
|
|
viewport. This is the single most important mobile fix. */
|
|
.app-main {
|
|
padding: 10px 8px;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
max-width: 100vw;
|
|
}
|
|
.app-main > * { min-width: 0; }
|
|
|
|
/* Markets bar: compact each chip so the full set fits the viewport
|
|
without horizontal scrolling. We drop:
|
|
- state word ("open" / "closed") — the dot already conveys that
|
|
- index label (e.g. "SPX") — implied by the market code
|
|
- index price — keep the change% which is the actionable number
|
|
- until-time — too detailed for a glance
|
|
Remaining: dot + market code + change%. The grid keeps auto-fit
|
|
but the minimum drops from 220px to 0 so it always fits. */
|
|
.markets-bar__inner {
|
|
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
|
gap: 0;
|
|
}
|
|
.markets-bar .mkt {
|
|
grid-template-columns: auto 1fr auto;
|
|
grid-template-rows: auto;
|
|
padding: 5px 6px;
|
|
gap: 4px;
|
|
font-size: 10px;
|
|
}
|
|
/* Re-flow the chip's grid so it's a single row of three: dot,
|
|
code, change. The 2-row layout (which had state/when on row 2)
|
|
is dropped along with the elements that lived there. */
|
|
.markets-bar .mkt .mkt__dot {
|
|
grid-row: 1; grid-column: 1;
|
|
width: 6px; height: 6px;
|
|
}
|
|
.markets-bar .mkt .mkt__name {
|
|
grid-row: 1; grid-column: 2;
|
|
font-size: 10px;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
.markets-bar .mkt .mkt__index {
|
|
grid-row: 1; grid-column: 3;
|
|
font-size: 10px;
|
|
}
|
|
/* Strip the now-redundant content. The elements still render but
|
|
occupy no space so the chip stays narrow. */
|
|
.markets-bar .mkt__state,
|
|
.markets-bar .mkt__when,
|
|
.markets-bar .mkt__index-label,
|
|
.markets-bar .mkt__index-price { display: none; }
|
|
}
|