Files
quiz/static/projector.css
ameer 168cffea8b feat(options): letterless student/projector UI + text-on-wire submit
Both student-facing surfaces (the per-device join page and the
front-of-room projector) now render options as text only, no A/B/C/D
chips, no numeric prefixes. The letter-namespace remains internal:
canonical A..D in pool.json + submissions storage; canonical position
1..4 in the CSV export. Admin dashboard keeps letters because it is the
instructor's private console.

Why this lands now: the discussion of per-student option shuffling
flagged that A/B/C/D as discussion handles ("the answer is B") is itself
a leaky channel. Removing the labels closes that channel for the
non-shuffled case and keeps it closed if shuffling is added later.

Wire protocol: the submit message carries the option's full text
("answer": "Pipelining"). Server's submit_answer resolves text -> letter
via app.pool.resolve_option_key, which also accepts a canonical letter
so internal callers and tests stay readable. A non-matching string is
recorded as a zero-score submission with answer=NULL, locked in via the
PK + existing_submit_ack short-circuit. So an attempted UI bypass that
posts a fabricated string just produces a wrong answer; no retry.

CSV: A=1 .. D=2 .. C=3 .. D=4 in the answer column. Empty when no option
matched. Header unchanged so downstream pandas readers don't break, but
the value type is int instead of letter.

Histogram: the failsafe "submitted-but-no-match" row buckets into
"missed" alongside genuine misses — both yield zero credit and the
instructor cares about the same thing.

Tests:
- test_submit_accepts_option_text_resolves_to_canonical: production
  wire format produces correct grading + canonical-letter storage.
- test_submit_failsafe_locks_in_zero_score_on_garbage_text: a non-
  matching string is recorded at score=0 and a follow-up correct
  submission cannot overwrite it.

71/71 green.
2026-05-04 17:31:12 +08:00

1210 lines
31 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.
/* ============================================================
* Projector view — front-of-room, single-screen, no-scroll.
*
* Aesthetic: an editorial broadside / front page hung at the
* front of a lecture hall. Shared style.css supplies the type
* system and color tokens; this file lays out the page like a
* printed gazette: ruled sections, folio numerals, marginalia,
* registration crosses, no chrome of any kind.
*
* Hard rules enforced here:
* - 100vh × 100vw, no scroll, at 1366×768 / 1920×1080 / 3440×1440
* - Question prose >= 3vw, options >= 1.5vw, leaderboard >= 1.4vw
* - All color via tokens (light & dark via prefers-color-scheme)
* - Honors prefers-reduced-motion (handled in style.css globally)
* ============================================================ */
/* ---------- Page shell ---------- */
.projector-body {
margin: 0;
padding: 0;
height: 100vh;
height: 100dvh;
width: 100vw;
overflow: hidden;
background:
/* faint diagonal weave that reads as paper grain on a projector */
repeating-linear-gradient(
135deg,
transparent 0 6px,
var(--bg-grain, rgba(31,29,24,0.018)) 6px 7px
),
radial-gradient(1400px 800px at 100% -10%, color-mix(in srgb, var(--primary) 6%, transparent) 0%, transparent 60%),
radial-gradient(1100px 600px at -10% 110%, color-mix(in srgb, var(--primary) 4%, transparent) 0%, transparent 55%),
var(--bg);
}
#projector-app {
height: 100vh;
height: 100dvh;
width: 100vw;
}
.projector-shell {
position: relative;
display: grid;
grid-template-rows: auto 1fr auto;
height: 100vh;
height: 100dvh;
padding: clamp(14px, 1.8vh, 24px) clamp(22px, 2.2vw, 38px) clamp(10px, 1.4vh, 18px);
gap: clamp(10px, 1.4vh, 18px);
box-sizing: border-box;
isolation: isolate;
}
/* Registration crosses in the four corners — the kind printers use for
* plate alignment. Pure decoration, hidden from a/y. */
.projector-shell::before,
.projector-shell::after {
content: "";
position: absolute;
width: 14px;
height: 14px;
pointer-events: none;
background:
linear-gradient(var(--rule), var(--rule)) center / 1px 100% no-repeat,
linear-gradient(var(--rule), var(--rule)) center / 100% 1px no-repeat;
opacity: 0.55;
}
.projector-shell::before { top: 6px; left: 6px; }
.projector-shell::after { bottom: 6px; right: 6px; }
/* The two opposite corners get separate elements so we don't need extra DOM */
.projector-shell > .reg-tr,
.projector-shell > .reg-bl {
position: absolute;
width: 14px;
height: 14px;
pointer-events: none;
background:
linear-gradient(var(--rule), var(--rule)) center / 1px 100% no-repeat,
linear-gradient(var(--rule), var(--rule)) center / 100% 1px no-repeat;
opacity: 0.55;
}
.projector-shell > .reg-tr { top: 6px; right: 6px; }
.projector-shell > .reg-bl { bottom: 6px; left: 6px; }
/* ---------- Topbar (masthead) ---------- */
.projector-topbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: end;
gap: clamp(16px, 2.4vw, 36px);
padding-bottom: clamp(8px, 1.2vh, 14px);
border-bottom: 1.5px solid var(--rule);
position: relative;
}
/* under-rule (double rule like a masthead) */
.projector-topbar::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: -4px;
border-bottom: 1px solid var(--rule);
opacity: 0.45;
}
.topbar-left { display: grid; gap: 4px; justify-items: start; }
.topbar-mid { display: grid; gap: 4px; justify-items: center; text-align: center; }
.topbar-right { display: grid; gap: 4px; justify-items: end; text-align: right; }
.brand {
font-family: var(--font-sans);
font-size: clamp(0.62rem, 0.82vw, 0.78rem);
font-weight: 700;
letter-spacing: 0.34em;
text-transform: uppercase;
color: var(--muted-2);
display: inline-flex;
align-items: center;
gap: 10px;
}
.brand::before,
.brand::after {
content: "";
display: inline-block;
width: clamp(18px, 2vw, 28px);
height: 1px;
background: currentColor;
opacity: 0.55;
}
.topbar-title {
font-family: var(--font-display);
font-size: clamp(1.4rem, 2.2vw, 2.1rem);
font-weight: 600;
letter-spacing: -0.014em;
margin: 0;
color: var(--text);
line-height: 1.05;
font-feature-settings: "ss01";
}
.folio {
font-family: var(--font-mono);
font-size: clamp(0.68rem, 0.95vw, 0.92rem);
font-weight: 500;
font-variant-numeric: tabular-nums;
letter-spacing: 0.08em;
color: var(--muted);
text-transform: uppercase;
}
.folio b {
font-weight: 600;
color: var(--text);
letter-spacing: -0.01em;
}
/* Override the global state-badge sizing for projector scale */
.projector-shell .state-badge {
font-size: clamp(0.7rem, 0.9vw, 0.86rem);
padding: 8px 14px;
letter-spacing: 0.22em;
}
/* ---------- Main grid (one row, varies per state) ---------- */
.projector-grid {
display: grid;
gap: clamp(12px, 1.6vw, 24px);
height: 100%;
min-height: 0;
}
.projector-grid.lobby { grid-template-columns: minmax(380px, 0.9fr) 1.1fr; }
.projector-grid.question { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); }
.projector-grid.between,
.projector-grid.finished { grid-template-columns: 1.05fr 1fr; }
/* ---------- Card primitive ---------- */
.projector-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: var(--shadow);
padding: clamp(16px, 2vw, 28px);
display: grid;
align-content: start;
gap: clamp(8px, 1.2vh, 14px);
min-height: 0;
overflow: hidden;
position: relative;
}
/* Eyebrows / section labels — small caps with rule */
.card-eyebrow,
.lobby-eyebrow {
font-family: var(--font-sans);
font-size: clamp(0.66rem, 0.86vw, 0.8rem);
font-weight: 700;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--muted);
margin: 0;
display: flex;
align-items: center;
gap: 12px;
}
.card-eyebrow::after {
content: "";
flex: 1;
height: 1px;
background: var(--border);
}
/* ============================================================
* STATE: lobby
* ============================================================ */
.join-card {
align-content: start;
justify-items: stretch;
gap: clamp(12px, 1.6vh, 18px);
grid-template-rows: auto 1fr auto;
}
.lobby-headline {
font-family: var(--font-display);
font-size: clamp(1.6rem, 2.4vw, 2.4rem);
font-weight: 600;
font-style: italic;
letter-spacing: -0.012em;
line-height: 1.1;
color: var(--text);
margin: 0;
}
.lobby-sub {
font-family: var(--font-sans);
font-size: clamp(0.85rem, 1.05vw, 1rem);
color: var(--muted);
margin: 0;
letter-spacing: 0.01em;
}
.qr-frame {
position: relative;
display: grid;
place-items: center;
width: 100%;
min-height: 0;
align-self: center;
}
.qr-big {
background: #ffffff;
padding: clamp(12px, 1.4vw, 18px);
border: 1px solid var(--border-strong);
border-radius: 2px;
width: clamp(280px, 28vw, 460px);
aspect-ratio: 1 / 1;
display: grid;
place-items: center;
box-shadow:
0 1px 0 rgba(20, 22, 28, 0.06),
0 24px 56px -28px rgba(20, 22, 28, 0.32);
position: relative;
}
.qr-big img {
width: 100%;
height: 100%;
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
/* corner braces */
.qr-frame::before,
.qr-frame::after {
content: "";
position: absolute;
width: 22px;
height: 22px;
border: 1.5px solid var(--text);
pointer-events: none;
}
.qr-frame::before { top: -10px; left: 50%; transform: translateX(calc(-50% - clamp(140px, 14vw, 230px))); border-right: 0; border-bottom: 0; }
.qr-frame::after { bottom: -10px; left: 50%; transform: translateX(calc(-50% + clamp(140px, 14vw, 230px))); border-left: 0; border-top: 0; }
.lobby-url {
font-family: var(--font-mono);
font-size: clamp(0.95rem, 1.25vw, 1.25rem);
font-weight: 500;
letter-spacing: 0.02em;
color: var(--text);
background: var(--surface-2);
padding: 10px 16px;
border: 1px solid var(--border);
border-radius: 2px;
word-break: break-all;
text-align: center;
font-variant-numeric: tabular-nums;
}
/* lobby-status (right column) */
.lobby-status {
align-content: stretch;
grid-template-rows: auto auto 1fr auto;
gap: clamp(10px, 1.4vh, 16px);
}
.participant-count {
display: grid;
grid-template-columns: auto 1fr;
align-items: end;
gap: clamp(12px, 1.6vw, 22px);
padding: clamp(8px, 1.2vh, 14px) 0;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.participant-count b {
font-family: var(--font-mono);
font-size: clamp(4rem, 8.6vw, 8.4rem);
font-weight: 500;
font-variant-numeric: tabular-nums;
letter-spacing: -0.05em;
line-height: 0.92;
color: var(--text);
display: block;
transform-origin: left bottom;
}
.participant-count.bump b {
animation: count-bump 520ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
@keyframes count-bump {
0% { transform: scale(1); color: var(--text); }
35% { transform: scale(1.08); color: var(--primary); }
100% { transform: scale(1); color: var(--text); }
}
.participant-count .label {
display: grid;
gap: 2px;
padding-bottom: clamp(8px, 1.2vh, 14px);
}
.participant-count .label .word {
font-family: var(--font-display);
font-style: italic;
font-size: clamp(1.1rem, 1.6vw, 1.7rem);
color: var(--text-soft);
line-height: 1.05;
}
.participant-count .label .meta {
font-family: var(--font-sans);
font-size: clamp(0.7rem, 0.9vw, 0.85rem);
color: var(--muted);
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 600;
}
.constellation {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(clamp(11px, 1.1vw, 16px), 1fr));
gap: clamp(5px, 0.6vw, 9px);
align-content: start;
min-height: 0;
overflow: hidden;
align-self: stretch;
padding: 4px 0;
}
.constellation li {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 999px;
background: var(--primary);
opacity: 0.78;
animation: dot-in 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) both;
animation-delay: var(--d, 0ms);
}
@keyframes dot-in {
from { opacity: 0; transform: scale(0.4); }
to { opacity: 0.78; transform: scale(1); }
}
.lobby-rule {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 12px;
font-family: var(--font-sans);
font-size: clamp(0.7rem, 0.92vw, 0.86rem);
letter-spacing: 0.22em;
text-transform: uppercase;
font-weight: 600;
color: var(--muted);
}
.lobby-rule::before,
.lobby-rule::after {
content: "";
display: block;
height: 1px;
background: var(--border);
}
.lobby-meta-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: clamp(8px, 1vw, 14px);
}
.lobby-meta-grid .cell {
display: grid;
gap: 2px;
padding: clamp(8px, 1vh, 12px) clamp(10px, 1vw, 14px);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 2px;
}
.lobby-meta-grid .cell .v {
font-family: var(--font-mono);
font-size: clamp(1.2rem, 1.7vw, 1.7rem);
font-weight: 500;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
color: var(--text);
line-height: 1.0;
}
.lobby-meta-grid .cell .k {
font-family: var(--font-sans);
font-size: clamp(0.62rem, 0.82vw, 0.74rem);
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
/* ============================================================
* STATE: question (open + closed/reveal)
* ============================================================ */
.question-card {
gap: clamp(10px, 1.4vh, 18px);
grid-template-rows: auto 1fr auto;
}
.question-head {
display: grid;
grid-template-columns: 1fr auto;
align-items: start;
gap: clamp(14px, 2vw, 28px);
}
.big-question {
font-family: var(--font-display);
/* Per spec: question prose >= ~3vw — go just above to be safe at 1366×768 */
font-size: clamp(1.7rem, 3vw, 3rem);
font-weight: 500;
line-height: 1.18;
letter-spacing: -0.014em;
margin: 0;
color: var(--text);
font-feature-settings: "ss01";
hyphens: auto;
}
/* Countdown ring — far more legible across a hall than a number alone.
* Uses CSS conic-gradient for the arc; text sits in the middle. */
.countdown-ring {
--size: clamp(90px, 8vw, 140px);
--stroke: clamp(6px, 0.7vw, 10px);
--pct: 100; /* 0..100 */
--hue: var(--primary);
width: var(--size);
height: var(--size);
position: relative;
flex-shrink: 0;
display: grid;
place-items: center;
background:
conic-gradient(var(--hue) calc(var(--pct) * 1%), var(--border) 0);
border-radius: 999px;
}
.countdown-ring::before {
content: "";
position: absolute;
inset: var(--stroke);
background: var(--surface);
border-radius: 999px;
}
.countdown-ring .num {
position: relative;
font-family: var(--font-mono);
font-size: clamp(1.7rem, 2.6vw, 2.6rem);
font-weight: 500;
font-variant-numeric: tabular-nums;
letter-spacing: -0.04em;
color: var(--text);
line-height: 1;
}
.countdown-ring.urgent {
--hue: var(--danger);
animation: ring-urgent 0.9s ease-in-out infinite;
}
.countdown-ring.urgent .num {
color: var(--danger);
}
@keyframes ring-urgent {
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger) 28%, transparent); }
50% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--danger) 0%, transparent); }
}
.countdown-ring.spent { --hue: var(--muted); }
/* Options */
.big-options {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: clamp(7px, 0.95vh, 12px);
align-content: start;
}
.big-options li {
display: grid;
grid-template-columns: clamp(46px, 4.4vw, 72px) 1fr clamp(110px, 14vw, 200px) clamp(74px, 9vw, 110px);
gap: clamp(10px, 1.2vw, 18px);
align-items: center;
padding: clamp(10px, 1.4vh, 16px) clamp(14px, 1.6vw, 22px);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 2px;
position: relative;
transition:
border-color 280ms ease,
background 280ms ease,
opacity 280ms ease,
color 280ms ease;
}
/* Letterless variant — drop the leading key column, give the text more
* room. Histogram bars + counts continue to anchor each row visually. */
.big-options.letterless li {
grid-template-columns: 1fr clamp(110px, 14vw, 200px) clamp(74px, 9vw, 110px);
}
.big-options li::before {
/* tiny "this is a row" tick on the left, like an editorial bullet */
content: "";
position: absolute;
left: -1px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 38%;
background: var(--border);
transition: background 280ms ease, height 280ms ease;
}
.big-options.revealed li.correct {
background: var(--correct-bg);
border-color: var(--correct-border);
border-width: 1.5px;
}
.big-options.revealed li.correct::before {
background: var(--correct-border);
height: 70%;
}
.big-options.revealed li.incorrect {
opacity: 0.45;
}
.big-options.revealed li.incorrect .opt-text {
text-decoration: line-through;
text-decoration-color: var(--muted-2);
text-decoration-thickness: 1px;
}
.opt-key {
font-family: var(--font-display);
font-weight: 600;
font-size: clamp(1.6rem, 2.4vw, 2.3rem);
color: var(--muted);
border-right: 1px solid var(--border);
padding-right: clamp(10px, 1.1vw, 16px);
letter-spacing: -0.01em;
text-align: center;
line-height: 1;
font-feature-settings: "ss01";
}
.big-options li.correct .opt-key { color: var(--correct-border); border-right-color: var(--correct-border); }
.opt-text {
font-family: var(--font-display);
/* Per spec: option text >= ~1.5vw */
font-size: clamp(1.05rem, 1.6vw, 1.6rem);
line-height: 1.28;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.opt-bar {
display: block;
height: clamp(10px, 1.5vh, 16px);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 0;
overflow: hidden;
position: relative;
}
/* tick marks at 25/50/75% — subtle, like graph paper */
.opt-bar::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(to right, transparent 0 calc(25% - 0.5px), var(--border) calc(25% - 0.5px) calc(25% + 0.5px), transparent calc(25% + 0.5px)),
linear-gradient(to right, transparent 0 calc(50% - 0.5px), var(--border) calc(50% - 0.5px) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
linear-gradient(to right, transparent 0 calc(75% - 0.5px), var(--border) calc(75% - 0.5px) calc(75% + 0.5px), transparent calc(75% + 0.5px));
opacity: 0.6;
pointer-events: none;
}
.opt-bar-fill {
display: block;
height: 100%;
background: var(--primary);
width: 0%;
transition: width 600ms cubic-bezier(0.22, 0.61, 0.36, 1), background 280ms ease;
position: relative;
z-index: 1;
}
.big-options li.correct .opt-bar-fill { background: var(--correct-border); }
.big-options.revealed li.incorrect .opt-bar-fill { background: var(--muted-2); }
.opt-count {
font-family: var(--font-mono);
font-size: clamp(1.15rem, 1.55vw, 1.55rem);
font-weight: 500;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
text-align: right;
color: var(--text);
display: grid;
gap: 0;
line-height: 1.05;
}
.opt-count small {
display: block;
font-size: 0.55em;
color: var(--muted);
font-weight: 500;
font-family: var(--font-sans);
letter-spacing: 0.05em;
}
/* Pre-question state: when histogram is empty, hide bars to keep layout clean */
.big-options.pre-vote .opt-bar,
.big-options.pre-vote .opt-count {
visibility: hidden;
}
.big-explanation {
font-family: var(--font-display);
font-style: italic;
font-size: clamp(1rem, 1.35vw, 1.3rem);
line-height: 1.5;
color: var(--text-soft);
border-left: 3px solid var(--correct-border);
padding: 6px 0 6px clamp(10px, 1.2vw, 18px);
margin: 0;
animation: fade-up 600ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* Submission progress strip at the bottom of the question card */
.submission-strip {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: clamp(10px, 1.2vw, 16px);
padding-top: clamp(8px, 1vh, 12px);
border-top: 1px solid var(--border);
}
.submission-strip .label {
font-family: var(--font-sans);
font-size: clamp(0.7rem, 0.9vw, 0.84rem);
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
.submission-strip .track {
height: clamp(6px, 0.8vh, 9px);
background: var(--surface-2);
border: 1px solid var(--border);
position: relative;
overflow: hidden;
}
.submission-strip .track .fill {
position: absolute;
inset: 0 auto 0 0;
width: var(--p, 0%);
background: var(--primary);
transition: width 600ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.submission-strip .nums {
font-family: var(--font-mono);
font-size: clamp(0.95rem, 1.2vw, 1.2rem);
font-weight: 500;
font-variant-numeric: tabular-nums;
color: var(--text);
letter-spacing: -0.01em;
}
.submission-strip .nums small {
font-size: 0.66em;
color: var(--muted);
font-family: var(--font-sans);
letter-spacing: 0.06em;
margin-left: 4px;
}
/* ---------- Side card (response time + top-5) ---------- */
.side-card {
gap: clamp(8px, 1.2vh, 14px);
align-content: start;
grid-template-rows: auto auto auto 1fr;
}
.side-meta {
margin: 0;
text-align: right;
font-family: var(--font-mono);
font-size: clamp(0.78rem, 0.95vw, 0.95rem);
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ============================================================
* Bar charts (response time, score histogram)
* ============================================================ */
.bar-chart {
--baseline: 1px;
display: grid;
grid-template-rows: 1fr auto auto;
align-items: end;
gap: 6px;
padding: 4px 0 0;
height: clamp(140px, 22vh, 240px);
position: relative;
}
.bar-chart.small { height: clamp(110px, 16vh, 180px); }
.bar-chart .bars {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: clamp(4px, 0.6vw, 8px);
align-items: end;
position: relative;
height: 100%;
border-bottom: 1px solid var(--rule);
}
/* Faint horizontal gridlines at 25 / 50 / 75 / 100 % */
.bar-chart .bars::before {
content: "";
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(
to top,
transparent 0 calc(25% - 0.5px),
var(--border) calc(25% - 0.5px) calc(25% + 0.5px),
transparent calc(25% + 0.5px) 25%
);
opacity: 0.4;
pointer-events: none;
}
.bar-chart .bars > * { z-index: 1; position: relative; }
.bar-cell {
display: grid;
grid-template-rows: 1fr;
height: 100%;
align-items: end;
text-align: center;
min-width: 0;
}
.bar-fill {
display: block;
background: var(--primary);
height: var(--h, 2%);
width: 100%;
border-radius: 1px 1px 0 0;
transition: height 600ms cubic-bezier(0.22, 0.61, 0.36, 1);
align-self: end;
position: relative;
}
.bar-fill::after {
/* thin top-cap, like a printed almanac bar */
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: color-mix(in srgb, var(--text) 55%, var(--primary));
opacity: 0.85;
}
.bar-fill[data-empty="true"] { background: transparent; border-top: 1px dashed var(--border); }
.bar-fill[data-empty="true"]::after { display: none; }
.bar-num {
font-family: var(--font-mono);
font-size: clamp(0.7rem, 0.9vw, 0.92rem);
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--text);
line-height: 1;
}
.bar-label {
font-family: var(--font-mono);
font-size: clamp(0.58rem, 0.74vw, 0.74rem);
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0;
line-height: 1;
}
.bar-chart .nums,
.bar-chart .labels {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: clamp(4px, 0.6vw, 8px);
text-align: center;
}
/* ============================================================
* Score-distribution area chart (between + finished)
* ============================================================ */
.area-chart {
position: relative;
width: 100%;
height: clamp(180px, 28vh, 320px);
display: grid;
grid-template-rows: 1fr auto;
gap: 4px;
}
.area-chart svg {
width: 100%;
height: 100%;
display: block;
overflow: visible;
}
.area-chart .grid-line {
stroke: var(--border);
stroke-width: 1;
vector-effect: non-scaling-stroke;
opacity: 0.55;
stroke-dasharray: 2 4;
}
.area-chart .axis {
stroke: var(--rule);
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
.area-chart .area-fill {
fill: var(--primary);
fill-opacity: 0.14;
transition: d 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.area-chart .area-line {
fill: none;
stroke: var(--primary);
stroke-width: 3;
stroke-linejoin: round;
stroke-linecap: round;
transition: d 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
/* tell the renderer not to scale the stroke when SVG is stretched
non-uniformly; the perceived width stays consistent */
vector-effect: non-scaling-stroke;
}
.area-chart .data-point {
fill: var(--surface);
stroke: var(--primary);
stroke-width: 2.5;
vector-effect: non-scaling-stroke;
transition: cx 700ms cubic-bezier(0.22, 0.61, 0.36, 1), cy 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.area-chart .data-label {
font-family: var(--font-mono);
font-size: 18px;
font-weight: 600;
font-variant-numeric: tabular-nums;
fill: var(--text);
text-anchor: middle;
dominant-baseline: hanging;
}
.area-chart .x-tick-label {
font-family: var(--font-mono);
font-size: 16px;
fill: var(--muted);
text-anchor: middle;
}
.area-chart .y-tick-label {
font-family: var(--font-mono);
font-size: 14px;
fill: var(--muted);
text-anchor: end;
dominant-baseline: middle;
}
.area-chart .axis-title {
font-family: var(--font-sans);
font-size: 14px;
font-weight: 600;
fill: var(--muted);
text-transform: uppercase;
letter-spacing: 0.18em;
}
.area-chart .median-line {
stroke: var(--text);
stroke-width: 1.5;
stroke-dasharray: 4 3;
opacity: 0.6;
vector-effect: non-scaling-stroke;
}
.area-chart .median-tag {
font-family: var(--font-mono);
font-size: 16px;
font-weight: 600;
fill: var(--text);
}
.chart-legend {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
font-family: var(--font-sans);
font-size: clamp(0.68rem, 0.86vw, 0.82rem);
color: var(--muted);
letter-spacing: 0.14em;
text-transform: uppercase;
font-weight: 600;
padding-top: 4px;
}
.chart-legend .stat {
font-family: var(--font-mono);
letter-spacing: -0.01em;
text-transform: none;
color: var(--text);
}
.chart-legend .stat b {
font-weight: 600;
}
/* ============================================================
* Big leaderboard
* ============================================================ */
.big-leaderboard {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: clamp(2px, 0.4vh, 6px);
align-content: start;
}
.big-leaderboard li {
display: grid;
grid-template-columns: clamp(40px, 3.4vw, 64px) 1fr auto;
align-items: baseline;
gap: clamp(10px, 1vw, 18px);
padding: clamp(7px, 0.95vh, 11px) clamp(10px, 1vw, 16px);
border-bottom: 1px solid var(--border);
border-left: 3px solid transparent;
position: relative;
animation: row-in 500ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
animation-delay: var(--d, 0ms);
}
.big-leaderboard li:last-child { border-bottom: 0; }
@keyframes row-in {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: translateX(0); }
}
.big-leaderboard .rank {
font-family: var(--font-mono);
font-size: clamp(1.05rem, 1.4vw, 1.45rem);
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--muted);
letter-spacing: -0.02em;
text-align: right;
}
.big-leaderboard .name {
font-family: var(--font-display);
/* Per spec: leaderboard names >= 1.4vw */
font-size: clamp(1.05rem, 1.45vw, 1.55rem);
font-weight: 500;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-feature-settings: "ss01";
}
.big-leaderboard .score {
font-family: var(--font-mono);
font-size: clamp(1.05rem, 1.5vw, 1.5rem);
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--text);
letter-spacing: -0.02em;
}
.big-leaderboard li:nth-child(1) {
background: color-mix(in srgb, var(--medal-gold) 10%, var(--surface));
border-left-color: var(--medal-gold);
}
.big-leaderboard li:nth-child(2) {
background: color-mix(in srgb, var(--medal-silver) 10%, var(--surface));
border-left-color: var(--medal-silver);
}
.big-leaderboard li:nth-child(3) {
background: color-mix(in srgb, var(--medal-bronze) 10%, var(--surface));
border-left-color: var(--medal-bronze);
}
.big-leaderboard li:nth-child(1) .rank,
.big-leaderboard li:nth-child(1) .score { color: var(--medal-gold); }
.big-leaderboard li:nth-child(2) .rank,
.big-leaderboard li:nth-child(2) .score { color: var(--medal-silver); }
.big-leaderboard li:nth-child(3) .rank,
.big-leaderboard li:nth-child(3) .score { color: var(--medal-bronze); }
/* ============================================================
* Finished: hero banner
* ============================================================ */
.finished-banner {
display: grid;
align-content: center;
justify-items: center;
gap: clamp(8px, 1.2vh, 14px);
padding: clamp(16px, 2vh, 28px) clamp(20px, 2vw, 32px);
border: 1px solid var(--border);
background:
linear-gradient(180deg, color-mix(in srgb, var(--primary) 5%, transparent), transparent 60%),
var(--surface);
border-radius: 4px;
position: relative;
}
.finished-banner .kicker {
font-family: var(--font-sans);
font-size: clamp(0.7rem, 0.92vw, 0.86rem);
font-weight: 700;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--primary);
}
.finished-banner h2 {
margin: 0;
font-family: var(--font-display);
font-style: italic;
font-size: clamp(2rem, 3.4vw, 3.4rem);
font-weight: 600;
letter-spacing: -0.018em;
color: var(--text);
line-height: 1.05;
border: 0;
padding: 0;
text-transform: none;
display: block;
}
.finished-banner .summary {
font-family: var(--font-display);
font-size: clamp(1rem, 1.3vw, 1.3rem);
color: var(--text-soft);
font-style: italic;
margin: 0;
text-align: center;
line-height: 1.4;
}
.finished-grid {
grid-template-rows: auto 1fr;
}
/* ============================================================
* Misc: error / empty states
* ============================================================ */
.empty-state {
display: grid;
place-items: center;
height: 100%;
text-align: center;
gap: 8px;
color: var(--muted);
}
.empty-state .glyph {
font-family: var(--font-display);
font-style: italic;
font-size: clamp(1.4rem, 2vw, 2rem);
color: var(--muted-2);
}
.empty-state p {
font-family: var(--font-sans);
font-size: clamp(0.78rem, 1vw, 0.95rem);
letter-spacing: 0.08em;
margin: 0;
}
.fatal-card {
align-content: center;
justify-items: center;
text-align: center;
gap: 12px;
height: 60vh;
align-self: center;
margin: auto;
}
/* ============================================================
* Footer (tickerline)
* ============================================================ */
.projector-foot {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: clamp(10px, 1.4vw, 18px);
padding-top: clamp(6px, 0.8vh, 10px);
border-top: 1px solid var(--rule);
font-family: var(--font-mono);
font-size: clamp(0.66rem, 0.84vw, 0.78rem);
font-variant-numeric: tabular-nums;
letter-spacing: 0.04em;
color: var(--muted);
}
.projector-foot .left,
.projector-foot .right {
display: inline-flex;
align-items: center;
gap: 10px;
}
.projector-foot .dot {
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--correct-border);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--correct-border) 18%, transparent);
}
.projector-foot .dot.dim { background: var(--muted-2); box-shadow: none; }
.projector-foot .center {
text-align: center;
font-family: var(--font-sans);
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 600;
}
/* ============================================================
* Responsive — keep everything visible on 1366×768 and below
* ============================================================ */
@media (max-width: 1280px) {
.projector-grid.lobby { grid-template-columns: 0.95fr 1fr; }
.projector-grid.question { grid-template-columns: 1.5fr 1fr; }
}
@media (max-width: 1100px) {
.projector-grid.question { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
.side-card { display: none; }
.topbar-mid { display: none; }
}
@media (max-aspect-ratio: 5/4) {
/* Boxy aspect: stack lobby vertically */
.projector-grid.lobby { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
.qr-big { width: clamp(220px, 32vw, 380px); }
}
/* ============================================================
* Reduced motion — defer to global rule, but explicitly silence
* the ones we introduce here
* ============================================================ */
@media (prefers-reduced-motion: reduce) {
.countdown-ring.urgent { animation: none; }
.participant-count.bump b { animation: none; }
.constellation li { animation: none; opacity: 0.78; }
.big-leaderboard li { animation: none; }
.big-explanation { animation: none; }
}