+
)}
diff --git a/frontend/src/pages/QuizPage.test.jsx b/frontend/src/pages/QuizPage.test.jsx
index 4247c2e..82eec7b 100644
--- a/frontend/src/pages/QuizPage.test.jsx
+++ b/frontend/src/pages/QuizPage.test.jsx
@@ -155,7 +155,9 @@ describe('quiz player', () => {
// "hard" narrows the answer before the stem has been read, so it waits
// until the answer is in.
expect(within(meta).queryByText('hard')).not.toBeInTheDocument()
- expect(within(meta).getByText('Multiple choice')).toBeInTheDocument()
+ // And "Multiple choice" is not said at all: every question is, and a pill
+ // repeating it above five lettered options labels the obvious.
+ expect(within(meta).queryByText('Multiple choice')).not.toBeInTheDocument()
fireEvent.keyDown(window, { key: '1' })
expect(await within(meta).findByText('hard')).toBeInTheDocument()
@@ -693,7 +695,11 @@ describe('quiz player', () => {
await userEvent.click(within(nav).getByRole('button', { name: /Next/ }))
await findStem('Full second clinical question.')
expect(document.querySelector('.quiz-block-meta').textContent).toContain('Item: 2 of 2')
- expect(within(document.querySelector('.quiz-item-nav')).getByRole('button', { name: /Next/ })).toBeDisabled()
+ // Next stays live at the last item and ends the block, which is what
+ // "next" means at the end of a paper. End Block is still its own control.
+ await userEvent.click(within(document.querySelector('.quiz-item-nav')).getByRole('button', { name: /Next/ }))
+ expect(screen.getByRole('dialog', { name: /This block is incomplete/ })).toBeInTheDocument()
+ await userEvent.click(screen.getByRole('button', { name: 'Remain in Block' }))
// The tools kept their end of the bar.
expect(within(document.querySelector('.quiz-top-actions')).getByRole('button', { name: 'Lab values' })).toBeInTheDocument()
diff --git a/frontend/src/pages/QuizPlayer.css b/frontend/src/pages/QuizPlayer.css
index a01e781..f1ea8a5 100644
--- a/frontend/src/pages/QuizPlayer.css
+++ b/frontend/src/pages/QuizPlayer.css
@@ -552,8 +552,11 @@ body:has(.quiz-player.is-boxed) .site-footer { display: none; }
Both are optional columns; the question takes whatever they leave. */
.quiz-player .quiz-layout.is-rail-closed { grid-template-columns: minmax(0, 1fr); }
.quiz-player .quiz-layout.is-rail-closed .quiz-sidebar { display: none; }
-.quiz-player .quiz-layout.has-labs { grid-template-columns: 260px minmax(0, 1fr) minmax(300px, 360px); }
-.quiz-player .quiz-layout.has-labs.is-rail-closed { grid-template-columns: minmax(0, 1fr) minmax(300px, 360px); }
+/* Wide enough for the whole row. At 360px the reference range — the one
+ column anybody opens this for — ran off the right-hand edge and there was
+ nothing to scroll sideways to reach it. */
+.quiz-player .quiz-layout.has-labs { grid-template-columns: 260px minmax(0, 1fr) minmax(360px, 440px); }
+.quiz-player .quiz-layout.has-labs.is-rail-closed { grid-template-columns: minmax(0, 1fr) minmax(360px, 440px); }
.quiz-rail-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.quiz-rail-hide, .quiz-rail-reopen {
@@ -565,16 +568,61 @@ body:has(.quiz-player.is-boxed) .site-footer { display: none; }
/* Sits where the rail was, so putting it back is where you last saw it. */
.quiz-rail-reopen { align-self: start; margin-top: 4px; }
-.quiz-labs {
+.quiz-labpanel {
display: flex; flex-direction: column; min-height: 0;
border-left: 1px solid var(--border); background: var(--card-bg);
}
-.quiz-labs-head {
+.quiz-labpanel-head {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 10px 12px; border-bottom: 1px solid var(--border); flex: none;
}
-.quiz-labs-head button { border: 0; background: none; cursor: pointer; color: var(--text-muted); font-size: 0.95rem; }
-.quiz-labs-body { flex: 1; min-height: 0; overflow-y: auto; padding: 10px 12px; }
+.quiz-labpanel-head button { border: 0; background: none; cursor: pointer; color: var(--text-muted); font-size: 0.95rem; }
+.quiz-labpanel-body { flex: 1; min-height: 0; overflow-y: auto; padding: 10px 12px; }
+
+/* The reference list was written for a dialog, where `.quiz-tool-body` gives
+ its controls a size and a shape. Docked beside the question it had neither,
+ so the group filters came out as bare browser buttons crammed edge to edge
+ and the search box was a default input with a Refresh beside it running off
+ the panel. Same controls, same look, in the column. */
+.quiz-labpanel-body .quiz-reference-controls { align-items: stretch; gap: 8px; }
+.quiz-labpanel-body .quiz-reference-controls label {
+ display: flex; flex-direction: column; gap: 4px;
+ font-size: .72rem; font-weight: 600; letter-spacing: .04em;
+ text-transform: uppercase; color: var(--text-muted);
+}
+.quiz-labpanel-body input:not([type=checkbox]) {
+ width: 100%; padding: 8px 10px; font: inherit; font-size: .85rem;
+ color: var(--text); background: var(--input-bg);
+ border: 1px solid var(--border); border-radius: 7px;
+}
+.quiz-labpanel-body button {
+ padding: 7px 11px; font: inherit; font-size: .8rem; cursor: pointer;
+ color: var(--text); background: var(--bg);
+ border: 1px solid var(--border); border-radius: 7px;
+}
+.quiz-labpanel-body button:hover { border-color: var(--primary); color: var(--primary); }
+.quiz-labpanel-body button:disabled { opacity: .5; cursor: not-allowed; }
+
+/* The group filters are a row of pills, and they wrap rather than squeeze. */
+.quiz-labpanel-body .quiz-tool-tabs { gap: 5px; margin: 12px 0; }
+.quiz-labpanel-body .quiz-tool-tabs button { padding: 5px 10px; font-size: .75rem; border-radius: 999px; }
+.quiz-labpanel-body .quiz-tool-tabs button[aria-pressed=true] {
+ background: var(--primary); border-color: var(--primary); color: #fff;
+}
+.quiz-labpanel-body .quiz-reference-note { margin: 0 0 12px; font-size: .78rem; line-height: 1.5; }
+.quiz-labpanel-body .quiz-reference-scroll { max-height: none; overflow-x: visible; }
+
+/* One column, not two. The dialog is wide enough to put the test and its range
+ side by side; this column is not, and the half that got squeezed was the
+ range. Stacked, every value is legible at any panel width, and the list
+ scrolls rather than the row clipping. */
+.quiz-labpanel-body .quiz-lab-row {
+ grid-template-columns: minmax(0, 1fr);
+ padding: 10px 12px; row-gap: 4px;
+}
+.quiz-labpanel-body .quiz-lab-age-line { grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; }
+.quiz-labpanel-body .quiz-lab-range { white-space: normal; overflow-wrap: anywhere; text-align: right; }
+.quiz-labpanel-body .quiz-lab-values { width: 100%; }
@media (max-width: 1150px) {
/* The rail is a drawer down here, so its handle has nothing to reopen. */
@@ -582,7 +630,7 @@ body:has(.quiz-player.is-boxed) .site-footer { display: none; }
/* No room for a third column; the labs go back to being a dialog. */
.quiz-player .quiz-layout.has-labs,
.quiz-player .quiz-layout.has-labs.is-rail-closed { grid-template-columns: minmax(0, 1fr); }
- .quiz-labs { display: none; }
+ .quiz-labpanel { display: none; }
}
@@ -596,8 +644,14 @@ body:has(.quiz-player.is-boxed) .site-footer { display: none; }
.quiz-player.is-exam-chrome {
position: fixed; inset: 0; z-index: 400;
height: 100dvh; max-height: 100dvh;
+ /* Flush. The player's own 24/32/40 gutters are for a page inside a site;
+ this is the whole window, and a white margin around a navy bar reads as a
+ dialog somebody forgot to close. */
+ padding: 0;
background: var(--card-bg);
}
+.quiz-player.is-exam-chrome .quiz-layout { gap: 0; }
+.quiz-player.is-exam-chrome .quiz-main > *:not(.quiz-topbar) { padding: 0 24px 24px; }
body:has(.quiz-player.is-exam-chrome) .navbar,
body:has(.quiz-player.is-exam-chrome) .site-footer { display: none; }
/* The title, the mode pill and the progress bar are the session list's way of
@@ -620,7 +674,8 @@ body:has(.quiz-player.is-exam-chrome) .site-footer { display: none; }
font-size: .82rem; line-height: 1.35; color: #fff;
background: rgba(255, 255, 255, .07); border: 1px solid rgba(255, 255, 255, .35);
}
-.quiz-player.is-exam-chrome .quiz-block-meta strong { font-weight: 700; }
+.quiz-player.is-exam-chrome .quiz-block-meta strong { color: #fff; font-weight: 700; }
+.quiz-player.is-exam-chrome .quiz-item-count { color: #fff; }
.quiz-player.is-exam-chrome .quiz-item-nav {
flex: 1; display: flex; align-items: center; justify-content: center; gap: 14px;
@@ -655,7 +710,22 @@ body:has(.quiz-player.is-exam-chrome) .site-footer { display: none; }
/* The rail is the exam's question-status list: numbers and how each stands,
without the excerpt a study session shows. */
-.quiz-player.is-exam-chrome .quiz-rail { background: var(--exam-bar); border: 0; border-radius: 0; }
+.quiz-player.is-exam-chrome .quiz-rail {
+ padding: 14px 12px; background: var(--exam-bar); border: 0; border-radius: 0;
+}
+.quiz-player.is-exam-chrome .quiz-rail-hide {
+ background: transparent; border-color: rgba(255, 255, 255, .55); color: #fff;
+}
+.quiz-player.is-exam-chrome .quiz-rail-hide:hover {
+ background: rgba(255, 255, 255, .18); border-color: #fff; color: #fff;
+}
+/* And the handle that brings it back, which sits on the navy where the rail
+ was rather than on the page. */
+.quiz-player.is-exam-chrome .quiz-rail-reopen {
+ margin: 10px 0 0 10px;
+ background: var(--exam-bar); border-color: var(--exam-bar); color: #fff;
+}
+.quiz-player.is-exam-chrome .quiz-rail-reopen:hover { background: #14304f; border-color: #14304f; }
.quiz-player.is-exam-chrome .quiz-rail,
.quiz-player.is-exam-chrome .quiz-rail-head span,
.quiz-player.is-exam-chrome .quiz-rail-item { color: #eef2f8; }
diff --git a/frontend/src/pages/ResultsPage.jsx b/frontend/src/pages/ResultsPage.jsx
index e7d2700..d04c00e 100644
--- a/frontend/src/pages/ResultsPage.jsx
+++ b/frontend/src/pages/ResultsPage.jsx
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { useParams, useNavigate, useLocation, useSearchParams, Link } from 'react-router-dom'
import api from '../api/client'
import { useSessionDrawer } from '../context/SessionDrawer'
+import { useClaimSessionChrome } from '../context/SessionChrome'
import useMediaQuery from '../hooks/useMediaQuery'
import QuizTools, { LabValues } from '../components/QuizTools'
import FigureStrip from '../components/FigureStrip'
@@ -45,6 +46,7 @@ export default function ResultsPage() {
const [responseStats, setResponseStats] = useState(null)
const [navOpen, setNavOpen] = useState(false)
const hasRail = useMediaQuery('(min-width: 1151px)')
+ useClaimSessionChrome(true)
// On a phone the site's burger opens this attempt's questions, exactly as it
// does while one is being sat — the rail has nowhere to live at that width,
// and a second hamburger for the same list is not an answer.
@@ -273,12 +275,12 @@ export default function ResultsPage() {
{labsOpen && (
-