ui: header toggles expand downward, not sideways

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>
This commit is contained in:
Giorgio Gilestro 2026-05-29 11:11:46 +02:00
parent 31a8efc27d
commit f57c863145

View file

@ -173,27 +173,60 @@ body.drawer-open .drawer-backdrop { opacity: 1; }
color: var(--bg); color: var(--bg);
} }
/* Collapse-when-idle behaviour: on hover-capable devices, hide the /* Collapse-when-idle behaviour: on hover-capable devices each toggle
* non-active option until the user hovers (or keyboard-focuses) the * shows only its active option. Hover or keyboard focus reveals the
* toggle. The mobile drawer overrides this further down. */ * 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) { @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, .tone-toggle button,
.theme-toggle button, .theme-toggle button,
.lang-toggle button { display: none; } .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="NOVICE"] button[data-value="NOVICE"],
.tone-toggle[data-tone="INTERMEDIATE"] button[data-value="INTERMEDIATE"], .tone-toggle[data-tone="INTERMEDIATE"] button[data-value="INTERMEDIATE"],
.theme-toggle[data-theme="light"] button[data-value="light"], .theme-toggle[data-theme="light"] button[data-value="light"],
.theme-toggle[data-theme="dark"] button[data-value="dark"], .theme-toggle[data-theme="dark"] button[data-value="dark"],
.lang-toggle[data-lang="en"] button[data-value="en"], .lang-toggle[data-lang="en"] button[data-value="en"],
.lang-toggle[data-lang="it"] button[data-value="it"] { .lang-toggle[data-lang="it"] button[data-value="it"] {
display: inline-block; display: block;
position: static;
margin-top: 0;
border: 0;
} }
.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: inline-block; }
} }
.app-main { .app-main {