A question on screen

How the classyQuiz projector lays out a question, with or without its picture, and how a student's device shows its answers: the markup, and the rules from the site's stylesheet. What the stylesheet doesn't carry is in comments: the type sizes and the picture's size and place, which a script sets from the length of the text and the picture's shape, and what the reveal adds.

Markup

<!-- The projector, one question. Always the light theme: projectors wash out dark backgrounds. -->
<body class="screen"><main>
  <div class="bar" id="qbar"><span id="qpos">2 / 12</span><span class="strip" id="strip"><!-- an avatar per answer that has arrived --></span><span id="qicons"></span><span class="qcount">👤 <span>17 / 24</span></span></div>
  <div class="stage">
    <h2 class="qtext">A jacket costs 80 francs and is reduced by 25%. What does it cost now?</h2>
    <div class="tiles">
      <div class="tile a0"><svg class="shape" viewBox="0 0 100 100"><path d="M50 12 L90 84 H10Z"/></svg><span class="txt">55 francs</span><span class="side"></span></div>
      <div class="tile a1"><svg class="shape" viewBox="0 0 100 100"><path d="M50 8 L92 50 L50 92 L8 50Z"/></svg><span class="txt">20 francs</span><span class="side"></span></div>
      <div class="tile a2"><svg class="shape" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40"/></svg><span class="txt">75 francs</span><span class="side"></span></div>
      <div class="tile a3"><svg class="shape" viewBox="0 0 100 100"><rect x="13" y="13" width="74" height="74" rx="4"/></svg><span class="txt">60 francs</span><span class="side"></span></div>
    </div>
  </div>
</main></body>
<!-- The bar has a fixed height (1.9em). While the class answers, the avatar of each student whose answer has
     arrived pops into .strip, sized so the whole class fits, in one row or more.
     At the votes: every tile's .side becomes <span class="n">17</span> (how many picked it), and each tile gets
     <span class="fill" style="width: 100 × its count ÷ the highest count %"></span>, growing from 0.
     At the reveal, the answer: a tile that doesn't count gets the class "dim", and a tile that counts gets
     <span class="mark">✓</span> before its count. The tiles are marked in place: nothing else is redrawn, so
     neither text nor picture moves. -->

<!-- With a countdown (the quiz's Tempo: line, or the teacher's choice in the lobby), a question first comes up for reading: the same
     layout, the tiles with the class "veiled" (their text hidden, colour and shape faded), laid out for their
     answers so nothing moves when the teacher presses Go. Then the text shows and a bar drains along the top edge
     of the screen, over everything and outside the layout:
     <div id="clock"><div class="clockbar wall"><span></span></div></div>. -->

<!-- A question with a picture (its Image: line). Between the question and the tiles: -->
  <div class="stage fig between">
    <h2 class="qtext">Welche Zahl ist mit dem Pfeil markiert?</h2>
    <div class="figure" style="width: 1298px; height: 214px"><img src="…" width="2000" height="330" alt=""></div>
    <div class="tiles"> … the tiles as above … </div>
  </div>
<!-- Or beside them, the picture on the left: -->
  <div class="stage fig beside" style="grid-template-columns: auto minmax(0, 1fr)">
    <div class="figure" style="width: 606px; height: 606px"><img …></div>
    <div class="col"><h2 class="qtext">…</h2><div class="tiles"> … one column … </div></div>
  </div>
<!-- Which one, and the figure's size in px, the script works out from the picture's shape and the room the
     question leaves (sizes for a 1366×768 screen above). It lays out "between": the question first, at most
     30% of the height; the tiles keep at least 13.5% of the height per row for answers up to 12 characters,
     16% up to 30, 20% beyond; the picture gets the rest, at most 62% of the height, never wider than the screen.
     It then takes "beside" if that shows the picture at least 1.3 times larger, with the picture at 58% (else 50%)
     of the width, and only if the question stays at 1.5em or more and no answer drops below 1.3em in the
     narrower column. So wide figures such as number lines go between, square and upright ones and photos
     beside, and a long question keeps its picture between. A picture smaller than its box is enlarged.

     Z, or a click on the picture, enlarges it over the whole screen until the next step:
       <div id="zoom"><div class="figure" style="(as large as fits in 96% × 94% of the screen)"><img …></div></div>

     With a background (a Background: line), the body gets the class "has-backdrop" and, first in it:
       <div id="backdrop"><img …></div>
     The question and the bar then sit on white cards. -->

<!-- A student's phone or tablet: the answers as buttons, no picture; the question is on the projector. -->
<body class="play"><main>
  <div class="buttons n4">
    <button class="ansbtn a0"><svg class="shape" viewBox="0 0 100 100"><path d="M50 12 L90 84 H10Z"/></svg><span>55 francs</span></button>
    <button class="ansbtn a1"><svg class="shape" viewBox="0 0 100 100"><path d="M50 8 L92 50 L50 92 L8 50Z"/></svg><span>20 francs</span></button>
    <button class="ansbtn a2"><svg class="shape" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40"/></svg><span>75 francs</span></button>
    <button class="ansbtn a3"><svg class="shape" viewBox="0 0 100 100"><rect x="13" y="13" width="74" height="74" rx="4"/></svg><span>60 francs</span></button>
  </div>
</main></body>
<!-- .a0 to .a3 are places on screen, not places in the quiz text: the site shuffles each question's answers at
     upload (and again for another class), the same on the projector, every tablet and the teacher's screens.
     A question with the line "Order: fixed" keeps the text's order. -->

Stylesheet

:root { --bg: #fbf8f2; --fg: #1d1d1b; --muted: #5d5c57; --good: #1f8a4c; --bad: #c62828; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 17px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.a0 { --ans: #d93b3b; --ans-ink: #fff; }      /* 1st answer: red triangle   */
.a1 { --ans: #2f6fd6; --ans-ink: #fff; }      /* 2nd: blue diamond          */
.a2 { --ans: #e8b100; --ans-ink: #1b1b1f; }   /* 3rd: yellow circle         */
.a3 { --ans: #1f9d55; --ans-ink: #fff; }      /* 4th: green square          */
.shape { width: 1em; height: 1em; display: inline-block; flex: none; fill: currentColor; }

/* Projector */
body.screen { overflow: hidden; height: 100dvh; font-size: clamp(16px, 1.6vw, 40px); }
.screen main { height: 100dvh; display: flex; flex-direction: column; padding: 2.2vh 2.5vw; gap: 2vh; }
.screen .bar { display: flex; align-items: center; gap: 1.2em; font-weight: 700; color: var(--muted); font-size: 1.2em; }
.screen .bar .grow { flex: 1; }
.stage { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 2vh; overflow: hidden; }
.qtext { font-weight: 700; text-align: center; line-height: 1.2; margin: 0; overflow-wrap: anywhere; display: flex; align-items: center; justify-content: center; flex: 0 1 auto; min-height: 18vh; }
/* .qtext font-size is set by script from the question's length: up to 40 characters 3.6em, up to 90 3em,
   up to 160 2.4em, longer 1.9em; then reduced in 0.15em steps until the question fits in 34vh, down to 1.1em.
   With a picture: 3em, 2.5em, 2.1em, 1.8em, fitting in 30% of the stage's height (42% beside). */
.tiles { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 1.6vh; min-height: 0; }
/* .tiles font-size is set by script from the longest answer: up to 12 characters 2.6em, up to 30 2em,
   up to 60 1.55em, longer 1.25em; then reduced in 0.1em steps until no tile overflows, down to 1em;
   if it still overflows, words are broken. A number written in groups ("73 000") stays on one line. */
.tile { background: var(--ans); color: var(--ans-ink); border-radius: 18px; display: flex; align-items: center; gap: .6em; padding: 1.2vh 1.6vw; font-weight: 700; position: relative; overflow: hidden; min-height: 0; }
.tile .shape { width: 1.6em; height: 1.6em; }
.tile .txt { flex: 1; min-width: 0; line-height: 1.15; }
.tile .side { flex: none; width: 3.4em; display: flex; align-items: center; justify-content: flex-end; gap: .2em; }
.tile .n { font-size: 1.3em; font-weight: 800; font-variant-numeric: tabular-nums; }
.tile .fill { position: absolute; left: 0; bottom: 0; height: 1.6vh; background: currentColor; opacity: .7; transition: width .6s; }
.tile.dim { opacity: .5; }
.tiles.veiled .txt { visibility: hidden; }
.tiles.veiled .tile { opacity: .55; }
#clock { position: fixed; top: 0; left: 0; right: 0; z-index: 6; pointer-events: none; }
.clockbar.wall { height: max(10px, 1.7vh); overflow: hidden; background: rgba(29, 29, 27, .12); }
.clockbar > span { display: block; height: 100%; background: #2a5db0; transform-origin: left center; } /* drains to scaleX(0) over the countdown, turning orange in its last fifth */

/* A question's picture: always on white, so a transparent drawing reads on any background. */
.stage.fig .qtext { min-height: 0; flex: none; }
.figure { flex: none; align-self: center; background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 6px rgba(0, 0, 0, .14); }
.figure img { display: block; width: 100%; height: 100%; object-fit: contain; }
.stage.beside { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; column-gap: 2.5vw; }
.stage.beside .col { height: 100%; min-height: 0; display: flex; flex-direction: column; gap: 2vh; }
.stage.beside .tiles { grid-template-columns: minmax(0, 1fr); }
#zoom { position: fixed; inset: 0; z-index: 5; background: var(--bg); display: flex; align-items: center; justify-content: center; }

/* A background: behind everything, under a light veil; the text on white cards. */
#backdrop { position: fixed; inset: 0; z-index: -1; overflow: hidden; }
#backdrop img { width: 100%; height: 100%; object-fit: cover; display: block; }
#backdrop::after { content: ""; position: absolute; inset: 0; background: rgba(251, 248, 242, .55); }
.has-backdrop .qtext { align-self: center; max-width: 100%; padding: .25em .7em; background: rgba(255, 255, 255, .9); border-radius: 18px; }
body.screen.has-backdrop .bar { background: rgba(255, 255, 255, .85); border-radius: 12px; padding: .2em .6em; }

/* Student */
.play main { min-height: 100dvh; display: flex; flex-direction: column; padding: 12px; gap: 12px; }
.play .buttons { flex: 1; display: grid; gap: 12px; grid-template-columns: 1fr 1fr; grid-auto-rows: 1fr; }
.play .buttons.n2 { grid-template-columns: 1fr; }
.ansbtn { background: var(--ans); color: var(--ans-ink); border: 0; border-radius: 18px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 10px; font-size: clamp(1.3rem, 4.5vmin, 2.6rem); font-weight: 700; min-height: 90px; overflow-wrap: anywhere; }
.ansbtn .shape { width: clamp(52px, 12vmin, 140px); height: clamp(52px, 12vmin, 140px); }