read.markets/app/static/css/layout.css
Giorgio Gilestro 3e1a14f334 ui: flip tone relabel — "Pro" now maps to INTERMEDIATE, not NOVICE
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>
2026-05-29 11:23:52 +02:00

489 lines
16 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 {
/* 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); }
.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; }
}