Files
quiz/static/style.css
ameer 19603abc58 fix: hide score on submit + total denominator + projector chart cleanup
Three small UX/fairness tweaks from manual live testing:

1. Post-submit "wait for reveal" screen: show only the response time, no
   score. The +score reveal leaked correctness — any positive number =
   correct, zero = wrong — short-circuiting the "stop and think" beat
   the reveal pause was supposed to enforce. Time stays as the
   engagement signal; score now waits for the instructor reveal.

2. Final-screen "Correct X / Y" denominator is now total_questions
   instead of questions_answered. Missed questions are scored zero, so
   they belong in the denominator visibly. Server adds total_questions
   to the session_ended payload.

3. Projector score-distribution: drop the in-chart count labels (they
   collided with each other and with the median tag at small N), restore
   the previously-computed-but-not-rendered x-axis tick labels at the
   bottom. Stats line at the foot keeps n / mean / max.

Also: short-circuit the per-submit instructor + presence broadcasts
when no instructor / projector is connected (no listener, no DB work).
The 50-student load test was tight on margin against its 2 s
time_limit; with the new presence_message / live_histogram_message DB
queries firing on every submit, the margin disappeared on busy boxes.
Conftest fixture also bumped to 8 s per question for the same reason —
gives breathing room for sequential WS submits in the load test.

71/71 pytest green.
2026-05-04 18:25:44 +08:00

1575 lines
39 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
* Quiz portal — Editorial Lecture Hall stylesheet.
*
* Aesthetic: a calm, typographic, textbook-like surface for an in-class
* live quiz. Source Serif 4 carries question prose; IBM Plex Sans is the
* chrome face; IBM Plex Mono handles tabular numerics. One ink-blue
* accent in light mode; brass-amber in dark mode.
*
* Class names align with admin.js and quiz.js. Behaviour is unchanged;
* this file is purely visual.
* ============================================================ */
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&display=swap");
:root {
color-scheme: light dark;
/* Surfaces — warm paper, restrained */
--bg: #f4efe6;
--bg-grain: rgba(31, 29, 24, 0.018);
--surface: #fdfbf6;
--surface-2: #f8f3e8;
--border: #d9d2c2;
--border-strong: #1a1a1a;
--rule: #1a1a1a;
/* Ink */
--text: #15171c;
--text-soft: #2c2f38;
--muted: #6a6760;
--muted-2: #908b80;
/* One accent — ink-blue */
--primary: #1f3d6b;
--primary-soft: #2e5292;
--primary-text: #fbf7ec;
--accent: #1f3d6b;
/* Status */
--warn: #8a5a14;
--warn-text: #fbf7ec;
--danger: #7a221c;
--danger-text: #fbf7ec;
--info: #1f3d6b;
--correct-border: #4f6b35;
--correct-bg: rgba(79, 107, 53, 0.08);
--wrong-border: #7a221c;
--wrong-bg: rgba(122, 34, 28, 0.08);
/* Top-3 medals — subtle, restrained, but readable across a hall */
--medal-gold: #a07a1f;
--medal-silver: #595c66;
--medal-bronze: #8a4a1f;
--shadow-sm: 0 1px 0 rgba(20, 22, 28, 0.04);
--shadow: 0 1px 0 rgba(20, 22, 28, 0.05), 0 12px 36px -20px rgba(20, 22, 28, 0.18);
--shadow-strong: 0 1px 0 rgba(20, 22, 28, 0.06), 0 22px 48px -24px rgba(20, 22, 28, 0.28);
/* Type system */
--font-display: "Source Serif 4", "Source Serif Pro", ui-serif, Georgia, "Times New Roman", serif;
--font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-family: var(--font-sans);
background: var(--bg);
color: var(--text);
font-feature-settings: "ss01", "ss02", "cv11";
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100dvh;
background:
radial-gradient(1200px 700px at 100% -10%, color-mix(in srgb, var(--primary) 5%, transparent) 0%, transparent 60%),
radial-gradient(900px 500px at -10% 110%, color-mix(in srgb, var(--primary) 4%, transparent) 0%, transparent 55%),
var(--bg);
color: var(--text);
font-size: 16px;
line-height: 1.5;
letter-spacing: 0.005em;
}
::selection { background: color-mix(in srgb, var(--primary) 30%, transparent); color: var(--text); }
/* ---------- Headings & inline ---------- */
h1, h2, h3 { margin: 0 0 0.4rem; line-height: 1.15; }
h1 {
font-family: var(--font-display);
font-size: 1.6rem;
font-weight: 600;
letter-spacing: -0.012em;
font-feature-settings: "ss01";
}
h2 {
font-family: var(--font-sans);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--text);
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
h3 {
font-family: var(--font-sans);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
margin: 18px 0 10px;
}
p { margin: 0 0 0.6rem; }
code {
font-family: var(--font-mono);
font-size: 0.85em;
letter-spacing: -0.01em;
}
.muted { color: var(--muted); }
.small { font-size: 0.85rem; }
.eyebrow {
color: var(--muted);
font-family: var(--font-sans);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.2em;
font-weight: 600;
margin: 0 0 0.6rem;
}
/* ---------- Layout containers ---------- */
.bootstrap-loading {
display: grid;
place-items: center;
min-height: 100dvh;
color: var(--muted);
font-size: 0.78rem;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.bootstrap-loading::after {
content: "";
display: inline-block;
width: 22px;
height: 1px;
background: var(--text);
margin-left: 14px;
animation: lineblink 1.1s ease-in-out infinite;
}
@keyframes lineblink {
0%, 100% { opacity: 0.2; transform: scaleX(0.4); }
50% { opacity: 1; transform: scaleX(1); }
}
.centered-shell {
display: grid;
place-items: center;
min-height: 100dvh;
padding: 24px 18px;
}
/* ---------- Cards & panels ---------- */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: var(--shadow);
padding: 28px;
position: relative;
}
.card.narrow { width: min(440px, 100%); }
.card-header { margin-bottom: 18px; }
.card-header h1 { margin-bottom: 6px; }
.card.center { text-align: center; }
.panel { padding: 22px 24px; }
.panel + .panel { margin-top: 16px; }
.panel h2 .count {
background: transparent;
color: var(--muted);
border: 1px solid var(--border);
border-radius: 0;
padding: 1px 8px;
font-size: 0.72rem;
font-family: var(--font-mono);
letter-spacing: 0;
font-weight: 500;
margin-left: auto;
}
.stack { display: grid; gap: 16px; }
/* ---------- Forms ---------- */
.field { display: grid; gap: 8px; }
.field > span {
font-family: var(--font-sans);
font-weight: 600;
font-size: 0.7rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
input, textarea, select {
font: inherit;
font-family: var(--font-sans);
border: 1px solid var(--border);
border-radius: 2px;
padding: 11px 14px;
background: var(--surface);
color: var(--text);
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input:focus, textarea:focus, select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
/* ---------- Buttons ---------- */
.btn, button {
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 500;
letter-spacing: 0.04em;
border: 1px solid var(--border-strong);
border-radius: 2px;
padding: 10px 18px;
background: var(--surface);
color: var(--text);
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition:
transform 0.08s ease,
background 0.18s ease,
color 0.18s ease,
border-color 0.18s ease,
box-shadow 0.18s ease;
}
.btn:hover:not(:disabled), button:hover:not(:disabled) {
background: var(--text);
color: var(--surface);
}
.btn:active, button:active { transform: translateY(1px); }
.btn:disabled, button:disabled { cursor: not-allowed; opacity: 0.45; }
.btn:focus-visible, button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 35%, transparent);
}
.btn.primary {
background: var(--primary);
border-color: var(--primary);
color: var(--primary-text);
}
.btn.primary:hover:not(:disabled) {
background: var(--text);
border-color: var(--text);
color: var(--surface);
}
.btn.warn {
background: var(--surface);
border-color: var(--warn);
color: var(--warn);
}
.btn.warn:hover:not(:disabled) {
background: var(--warn);
color: var(--warn-text);
}
.btn.danger {
background: var(--danger);
border-color: var(--danger);
color: var(--danger-text);
}
.btn.danger:hover:not(:disabled) {
background: var(--text);
border-color: var(--text);
color: var(--surface);
}
.btn.ghost {
background: transparent;
border-color: var(--border);
color: var(--text-soft);
}
.btn.ghost:hover:not(:disabled) {
background: transparent;
border-color: var(--text);
color: var(--text);
}
.btn.block { width: 100%; }
.btn.big {
padding: 14px 26px;
font-size: 0.95rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.btn.small { padding: 6px 12px; font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase; }
/* ---------- Alerts ---------- */
.alert {
padding: 12px 18px;
border-radius: 2px;
font-size: 0.88rem;
border-left: 3px solid var(--border);
background: var(--surface);
margin: 0 24px 16px;
}
.alert.error { border-left-color: var(--danger); color: var(--danger); background: var(--wrong-bg); }
.alert.error { margin: 0; }
.alert.info { border-left-color: var(--primary); color: var(--primary); background: color-mix(in srgb, var(--primary) 5%, transparent); }
/* ---------- Reconnect banner ---------- */
.reconnect-banner {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
z-index: 50;
padding: 8px 16px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
color: var(--warn-text);
background: var(--warn);
box-shadow: var(--shadow);
letter-spacing: 0.01em;
pointer-events: none;
}
.reconnect-banner[hidden] { display: none; }
/* ---------- Admin topbar ---------- */
.admin-body { padding-bottom: 32px; }
.topbar {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 16px;
padding: 28px 32px 18px;
border-bottom: 1px solid var(--rule);
flex-wrap: wrap;
position: relative;
}
.topbar::before {
content: "Live Quiz";
position: absolute;
top: 18px;
left: 32px;
font-family: var(--font-sans);
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--muted-2);
}
.topbar-title { padding-top: 18px; }
.topbar-title h1 {
font-family: var(--font-display);
font-size: 1.7rem;
font-weight: 600;
letter-spacing: -0.012em;
margin-bottom: 4px;
}
.topbar-title p {
margin: 0;
font-size: 0.82rem;
color: var(--muted);
letter-spacing: 0.02em;
}
.topbar-actions { display: flex; gap: 12px; align-items: center; }
/* State badge — readable across a lecture hall */
.state-badge {
border-radius: 0;
padding: 7px 14px;
font-size: 0.72rem;
font-family: var(--font-sans);
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
border: 1px solid var(--border-strong);
background: var(--surface);
color: var(--text);
display: inline-flex;
align-items: center;
gap: 8px;
}
.state-badge::before {
content: "";
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--muted);
flex-shrink: 0;
}
.state-badge.state-lobby::before { background: var(--muted); }
.state-badge.state-question_open {
background: var(--primary);
color: var(--primary-text);
border-color: var(--primary);
}
.state-badge.state-question_open::before {
background: var(--primary-text);
animation: pulse 1.4s ease-in-out infinite;
}
.state-badge.state-question_closed::before { background: var(--warn); }
.state-badge.state-finished::before { background: var(--correct-border); }
.state-badge.state-correct {
background: var(--correct-bg);
color: var(--correct-border);
border-color: var(--correct-border);
}
.state-badge.state-correct::before { background: var(--correct-border); }
.state-badge.state-wrong {
background: var(--wrong-bg);
color: var(--danger);
border-color: var(--danger);
}
.state-badge.state-wrong::before { background: var(--danger); }
@keyframes pulse {
0%, 100% { opacity: 0.45; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.15); }
}
/* ---------- Admin dashboard layout ---------- */
.dashboard {
display: grid;
gap: 18px;
grid-template-columns: minmax(300px, 380px) 1fr;
padding: 22px 32px;
align-items: start;
}
@media (max-width: 900px) {
.dashboard { grid-template-columns: 1fr; padding: 18px; }
.topbar { padding: 26px 18px 16px; }
.topbar::before { left: 18px; }
}
.dashboard-side { display: grid; gap: 18px; }
.dashboard-main { display: grid; gap: 18px; }
/* ---------- Join panel (QR + URL + roster) ---------- */
.join-panel { padding: 0; overflow: hidden; }
.join-panel h2 {
margin: 0;
padding: 18px 22px 14px;
border-bottom: 1px solid var(--rule);
}
.join-panel h2::before {
content: "01";
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--muted-2);
letter-spacing: 0;
margin-right: 4px;
}
.qr-wrap {
background: #ffffff;
padding: 18px;
border: 0;
border-bottom: 1px solid var(--rule);
display: grid;
place-items: center;
margin: 0;
position: relative;
}
.qr-wrap::before {
content: "Scan to join";
position: absolute;
top: 14px;
left: 18px;
font-size: 0.62rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #2c2f38;
font-weight: 600;
}
.qr-wrap img {
width: 100%;
height: auto;
max-width: 260px;
display: block;
margin-top: 18px;
image-rendering: pixelated;
}
.qr-fallback { padding: 40px; color: var(--muted); }
.join-url-row {
display: flex;
gap: 10px;
align-items: stretch;
padding: 14px 22px;
border-bottom: 1px solid var(--border);
}
.join-url {
flex: 1 1 200px;
background: var(--surface-2);
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 2px;
font-family: var(--font-mono);
font-size: 0.78rem;
word-break: break-all;
color: var(--text);
display: flex;
align-items: center;
}
.join-panel .small {
padding: 10px 22px 18px;
margin: 0;
font-size: 0.75rem;
color: var(--muted);
letter-spacing: 0.04em;
}
.join-panel .small code {
background: var(--surface-2);
padding: 1px 6px;
border: 1px solid var(--border);
border-radius: 2px;
}
.roster {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0;
max-height: 380px;
overflow-y: auto;
}
.roster li {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px dotted var(--border);
animation: rosterIn 0.35s ease both;
}
.roster li:last-child { border-bottom: none; }
.roster .dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--muted-2);
flex-shrink: 0;
transition: background 0.5s ease, box-shadow 0.5s ease;
}
.roster li.is-fresh .dot {
background: var(--correct-border);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--correct-border) 22%, transparent);
}
.roster li.is-fresh:first-child .dot {
/* Newest-first emphasis — soft pulsing halo on the top row only. */
animation: rosterDotPulse 2.2s ease-in-out infinite;
}
.roster .who { display: grid; line-height: 1.25; }
.roster .who b { font-weight: 500; font-size: 0.95rem; }
.roster .who small {
color: var(--muted);
font-size: 0.74rem;
font-family: var(--font-mono);
letter-spacing: 0;
}
@keyframes rosterIn {
from { opacity: 0; transform: translateX(-6px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes rosterDotPulse {
0%, 100% { box-shadow: 0 0 0 2px color-mix(in srgb, var(--correct-border) 22%, transparent); }
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--correct-border) 12%, transparent); }
}
/* ---------- State CTA panel (lobby / finished) ----------
* The right column gets editorial weight here so the page doesn't read
* empty before the quiz starts (or after it ends). */
.state-cta { display: grid; gap: 14px; }
.state-cta h2 {
font-family: var(--font-display);
font-size: clamp(1.6rem, 2.4vw, 2.1rem);
font-weight: 500;
line-height: 1.12;
letter-spacing: -0.014em;
text-transform: none;
color: var(--text);
border-bottom: 0;
padding-bottom: 0;
margin-bottom: 2px;
}
.state-cta p {
font-family: var(--font-display);
font-size: 1.02rem;
color: var(--text-soft);
line-height: 1.55;
max-width: 60ch;
}
.state-cta .cta-eyebrow {
font-family: var(--font-sans);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
margin: 0 0 -6px;
display: flex;
align-items: baseline;
gap: 10px;
max-width: none;
}
.state-cta .cta-num {
font-family: var(--font-mono);
font-weight: 500;
font-size: 0.72rem;
letter-spacing: 0;
color: var(--muted-2);
}
.state-cta .btn.big { justify-self: start; margin-top: 14px; }
/* Inline numeric stats inside a CTA panel — gives the lobby/finished
* card visual mass without needing a hero illustration. Cap the strip
* width so it stays a coherent eye-block on a 1920px projector. */
.cta-stats {
display: flex;
flex-wrap: wrap;
gap: 0;
margin: 14px 0 4px;
max-width: 580px;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.cta-stat {
flex: 1 1 0;
min-width: 110px;
padding: 14px 18px;
border-right: 1px dotted var(--border);
}
.cta-stat:first-child { padding-left: 0; }
.cta-stat:last-child { border-right: 0; padding-right: 0; }
.cta-stat span {
display: block;
font-family: var(--font-sans);
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.cta-stat b {
display: block;
font-family: var(--font-mono);
font-size: 1.6rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
margin-top: 6px;
color: var(--text);
}
.cta-stat b small {
font-size: 0.78rem;
margin-left: 2px;
color: var(--muted);
letter-spacing: 0;
}
.action-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 22px;
padding-top: 18px;
border-top: 1px solid var(--border);
}
/* ---------- Question card (admin + student share) ---------- */
.question-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.qnum {
font-family: var(--font-mono);
font-weight: 500;
color: var(--muted);
font-size: 0.78rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.countdown {
font-family: var(--font-mono);
font-weight: 500;
font-size: 1.5rem;
font-variant-numeric: tabular-nums;
color: var(--text);
letter-spacing: -0.02em;
}
.countdown.urgent {
color: var(--danger);
animation: urgent-blink 0.8s ease-in-out infinite;
}
@keyframes urgent-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.55; }
}
.qbar {
height: 3px;
background: var(--surface-2);
border-radius: 0;
overflow: hidden;
margin-bottom: 22px;
position: relative;
}
.qbar span {
display: block;
height: 100%;
width: 100%;
background: var(--primary);
transition: width 0.25s linear;
transform-origin: left center;
}
.qbar::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--primary) 40%, transparent) 50%, transparent 100%);
animation: barbreath 2.4s ease-in-out infinite;
pointer-events: none;
mix-blend-mode: overlay;
opacity: 0.35;
}
@keyframes barbreath {
0%, 100% { transform: translateX(-30%); }
50% { transform: translateX(30%); }
}
.question-text,
h2.question-text,
h1.question-text {
font-family: var(--font-display);
font-size: clamp(1.5rem, 2.4vw, 1.95rem);
font-weight: 500;
line-height: 1.3;
letter-spacing: -0.008em;
text-transform: none;
margin: 14px 0 24px;
color: var(--text);
max-width: 60ch;
border-bottom: 0;
padding-bottom: 0;
display: block;
}
.question-text.small,
h2.question-text.small {
font-size: 1.2rem;
font-weight: 500;
margin: 10px 0 16px;
}
.options {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
counter-reset: option;
}
.options li {
display: grid;
grid-template-columns: 32px 1fr auto;
gap: 14px;
align-items: center;
padding: 12px 16px;
background: transparent;
border: 1px solid var(--border);
border-radius: 2px;
transition: border-color 0.18s ease, background 0.18s ease;
}
.options .key {
font-family: var(--font-mono);
font-weight: 600;
font-size: 0.92rem;
letter-spacing: 0;
text-align: left;
width: auto;
color: var(--muted);
background: transparent;
padding: 0;
border-right: 1px solid var(--border);
padding-right: 14px;
height: 22px;
display: inline-flex;
align-items: center;
}
.options .opt-text {
color: var(--text);
font-family: var(--font-display);
font-size: 1.02rem;
line-height: 1.4;
}
.options .opt-count {
color: var(--muted);
font-size: 0.78rem;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
.options.reveal li.correct {
background: var(--correct-bg);
border-color: var(--correct-border);
animation: correctDraw 0.5s ease both;
}
.options.reveal li.correct .key { color: var(--correct-border); border-right-color: var(--correct-border); }
.options.reveal li.wrong-pick {
border-color: var(--wrong-border);
background: var(--wrong-bg);
}
.options.reveal li.wrong-pick .key { color: var(--wrong-border); border-right-color: var(--wrong-border); }
@keyframes correctDraw {
0% { box-shadow: inset 0 0 0 0 var(--correct-border); }
60% { box-shadow: inset 0 0 0 2px var(--correct-border); }
100% { box-shadow: inset 0 0 0 0 var(--correct-border); }
}
.explanation {
background: transparent;
padding: 14px 18px;
border-radius: 0;
border: 0;
border-left: 2px solid var(--primary);
margin-top: 18px;
font-size: 0.98rem;
font-family: var(--font-display);
font-style: italic;
color: var(--text-soft);
line-height: 1.5;
max-width: 62ch;
}
/* ---------- Histogram ---------- */
.hist { margin-top: 22px; display: grid; gap: 10px; }
.hist::before {
content: "Live distribution";
font-family: var(--font-sans);
font-size: 0.68rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.hist.final::before { content: "Final distribution"; }
.hist-summary {
display: flex;
gap: 18px;
flex-wrap: wrap;
font-size: 0.85rem;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
color: var(--muted);
padding-bottom: 6px;
}
.hist-summary b { color: var(--text); font-weight: 600; }
.hist-awaiting {
margin: 4px 0 0;
padding: 0;
font-style: italic;
color: var(--muted);
}
.hist-rows { display: grid; gap: 6px; }
.hist-row {
display: grid;
grid-template-columns: 22px 1fr 90px;
gap: 12px;
align-items: center;
font-family: var(--font-mono);
font-size: 0.82rem;
}
.hist-row .key {
font-weight: 600;
color: var(--muted);
letter-spacing: 0;
font-family: var(--font-mono);
background: transparent;
border: 0;
padding: 0;
width: auto;
text-align: left;
}
.hist-row .bar {
height: 6px;
background: var(--surface-2);
border-radius: 0;
overflow: hidden;
border: 1px solid var(--border);
}
.hist-row .bar .fill {
display: block;
height: 100%;
background: var(--primary);
transition: width 0.45s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.hist-row.is-correct .bar .fill { background: var(--correct-border); }
.hist-row.is-correct .key { color: var(--correct-border); }
.hist-row.missed .bar { background: transparent; border-color: transparent; }
.hist-row.missed .key { color: var(--muted-2); }
.hist-row .num {
font-size: 0.78rem;
text-align: right;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ---------- Leaderboard ---------- */
.leaderboard {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0;
border-top: 1px solid var(--border);
}
.leaderboard li {
display: grid;
grid-template-columns: 36px 1fr auto;
gap: 14px;
align-items: center;
/* Reserve the 3px medal stripe space on every row so rank columns align. */
padding: 10px 12px 10px 15px;
border-radius: 0;
border-left: 3px solid transparent;
border-bottom: 1px solid var(--border);
position: relative;
transition: background 0.18s ease;
}
.leaderboard li:nth-child(even) { background: var(--surface-2); }
.leaderboard li:hover { background: color-mix(in srgb, var(--primary) 6%, transparent); }
.leaderboard .rank {
font-family: var(--font-mono);
font-weight: 600;
color: var(--muted);
font-variant-numeric: tabular-nums;
font-size: 0.85rem;
letter-spacing: 0;
text-align: right;
padding-right: 6px;
}
.leaderboard .who { display: grid; line-height: 1.2; min-width: 0; }
.leaderboard .who b {
font-weight: 500;
font-family: var(--font-display);
font-size: 1rem;
color: var(--text);
letter-spacing: -0.005em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.leaderboard .who small {
color: var(--muted);
font-size: 0.72rem;
font-family: var(--font-mono);
letter-spacing: 0;
}
.leaderboard .score {
font-family: var(--font-mono);
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--text);
font-size: 1.05rem;
letter-spacing: -0.01em;
}
/* Top-3 medal treatment — colored stripe + tinted plate. Stripe width is
* already reserved on every row, so rank columns stay aligned. */
.leaderboard li:nth-child(1) { border-left-color: var(--medal-gold); background: color-mix(in srgb, var(--medal-gold) 9%, var(--surface)); }
.leaderboard li:nth-child(2) { border-left-color: var(--medal-silver); background: color-mix(in srgb, var(--medal-silver) 9%, var(--surface)); }
.leaderboard li:nth-child(3) { border-left-color: var(--medal-bronze); background: color-mix(in srgb, var(--medal-bronze) 9%, var(--surface)); }
.leaderboard li:nth-child(1) .rank { color: var(--medal-gold); }
.leaderboard li:nth-child(2) .rank { color: var(--medal-silver); }
.leaderboard li:nth-child(3) .rank { color: var(--medal-bronze); }
.leaderboard li:nth-child(1) .score,
.leaderboard li:nth-child(2) .score,
.leaderboard li:nth-child(3) .score {
font-weight: 600;
}
/* "That's me" highlight on the student leaderboard. Blue stripe + name
* recolour + small inline "you" tag below the name. The tag rides the
* existing grid row of .who so it never collides with the score. */
.leaderboard li.is-you {
background: color-mix(in srgb, var(--primary) 10%, var(--surface));
border-left-color: var(--primary);
}
.leaderboard li.is-you .who { gap: 1px; }
.leaderboard li.is-you .who b { color: var(--primary); font-weight: 600; }
.leaderboard li.is-you .who::after {
content: "you";
font-family: var(--font-sans);
font-size: 0.58rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--primary);
font-weight: 600;
opacity: 0.85;
}
.leaderboard li.is-you .score { color: var(--primary); }
.leaderboard li.is-you:nth-child(1),
.leaderboard li.is-you:nth-child(2),
.leaderboard li.is-you:nth-child(3) {
/* When the student IS on the podium, keep the medal stripe; brighten
* the row tint so they still read as "you". */
background:
linear-gradient(90deg, color-mix(in srgb, var(--primary) 12%, transparent), color-mix(in srgb, var(--primary) 6%, transparent)),
var(--surface);
}
/* ---------- Student-side answer buttons ---------- */
.quiz-card { width: min(680px, 100%); padding: 28px 30px 32px; }
.answer-grid {
display: grid;
gap: 10px;
margin: 22px 0 0;
}
.answer-btn {
display: grid;
/* Letterless: option text fills the row. Generous padding compensates
* for the missing letter chip so the button still reads as a tile. */
grid-template-columns: 1fr;
gap: 18px;
align-items: center;
text-align: left;
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: 2px;
padding: 18px 20px;
font-size: 1rem;
min-height: 64px;
font-family: var(--font-sans);
font-weight: 500;
color: var(--text);
position: relative;
transition:
transform 0.12s cubic-bezier(0.22, 0.61, 0.36, 1),
background 0.18s ease,
border-color 0.18s ease,
color 0.18s ease;
}
.answer-btn:hover:not(:disabled) {
background: var(--text);
color: var(--surface);
border-color: var(--text);
}
.answer-btn:hover:not(:disabled) .answer-key {
color: var(--surface);
border-right-color: var(--surface);
}
.answer-btn.picked {
background: var(--primary);
color: var(--primary-text);
border-color: var(--primary);
animation: pickSettle 0.32s cubic-bezier(0.22, 1.4, 0.36, 1) both;
}
.answer-btn.picked .answer-key {
color: var(--primary-text);
border-right-color: color-mix(in srgb, var(--primary-text) 40%, transparent);
}
@keyframes pickSettle {
0% { transform: scale(1); }
35% { transform: scale(0.97); }
100% { transform: scale(1); }
}
.answer-btn .answer-key {
font-family: var(--font-mono);
font-weight: 600;
border: 0;
border-right: 1px solid var(--border);
background: transparent;
color: var(--muted);
padding: 0 14px 0 0;
text-align: left;
width: auto;
font-size: 1.05rem;
letter-spacing: 0;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: flex-start;
transition: color 0.18s ease, border-right-color 0.18s ease;
}
.answer-btn .answer-text {
font-family: var(--font-display);
font-weight: 500;
font-size: 1.18rem;
line-height: 1.35;
}
/* Student-reveal letterless variant: drop the 32px key column from the
* options row. The "Your pick" ribbon (.options.student-reveal li.yours)
* and correct/wrong tinting still work because they target the <li>. */
.options.letterless {
/* options li uses display:grid; redefine the columns when letterless. */
}
.options.letterless li {
grid-template-columns: 1fr auto;
padding-left: 18px;
padding-right: 18px;
}
.big-score {
font-family: var(--font-mono);
font-size: 4.2rem;
font-weight: 500;
color: var(--text);
margin: 14px 0 6px;
font-variant-numeric: tabular-nums;
letter-spacing: -0.04em;
line-height: 1;
}
/* Unit suffix (e.g. "s" after a duration) — small, muted, baseline-sized
* so it reads as a tag, not part of the number. */
.big-score .unit {
font-size: 0.32em;
color: var(--muted);
letter-spacing: 0;
margin-left: 6px;
vertical-align: 0.55em;
font-weight: 500;
}
.spinner {
width: 22px;
height: 22px;
margin: 18px auto 0;
border: 1.5px solid var(--border);
border-top-color: var(--text);
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ---------- Reveal stats (student) ---------- */
.reveal-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
margin: 20px 0 6px;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.reveal-stats .stat {
text-align: center;
padding: 14px 8px;
background: transparent;
border-radius: 0;
border-right: 1px dotted var(--border);
}
.reveal-stats .stat:last-child { border-right: 0; }
.reveal-stats .stat span {
display: block;
font-size: 0.66rem;
color: var(--muted);
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 600;
}
.reveal-stats .stat b {
display: block;
font-family: var(--font-mono);
font-size: 1.5rem;
font-weight: 500;
margin-top: 6px;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
color: var(--text);
}
.reveal-stats .stat.big b {
font-size: 2.6rem;
color: var(--primary);
}
.celebration-card {
text-align: center;
width: min(680px, 100%);
position: relative;
padding: 32px 30px 36px;
}
.celebration-banner {
background: transparent;
color: var(--text);
padding: 0 0 18px;
border-radius: 0;
font-family: var(--font-sans);
font-weight: 600;
font-size: 0.7rem;
letter-spacing: 0.32em;
text-transform: uppercase;
margin: -8px 0 14px;
border-bottom: 1px solid var(--rule);
position: relative;
}
.celebration-banner::before,
.celebration-banner::after {
content: "—";
display: inline-block;
padding: 0 14px;
color: var(--muted-2);
font-weight: 400;
letter-spacing: 0;
}
.celebration-card h3 {
font-family: var(--font-display);
font-size: 1.2rem;
font-weight: 600;
text-transform: none;
letter-spacing: -0.005em;
color: var(--text);
margin-top: 24px;
}
/* ---------- Student-specific reveal trims ----------
* The "Your pick" tag sits as a thin top-edge ribbon so it can't collide
* with the count column on narrow phones. */
.options.student-reveal li.yours {
position: relative;
padding-top: 22px;
}
.options.student-reveal li.yours::before {
content: "Your pick";
position: absolute;
top: 4px;
left: 16px;
font-family: var(--font-sans);
font-size: 0.58rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.options.student-reveal li.yours.correct::before { color: var(--correct-border); }
.options.student-reveal li.yours.wrong-pick::before { color: var(--wrong-border); }
/* ---------- Join-form disclaimer accordion ---------- */
.join-disclaimer {
border: 1px solid var(--border);
border-left: 3px solid var(--warn);
border-radius: 2px;
background: color-mix(in srgb, var(--warn) 4%, var(--surface));
padding: 0;
font-size: 0.86rem;
line-height: 1.45;
}
.join-disclaimer > summary {
cursor: pointer;
list-style: none;
padding: 11px 14px;
font-family: var(--font-sans);
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--warn);
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.join-disclaimer > summary::-webkit-details-marker { display: none; }
.join-disclaimer > summary::after {
content: "+";
font-family: var(--font-mono);
font-weight: 600;
font-size: 0.95rem;
letter-spacing: 0;
color: var(--warn);
transition: transform 0.18s ease;
}
.join-disclaimer[open] > summary::after { content: ""; }
.join-disclaimer > ul {
margin: 0;
padding: 0 18px 14px 32px;
display: grid;
gap: 6px;
color: var(--text-soft);
}
.join-disclaimer > ul li { padding-left: 2px; }
.join-disclaimer > ul li b { color: var(--text); font-weight: 600; }
.join-disclaimer > ul li em { font-style: italic; color: var(--text); }
/* ---------- Live presence panel (admin) ---------- */
.presence-panel { padding: 18px 20px 16px; }
.presence-list {
list-style: none;
margin: 0 0 12px;
padding: 0;
display: grid;
gap: 0;
max-height: 420px;
overflow-y: auto;
}
.presence-row {
display: grid;
grid-template-columns: 14px 1fr auto auto;
gap: 10px;
align-items: center;
padding: 9px 0;
border-bottom: 1px dotted var(--border);
}
.presence-row:last-child { border-bottom: 0; }
.presence-row .dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--muted-2);
flex-shrink: 0;
transition: background 0.4s ease, box-shadow 0.4s ease;
}
.presence-row.is-online .dot {
background: var(--correct-border);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--correct-border) 18%, transparent);
}
.presence-row.is-online.is-fresh .dot {
animation: rosterDotPulse 2.2s ease-in-out infinite;
}
.presence-row.is-stale .dot { background: var(--warn); }
.presence-row.is-offline .dot { background: var(--muted-2); }
.presence-row .who { display: grid; line-height: 1.2; min-width: 0; }
.presence-row .who b {
font-weight: 500;
font-size: 0.92rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.presence-row .who small {
color: var(--muted);
font-size: 0.7rem;
font-family: var(--font-mono);
letter-spacing: 0;
}
.presence-flags {
display: inline-flex;
gap: 4px;
align-items: center;
flex-wrap: wrap;
}
.flag {
font-family: var(--font-mono);
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0;
padding: 2px 7px;
border-radius: 0;
border: 1px solid var(--border);
color: var(--muted);
background: var(--surface);
font-variant-numeric: tabular-nums;
min-width: 18px;
text-align: center;
}
.flag-ok { color: var(--correct-border); border-color: var(--correct-border); }
.flag-pending { color: var(--muted-2); }
.flag-warn { color: var(--warn); border-color: var(--warn); }
.flag-danger {
color: var(--danger-text);
background: var(--danger);
border-color: var(--danger);
}
.btn.xtiny {
padding: 2px 8px;
font-size: 0.78rem;
min-height: 0;
letter-spacing: 0;
text-transform: none;
border-radius: 2px;
}
.legend-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 999px;
margin-right: 4px;
margin-left: 8px;
vertical-align: middle;
background: var(--muted-2);
}
.legend-dot:first-child { margin-left: 0; }
.legend-dot.is-online { background: var(--correct-border); }
.legend-dot.is-stale { background: var(--warn); }
.legend-dot.is-offline { background: var(--muted-2); }
.xsmall { font-size: 0.72rem; letter-spacing: 0.04em; }
.duplicate-alerts {
margin: 0 32px 16px;
}
.duplicate-alerts .alert-title {
font-family: var(--font-sans);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 700;
color: var(--danger);
margin: 0 0 6px;
padding: 0;
border: 0;
}
.dup-list {
list-style: none;
padding: 0;
margin: 0 0 6px;
display: grid;
gap: 4px;
font-family: var(--font-mono);
font-size: 0.85rem;
}
.dup-list li { display: flex; gap: 12px; align-items: baseline; }
@media (max-width: 900px) {
.duplicate-alerts { margin: 0 18px 12px; }
}
/* ---------- Responsive: mobile student view ---------- */
@media (max-width: 480px) {
body { font-size: 15px; }
.card { padding: 22px 18px; }
.panel { padding: 18px 16px; }
.quiz-card, .celebration-card { padding: 24px 20px; }
.question-text { font-size: 1.4rem; }
.answer-btn .answer-text { font-size: 1.02rem; }
.answer-btn { padding: 16px; min-height: 60px; }
.topbar { padding: 24px 16px 14px; }
.topbar::before { left: 16px; }
.dashboard { padding: 16px; }
.reveal-stats .stat b { font-size: 1.25rem; }
.reveal-stats .stat.big b { font-size: 2rem; }
.big-score { font-size: 3.4rem; }
h2 { font-size: 0.72rem; }
}
@media (max-width: 360px) {
.answer-btn { grid-template-columns: 28px 1fr; gap: 12px; padding: 14px; }
.answer-btn .answer-text { font-size: 0.96rem; }
}
/* ---------- Dark mode: inkwell + brass ---------- */
@media (prefers-color-scheme: dark) {
:root {
--bg: #0c1018;
--surface: #131828;
--surface-2: #181f33;
--border: #2a3145;
--border-strong: #4a526a;
--rule: #404867;
--text: #ece7d6;
--text-soft: #d6d1c0;
--muted: #8d8a82;
--muted-2: #5e6377;
--primary: #c79a4a; /* brass */
--primary-soft: #d8b06a;
--primary-text: #15171c;
--accent: #c79a4a;
--info: #c79a4a;
--warn: #d8a84a;
--warn-text: #15171c;
--danger: #d36556;
--danger-text: #15171c;
--correct-border: #8db96b;
--correct-bg: rgba(141, 185, 107, 0.10);
--wrong-border: #d36556;
--wrong-bg: rgba(211, 101, 86, 0.10);
--medal-gold: #e1bd72;
--medal-silver: #c2c6d3;
--medal-bronze: #d99165;
--shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.4);
--shadow: 0 1px 0 rgba(0, 0, 0, 0.5), 0 16px 38px -22px rgba(0, 0, 0, 0.7);
--shadow-strong: 0 1px 0 rgba(0, 0, 0, 0.6), 0 24px 50px -24px rgba(0, 0, 0, 0.85);
}
body {
background:
radial-gradient(1200px 700px at 100% -10%, color-mix(in srgb, var(--primary) 8%, transparent) 0%, transparent 60%),
radial-gradient(900px 500px at -10% 110%, color-mix(in srgb, var(--primary) 5%, transparent) 0%, transparent 55%),
var(--bg);
}
.qr-wrap { background: #f6f1e2; }
.qr-wrap::before { color: #2c2f38; }
.join-url { background: var(--surface-2); border-color: var(--border); color: var(--text); }
.leaderboard li:nth-child(even) { background: color-mix(in srgb, var(--surface-2) 80%, transparent); }
.btn:hover:not(:disabled), button:hover:not(:disabled) {
background: var(--text);
color: var(--surface);
border-color: var(--text);
}
.btn.primary:hover:not(:disabled) {
background: var(--primary-soft);
border-color: var(--primary-soft);
color: var(--primary-text);
}
.answer-btn:hover:not(:disabled) {
background: var(--text);
color: var(--surface);
border-color: var(--text);
}
.answer-btn.picked {
background: var(--primary);
color: var(--primary-text);
border-color: var(--primary);
}
}
/* Honour reduced-motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}