diff --git a/frontend/src/components/AnalysisShell.css b/frontend/src/components/AnalysisShell.css index 4d1f955..2d7eaec 100644 --- a/frontend/src/components/AnalysisShell.css +++ b/frontend/src/components/AnalysisShell.css @@ -16,7 +16,10 @@ margin-inline: calc(50% - 50vw); padding-inline: max(16px, calc(50vw - 660px)); } -.ax-layout.is-collapsed { grid-template-columns: 0 minmax(0, 1fr); gap: 0; } +/* One column when the rail is away. It was `0 minmax(0, 1fr)`, and since the + rail is `display: none` the content became the *first* grid item — landing + in the zero-width column and wrapping one word per line. */ +.ax-layout.is-collapsed { grid-template-columns: minmax(0, 1fr); gap: 0; } .ax-main { min-width: 0; padding: 8px 0 48px; } diff --git a/frontend/src/components/ArticleSplitPane.css b/frontend/src/components/ArticleSplitPane.css index 78500d3..585a2fa 100644 --- a/frontend/src/components/ArticleSplitPane.css +++ b/frontend/src/components/ArticleSplitPane.css @@ -15,11 +15,11 @@ cross-reference must not move the article you were reading. */ .article-split.is-open > .article-split-main, .article-split.is-open > .article-split-pane { - max-height: calc(100vh - 120px); + max-height: calc(100dvh - 120px); overflow-y: auto; } /* The rail is sticky inside its own pane now, not inside the window. */ -.article-page.is-split .article-sections { top: 6px; max-height: calc(100vh - 200px); } +.article-page.is-split .article-sections { top: 6px; max-height: calc(100dvh - 200px); } .article-page.is-split .article-layout { grid-template-columns: 190px 1fr; gap: 14px; } .article-page.is-split .article-content { padding: 16px 18px; } diff --git a/frontend/src/components/TeachChat.jsx b/frontend/src/components/TeachChat.jsx index 98e7daa..6941503 100644 --- a/frontend/src/components/TeachChat.jsx +++ b/frontend/src/components/TeachChat.jsx @@ -304,7 +304,7 @@ export default function TeachChat({ question, attemptId, elevated = false }) { left: auto !important; top: 0 !important; width: 340px !important; - height: 100vh !important; + height: 100dvh !important; border-radius: 0 !important; border-top: none !important; border-left: 2px solid var(--primary) !important; diff --git a/frontend/src/index.css b/frontend/src/index.css index c65e3ac..1dc30f2 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -114,7 +114,18 @@ body { too wide then scrolls the whole page sideways — which is why the navbar could be found clipped mid-word at its left edge. Anything genuinely wider than the window scrolls inside its own box instead. */ -html, body { overflow-x: hidden; max-width: 100%; } +html, body { max-width: 100%; } +/* `overflow-x: hidden` on an ancestor silently disables `position: sticky` for + every descendant — it makes the element a scroll container, and a sticky + child sticks to that instead of to the viewport. That is why the session + rail, the settings nav and the article column would not stay put. `clip` + does the same job without becoming a scroll container. Hidden is kept as + the fallback, so a browser without `clip` still cannot be scrolled + sideways; it just loses stickiness, which is the lesser fault. */ +html, body { overflow-x: hidden; } +@supports (overflow-x: clip) { + html, body { overflow-x: clip; } +} /* ── Layout ─────────────────────────────────────────────────── */ .container { max-width: 1200px; margin: 0 auto; padding: 0 28px; } @@ -732,7 +743,7 @@ html, body { overflow-x: hidden; max-width: 100%; } border-radius: var(--card-radius); padding: 16px; box-shadow: var(--card-shadow); - max-height: calc(100vh - 100px); + max-height: calc(100dvh - 100px); overflow-y: auto; } diff --git a/frontend/src/pages/AiModePage.css b/frontend/src/pages/AiModePage.css index d28e37c..5aa0d00 100644 --- a/frontend/src/pages/AiModePage.css +++ b/frontend/src/pages/AiModePage.css @@ -3,7 +3,7 @@ .ai-page { display: grid; grid-template-columns: 230px 1fr; gap: 18px; align-items: start; max-width: 1060px; margin: 0 auto; } .ai-rail { - position: sticky; top: 76px; max-height: calc(100vh - 100px); overflow-y: auto; + position: sticky; top: 76px; max-height: calc(100dvh - 100px); overflow-y: auto; background: var(--card-bg); border: 1px solid var(--border); border-radius: 12px; padding: 12px; } @@ -72,7 +72,12 @@ .ai-error { color: var(--wrong-fg); font-size: 0.85rem; margin: 0; } -.ai-composer { display: flex; gap: 8px; align-items: flex-end; position: sticky; bottom: 0; padding-bottom: 12px; background: var(--bg); } +.ai-composer { + display: flex; gap: 8px; align-items: flex-end; + position: sticky; bottom: 0; background: var(--bg); + /* Clear of the home indicator, or the send button sits under it. */ + padding-bottom: calc(12px + env(safe-area-inset-bottom)); +} .ai-composer textarea { flex: 1; min-width: 0; resize: vertical; padding: 11px 14px; border: 1px solid var(--border); border-radius: 10px; diff --git a/frontend/src/pages/AnalysisSessionPage.test.jsx b/frontend/src/pages/AnalysisSessionPage.test.jsx index 8252d30..8742fed 100644 --- a/frontend/src/pages/AnalysisSessionPage.test.jsx +++ b/frontend/src/pages/AnalysisSessionPage.test.jsx @@ -138,6 +138,21 @@ describe('a session whose only attempt is still in progress', () => { }) }) +describe('the session rail', () => { + it('gives the content the whole width when the rail is put away', async () => { + mountAttempt() + await screen.findByRole('complementary', { name: 'Sessions' }) + const layout = document.querySelector('.ax-layout') + expect(layout).not.toHaveClass('is-collapsed') + + await userEvent.click(screen.getByRole('button', { name: /Hide|Collapse/i })) + // Hiding the rail used to leave the content in a zero-width grid column, + // wrapping one word per line beside an empty page. + expect(document.querySelector('.ax-layout')).toHaveClass('is-collapsed') + expect(document.querySelector('.ax-main')).toBeInTheDocument() + }) +}) + describe('a study session with every question answered', () => { it('becomes the review without waiting to be handed in', async () => { api.get.mockImplementation(url => { diff --git a/frontend/src/pages/ArticlesPage.css b/frontend/src/pages/ArticlesPage.css index 11ee83e..b794aa8 100644 --- a/frontend/src/pages/ArticlesPage.css +++ b/frontend/src/pages/ArticlesPage.css @@ -20,7 +20,7 @@ its whole length, scrolling on their own when there are more sections than screen. */ .article-sections { - position: sticky; top: 76px; max-height: calc(100vh - 96px); overflow-y: auto; + position: sticky; top: 76px; max-height: calc(100dvh - 96px); overflow-y: auto; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--card-radius); padding: 14px; } diff --git a/frontend/src/pages/ForgotPasswordPage.jsx b/frontend/src/pages/ForgotPasswordPage.jsx index 6ec80d5..47a4de7 100644 --- a/frontend/src/pages/ForgotPasswordPage.jsx +++ b/frontend/src/pages/ForgotPasswordPage.jsx @@ -30,7 +30,7 @@ export default function ForgotPasswordPage() { } return ( -
+
🏥 PedsHub
diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx index b6bc6e0..59e7e5b 100644 --- a/frontend/src/pages/LandingPage.jsx +++ b/frontend/src/pages/LandingPage.jsx @@ -353,7 +353,7 @@ export default function LandingPage() { }, []) return ( -
+
{/* Auth modal overlay */} {authModal && ( diff --git a/frontend/src/pages/QuizPlayer.css b/frontend/src/pages/QuizPlayer.css index b985eb2..fd156f2 100644 --- a/frontend/src/pages/QuizPlayer.css +++ b/frontend/src/pages/QuizPlayer.css @@ -7,7 +7,7 @@ /* Session rail beside the question, like a Qbank session's left column. Below 1150px there is not room for both, so the topbar dropdown takes over. */ .quiz-player .quiz-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 28px; align-items: start; } -.quiz-player .quiz-sidebar { display: block; order: -1; position: sticky; top: 16px; max-height: calc(100vh - 32px); overflow-y: auto; } +.quiz-player .quiz-sidebar { display: block; order: -1; position: sticky; top: 16px; max-height: calc(100dvh - 32px); overflow-y: auto; } .quiz-rail-head { font-size: .68rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: #8b929c; padding: 0 0 8px; } .quiz-rail-list { display: flex; flex-direction: column; } .quiz-rail-item { diff --git a/frontend/src/pages/ResetPasswordPage.jsx b/frontend/src/pages/ResetPasswordPage.jsx index 8010263..1f5fba8 100644 --- a/frontend/src/pages/ResetPasswordPage.jsx +++ b/frontend/src/pages/ResetPasswordPage.jsx @@ -30,7 +30,7 @@ export default function ResetPasswordPage() { } return ( -
+
🏥 PedsHub
diff --git a/frontend/src/pages/StudyPlanBlockPage.css b/frontend/src/pages/StudyPlanBlockPage.css index 5c9f93a..01c1d1c 100644 --- a/frontend/src/pages/StudyPlanBlockPage.css +++ b/frontend/src/pages/StudyPlanBlockPage.css @@ -7,7 +7,7 @@ } /* ── Blocks rail ─────────────────────────────────────────────────── */ -.spb-rail { position: sticky; top: 76px; align-self: start; max-height: calc(100vh - 100px); overflow-y: auto; } +.spb-rail { position: sticky; top: 76px; align-self: start; max-height: calc(100dvh - 100px); overflow-y: auto; } .spb-rail-plan { display: block; padding: 12px 14px; font-weight: 700; font-size: .95rem; color: var(--text); text-decoration: none; border-bottom: 1px solid var(--border); diff --git a/frontend/src/pages/VerifyEmailPage.jsx b/frontend/src/pages/VerifyEmailPage.jsx index 728d954..6264531 100644 --- a/frontend/src/pages/VerifyEmailPage.jsx +++ b/frontend/src/pages/VerifyEmailPage.jsx @@ -16,7 +16,7 @@ export default function VerifyEmailPage() { }, [token]) return ( -
+
{status === 'verifying' && ( <>

Verifying your email...