/* 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 { /* Three-column grid: brand+BETA pinned left, nav truly centered in the middle column regardless of side widths, header-right pinned right. The mobile-drawer wrapper is display:contents on desktop so its children (nav, .header-right) become direct grid items and land in columns 2 and 3 by source order. */ display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 14px; 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 .header-left { display: inline-flex; align-items: center; gap: 10px; justify-self: start; } .app-header nav { justify-self: center; } .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:first-child { margin-left: 0; } .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; justify-self: end; } /* 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; } /* Segmented toggles — tone (Novice | Intermediate), theme (Light | Dark) * and language (EN | IT) share one visual rhythm so the three controls * read as a single cluster in the header. By default only the currently * active option is rendered; hover or keyboard focus reveals both so the * user can pick the other. Touch devices (which can't hover) show both * options at all times; the @media (hover: hover) gate handles that. */ .tone-toggle, .theme-toggle, .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; } .tone-toggle button, .theme-toggle button, .lang-toggle button { background: transparent; color: var(--muted); border: 0; padding: 4px 10px; cursor: pointer; font: inherit; letter-spacing: inherit; text-transform: inherit; /* Fixed min-width so the active-only width matches the expanded width of a single button — prevents the layout jumping as the user mouses over and the second option appears. */ min-width: 5.5em; text-align: center; } .tone-toggle button + button, .theme-toggle button + button, .lang-toggle button + button { border-left: 1px solid var(--border); } /* The tone-toggle's longer option ("Intermediate", 12 chars) needs more room than the shared 5.5em min-width. We size both buttons to fit the longest one so the popup width (set by container width via left/right:0) doesn't get clipped when only the short "Pro" label is active. */ .tone-toggle button { min-width: 10em; } .tone-toggle button:hover, .theme-toggle button:hover, .lang-toggle button:hover { color: var(--accent); } /* Active-option highlighting (data-* attribute on the container is * authored by JS on load and on every change). */ .tone-toggle[data-tone="NOVICE"] button[data-value="NOVICE"], .tone-toggle[data-tone="INTERMEDIATE"] button[data-value="INTERMEDIATE"], .theme-toggle[data-theme="light"] button[data-value="light"], .theme-toggle[data-theme="dark"] button[data-value="dark"], .lang-toggle[data-lang="en"] button[data-value="en"], .lang-toggle[data-lang="it"] button[data-value="it"] { background: var(--accent); color: var(--bg); } /* Collapse-when-idle behaviour: on hover-capable devices each toggle * shows only its active option. Hover or keyboard focus reveals the * other option STACKED ABSOLUTELY BELOW so the toggle's in-flow size * never changes — neighbouring controls don't shift when the user * mouses over one of them. */ @media (hover: hover) { .tone-toggle, .theme-toggle, .lang-toggle { position: relative; } /* Hide every option by default. The active option's higher-specificity rule below puts it back into the static flow. */ .tone-toggle button, .theme-toggle button, .lang-toggle button { display: none; } /* Hover / focus: render every option as an absolutely-positioned button immediately under the container. The active-button rule immediately below wins on specificity and pins it back into the static flow at the top — only the non-active option(s) actually end up absolutely-positioned, so the popup grows downward only. */ .tone-toggle:hover button, .tone-toggle:focus-within button, .theme-toggle:hover button, .theme-toggle:focus-within button, .lang-toggle:hover button, .lang-toggle:focus-within button { display: block; position: absolute; top: 100%; left: 0; right: 0; margin-top: -1px; /* share the container's bottom border */ background: var(--surface); border: 1px solid var(--border); z-index: 60; /* above the markets bar (z-50) */ } /* Active option stays in static flow at the top of the container even while hovered. Two-attribute specificity (.X[data=Y] btn[data=Y]) beats the .X:hover button rule above. */ .tone-toggle[data-tone="NOVICE"] button[data-value="NOVICE"], .tone-toggle[data-tone="INTERMEDIATE"] button[data-value="INTERMEDIATE"], .theme-toggle[data-theme="light"] button[data-value="light"], .theme-toggle[data-theme="dark"] button[data-value="dark"], .lang-toggle[data-lang="en"] button[data-value="en"], .lang-toggle[data-lang="it"] button[data-value="it"] { display: block; position: static; margin-top: 0; border: 0; } } .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) { /* Revert to flex on mobile so the drawer-toggle can pin to the right via margin-left:auto and the off-screen drawer doesn't try to claim a grid column. */ .app-header { display: flex; 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; } /* 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 .tone-toggle, .mobile-drawer .theme-toggle, .mobile-drawer .lang-toggle { display: inline-flex; width: 100%; justify-content: center; } /* Inside the drawer all options stay visible — undoes the hover-collapse from the @media (hover: hover) block above. Also splits the row evenly and bumps the button padding for thumb taps. */ .mobile-drawer .tone-toggle button, .mobile-drawer .theme-toggle button, .mobile-drawer .lang-toggle button { display: inline-block; flex: 1; padding: 10px; font-size: 11.5px; min-width: 0; } /* 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; } }