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.
1210 lines
31 KiB
CSS
1210 lines
31 KiB
CSS
/* ============================================================
|
||
* 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; }
|
||
}
|