/* ============================================================================
   PixelSchoolWork — application composites

   Everything below names a thing from the PRODUCT (the account-type picker, the
   auth card), not from the system. Rule of thumb: a class that would still make
   sense in a different product belongs in design-system.css instead. Every value
   comes from a token — no literal colour, no literal spacing.

   MOBILE FIRST: the base rules are the phone rules and the media queries only
   ever ADD room. Nothing below hides content on a small screen.
   ========================================================================= */


/* ---------- Page heading row ----------
   Title on the left, primary action on the right. */

.page-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.page-header > h1 { margin: 0; }


/* ---------- Auth card ----------
   Sign in, register, licence. On a phone the card runs edge to edge with no
   border and no shadow: a floating panel inside a 360px viewport is 16px of
   wasted width on each side and a frame around something that has no siblings
   to be framed against. From the first breakpoint up it becomes a card again. */

.auth-card {
  margin-inline: calc(var(--space-4) * -1);
  border-inline: none;
  border-radius: 0;
  box-shadow: none;
}

@media (min-width: 480px) {
  .auth-card {
    margin-inline: 0;
    border-inline: var(--border-width) solid var(--c-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
  }
}


/* ---------- Account-type picker ----------
   Three targets, one per account type. Buttons rather than a radio group: the
   choice is the navigation — picking one moves to the next step — and a
   44px-tall row with its own explanation is what a thumb can hit.

   Kept here and not in the design system because "a big tappable row that
   explains itself" is how THIS product asks a question; a second product would
   want a different shape. */

.choice-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  /* Comfortably over the 44px minimum tap target, on purpose. */
  min-height: 64px;
  margin-bottom: var(--space-3);
  padding: var(--space-3) var(--space-4);
  font: inherit;
  text-align: left;
  color: var(--c-text);
  background: var(--c-surface);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
}

.choice-card:hover {
  background: var(--c-surface-hover);
  border-color: var(--c-border-strong);
}

.choice-card:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

/* min-width: 0 is what lets a long hint wrap instead of stretching the row. */
.choice-card-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.choice-card-title {
  font-weight: var(--weight-bold);
  font-size: var(--text-md);
}

.choice-card-hint {
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

/* The leading icon carries the type; the trailing chevron says "this goes
   somewhere". Neither should shrink when the hint wraps. */
.choice-card > .icon { flex: none; }
.choice-card > .icon:last-child { color: var(--c-text-muted); }


/* ---------- Assignment code entry ----------
   The field takes over the page while it is being filled in: everything marked
   data-blurable recedes, the field grows and lifts, and the password slides out
   from under it. A code is read off a board and typed once — nothing else
   should be competing for the eye.

   The blur is on the siblings rather than a backdrop-filter on an overlay:
   backdrop-filter is the expensive one on the mid-range phones these students
   actually hold, and it needs a vendor prefix to reach Safari. This does not. */

[data-blurable] {
  transition: filter var(--transition), opacity var(--transition);
}

.code-focused [data-blurable] {
  filter: blur(3px);
  opacity: 0.5;
  /* Nothing behind the dimming should be clickable: a tap out there dismisses,
     it does not follow a link the user cannot read. */
  pointer-events: none;
}

/* Catches the dismissing click. Below the code entry, above everything else,
   and deliberately not painted: the blur already says the page is set aside. */
.code-scrim {
  position: fixed;
  inset: 0;
  z-index: 1;
}

.code-entry {
  position: relative;
  /* Above the scrim, so it stays sharp and stays clickable. */
  z-index: 2;
}

.code-entry-title { margin-bottom: var(--space-4); }

.code-entry-label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
}

.code-input {
  width: 100%;
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  letter-spacing: 0.2em;
  text-align: center;
  /* transform-origin at the top: the field grows downwards, towards the
     password about to appear, instead of pushing into the header. */
  transform-origin: top center;
  transition: transform var(--transition), box-shadow var(--transition);
}

.code-entry.is-active .code-input {
  transform: scale(1.04);
  box-shadow: var(--shadow-lg);
}

/* The password field's own reveal. grid-template-rows 0fr → 1fr animates to
   the content's real height, which a guessed max-height cannot do without
   either clipping the content or overshooting and easing through empty space. */
.code-reveal {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--transition), opacity var(--transition);
}

.code-entry.is-active .code-reveal {
  grid-template-rows: 1fr;
  opacity: 1;
}

/* The row of a 0fr grid still shows its overflow; this is what actually hides
   the collapsed content. */
.code-reveal-inner { overflow: hidden; }

/* Motion is the whole effect here, so honouring the preference means removing
   the movement and keeping the meaning: the page still recedes, the password
   still appears, neither of them slides. */
@media (prefers-reduced-motion: reduce) {
  [data-blurable],
  .code-input,
  .code-reveal {
    transition: none;
  }

  .code-entry.is-active .code-input { transform: none; }
}


/* The student's home is a phone screen, even on a desktop. */
.home-column { max-width: 640px; margin-inline: auto; }


/* ---------- Assignment history ----------
   One line per assignment answered: what it was, when, and how it went. Used
   by the home page and by /assignments, which is why the list is a component
   and not two blocks of near-identical CSS. */

.submission-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The row is a card; the disclosure button is its whole top line, and the
   attempts unfold underneath. */
.submission-item {
  padding: var(--space-2) var(--space-4);
  background: var(--c-surface);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-3);
}

/* A button that has to look like a row: the browser's own button styling is
   removed rather than worked around. */
.submission-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) 0;
  font: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
}

.submission-toggle:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

/* Points down when closed, up when open — the one thing telling a reader the
   row has more behind it. */
.submission-chevron {
  flex: none;
  color: var(--c-text-muted);
  transition: transform var(--transition);
}

.submission-toggle[aria-expanded="true"] .submission-chevron { transform: rotate(180deg); }

/* How many turns are left, and the way to use one. */
.submission-footer {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-1) 0 var(--space-2);
}

.attempt-list {
  margin: 0;
  padding: var(--space-2) 0 var(--space-1);
  list-style: none;
  border-top: var(--border-width) solid var(--c-border);
}

.attempt-item {
  display: flex;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-2) 0;
  font-size: var(--text-sm);
}

.attempt-index { font-weight: var(--weight-medium); }

/* The game's picture-word: a tinted tile, so a row is recognised before it is
   read. */
.submission-icon {
  flex: none;
  width: 48px;
  height: 48px;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--c-primary-subtle);
  color: var(--c-primary-text);
}

/* min-width: 0 is what lets .truncate work on a flex child. */
.submission-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.submission-title { font-size: var(--text-md); font-weight: var(--weight-medium); }

/* Score above XP on a phone, side by side as soon as there is room. */
.submission-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
  flex: none;
}

@media (min-width: 480px) {
  .submission-meta {
    flex-direction: row;
    align-items: center;
    gap: var(--space-3);
  }
}


/* ---------- Paginator ----------
   Previous, where you are, next. Numbered links are not worth their width on a
   phone, and a history is read from the top down anyway — the design system's
   .pagination stays for the day a screen genuinely needs to jump to page 7. */

.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
}


/* ---------- Profile ---------- */

/* Picture on the left, its controls beside it — and stacked below the picture
   when the buttons would otherwise be squeezed against it. */
.avatar-editor {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-4);
}

.avatar-editor .avatar { flex: none; }

/* The XP bar. <progress> is one of the few elements whose default rendering is
   entirely browser-specific, so all three engines are addressed explicitly —
   without this the bar is grey in Chrome and green in Firefox. */
.xp-bar {
  --bar-fill: var(--c-xp);

  display: block;
  width: 100%;
  height: 14px;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--c-xp-track);
  /* Firefox reads the bar's own colour from these two. */
  accent-color: var(--bar-fill);
  color: var(--bar-fill);
}

/* The same bar, counting questions rather than XP: blue, the colour of
   moving forward, so it is never mistaken for a reward. */
.xp-bar.quiz-progress { --bar-fill: var(--c-primary); height: 10px; }

/* Chrome and Safari: the track. */
.xp-bar::-webkit-progress-bar {
  background: var(--c-xp-track);
  border-radius: var(--radius-pill);
}

/* Chrome and Safari: the filled part. */
.xp-bar::-webkit-progress-value {
  background: var(--bar-fill);
  border-radius: var(--radius-pill);
  transition: width var(--transition);
}

/* Firefox: the filled part. */
.xp-bar::-moz-progress-bar {
  background: var(--bar-fill);
  border-radius: var(--radius-pill);
}

/* ---------- Level card ----------
   The student's level on the home page, the whole card a link to the
   profile: level, bar, and how far the next one is. */

.level-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  color: inherit;
  text-decoration: none;
}

.level-card:hover { border-color: var(--c-border-strong); text-decoration: none; }

.level-card-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}


/* ---------- The teacher's games ----------
   One row per game: its name, and the three things that decide whether to open
   it — is it live, is it open, when was it last touched. */

.game-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.game-item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--c-surface);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-3);
  transition: border-color var(--transition), box-shadow var(--transition);
}

.game-item:hover {
  border-color: var(--c-border-strong);
  box-shadow: var(--shadow-sm);
}

/* Stretched link: one real link in the markup — so its text is the row's
   accessible name — with a transparent overlay making the whole row clickable. */
.game-open {
  color: inherit;
  text-decoration: none;
}

.game-open::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-lg);
}

.game-open:hover { text-decoration: none; }

/* The ring belongs around the row, not around the two words of the title. */
.game-open:focus-visible { outline: none; }

.game-item:has(.game-open:focus-visible) {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.game-title {
  display: block;
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
}

.game-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}


/* ---------- Access codes ----------
   The code is the point of the row, so it is set in the monospace face at a
   size that survives being read from the back of a classroom. */

/* ---------- Editor layout ----------
   One column per phone, three per desktop. Mobile first, so the stacked order
   IS the source order and nothing has to be reordered for the narrow case. */

.editor-grid {
  display: grid;
  gap: var(--space-5);
  align-items: start;
}

.editor-column {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  /* Without this a long question title stretches its column instead of
     wrapping, and the three widths stop meaning anything. */
  min-width: 0;
}

/* 1100px and not a token breakpoint: it is the width at which three columns
   stop being three cramped ones. Questions get half again as much room as the
   other two — it is the only column whose content is read rather than set. */
@media (min-width: 1100px) {
  .editor-grid { grid-template-columns: 1fr 1.5fr 1fr; }
}

.code-list {
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
}

/* ---------- The format picker ---------- */

/* A button wearing a select's clothes. It borrows .select for the border, the
   height and the focus ring, and only fixes what a <button> does differently:
   it centres its text and shrinks to it. The chevron says it opens something,
   which is the one thing it has in common with a real select. */
.select-button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  text-align: left;
  cursor: pointer;
}

.select-button::after {
  content: '';
  flex: none;
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--c-text-muted);
}

/* auto-fill and not auto-fit: with a single format in the catalogue, auto-fit
   would stretch its tile across the whole dialog. */
.game-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--space-3);
}

.game-tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}

.game-tile:hover {
  background: var(--c-surface-hover);
  border-color: var(--c-text-muted);
}

.game-tile:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: 2px;
}

/* A fixed ratio, so the grid does not reflow as each illustration arrives and
   so a picture drawn to the wrong size cannot break the row. */
.game-tile-image,
.game-details-image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunken);
}

.game-tile-title { font-weight: var(--weight-bold); }

.game-details {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.game-details-back { align-self: flex-start; }

.game-details-title { margin: 0; }

.game-details-description { margin: 0; color: var(--c-text-muted); }

/* Bottom right: where the eye lands last on a page it has to decide about. */
.game-details-actions { display: flex; justify-content: flex-end; }

/* ---------- Results, in the editor's third column ---------- */

.results-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A whole row is the target: it opens the page for that code. A <button> and
   not a <div> with a handler, so it is reachable by keyboard and announced as
   what it is. */
.results-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-2);
  border: none;
  border-bottom: var(--border-width) solid var(--c-border);
  border-radius: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.results-item:last-child { border-bottom: none; }
.results-item:hover { background: var(--c-surface-hover); }

.results-item:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: -2px;
}

.results-item-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}


/* ---------- Question recap ----------
   Under the results table: each question, a bar, and the share of the class
   that got it right. The figure is always printed: the bar's colour only
   repeats it. */

.recap-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.recap-item {
  --recap-fill: var(--c-success);

  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "index prompt prompt"
    ".     bar    value";
  align-items: center;
  gap: var(--space-2) var(--space-3);
}

.recap-item.is-mid { --recap-fill: var(--c-warning); }
.recap-item.is-low { --recap-fill: var(--c-danger); }

.recap-item > .question-index { grid-area: index; }
.recap-prompt { grid-area: prompt; overflow-wrap: anywhere; }

.recap-bar {
  grid-area: bar;
  display: block;
  height: 12px;
  border-radius: var(--radius-pill);
  background: var(--c-xp-track);
  overflow: hidden;
}

.recap-fill { display: block; height: 100%; background: var(--recap-fill); }

.recap-value {
  grid-area: value;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The bold line under a results table: the class as a whole, not one student. */
.table tfoot td { font-weight: var(--weight-bold); }

.code-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: var(--border-width) solid var(--c-border);
}

.code-item:last-child { border-bottom: none; }

/* A revoked code is dimmed, not hidden: it still explains where existing
   attempts came from, and the teacher may want it back. */
.code-item.revoked { opacity: 0.55; }
.code-item.revoked:hover { opacity: 1; }

.code-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.code-value {
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  letter-spacing: 0.08em;
}

.code-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}


/* ---------- Full-screen access code ----------
   The projector's content while a class types. Sized in vw so it fills
   whatever it is thrown onto rather than assuming a laptop. */

.code-display {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-5);
  padding: var(--space-6);
  background: var(--c-surface);
  overflow: auto;
}

/* The page underneath must not scroll behind the overlay. */
.display-open { overflow: hidden; }

.code-display-close {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
}

.code-display-label {
  margin: 0;
  font-size: var(--text-xl);
  color: var(--c-text-muted);
}

.code-display-pair {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}

.code-display-caption {
  font-size: var(--text-md);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--c-text-muted);
}

/* clamp, not a fixed size: this is read from two metres away on a projector
   and from arm's length on a laptop, and both are the same screen. */
.code-display-value {
  font-family: var(--font-mono);
  font-size: clamp(2.5rem, 14vw, 9rem);
  font-weight: var(--weight-bold);
  line-height: 1;
  letter-spacing: 0.08em;
}

.code-display-qr {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

/* On a white plate whatever the theme: a QR code is read by a camera, and the
   contrast it needs has nothing to do with the contrast a person needs.
   Inverting it in a dark theme is how it stops scanning. */
/* Where the QR sits when it has not been asked for yet: the button takes the
   slot the image will take, so nothing jumps when it arrives. */
.code-display-qr-slot {
  display: flex;
  justify-content: center;
  margin: 0;
}

.code-display-qr img {
  width: clamp(140px, 30vh, 320px);
  height: auto;
  padding: var(--space-2);
  background: #ffffff;
  border-radius: var(--radius-sm);
}

.code-display-qr figcaption {
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}


/* ---------- Question editor ---------- */

.question-list {
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
}

.question-item {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: var(--border-width) solid var(--c-border);
}

.question-item:last-child { border-bottom: none; }

/* The number is the question's position, and it has to stay put while a long
   prompt wraps beside it. */
.question-index {
  flex: none;
  width: 24px;
  text-align: center;
  font-weight: var(--weight-bold);
  color: var(--c-text-muted);
}

.question-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

/* Wrapped, not truncated: a question the teacher cannot read in the list is a
   question they have to open to identify. */
.question-prompt { overflow-wrap: anywhere; }

.question-actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}

/* One editable answer: its text, its correctness, and a way to drop it. */
.option-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.option-row > .input { flex: 1; min-width: 0; }

.option-correct { margin: 0; flex: none; white-space: nowrap; }

.option-list { margin-bottom: var(--space-2); }

/* The answer fields of one question type. A <fieldset> so that disabling it
   also bars its contents from the browser's constraint validation; it carries
   none of a fieldset's default chrome. */
.answer-section {
  border: none;
  padding: 0;
  margin: 0;
  min-width: 0;
}

/* A field and the (i) that explains it, on one line. */
.answer-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.answer-row > .input { flex: 1; min-width: 0; }


/* A question this game's format has no way of drawing. On the row rather than
   only on the publish button: this is where it gets fixed. */
.question-unplayable {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--c-warning-text);
}

.question-unplayable > .icon { flex: none; }


/* ---------- Playing ----------
   One question filling the screen. Nothing else is on it: no header, no menu,
   no navigation — a student answering is doing one thing. */

.play-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-4);
}

/* The way out on the left, the clock on the right, the game's name on its own
   line under them: a phone has no room for the three on one line, and a name
   cut to "Les fractio…" is no name. Back on one line from a tablet up. */
.play-header > h1 {
  order: 1;
  flex: 1 0 100%;
  margin: 0;
  font-size: var(--text-lg);
}

@media (min-width: 640px) {
  .play-header > h1 { order: 0; flex: 1 1 0; min-width: 0; text-align: center; }
}

/* The quit link is a quiet button: its left edge lines up with the page. */
.play-header > .btn:first-child { margin-left: calc(var(--space-3) * -1); }

/* The countdown. Monospaced so the digits do not shuffle the header sideways
   every second, and red only in the last minute — a timer that shouts from the
   start is one students stop reading. */
.play-clock {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 36px;
  padding: 0 var(--space-3);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  font-variant-numeric: tabular-nums;
  font-weight: var(--weight-medium);
  color: var(--c-text);
}

.play-clock.is-urgent { color: var(--c-danger-text); border-color: var(--c-danger-text); }

/* The question gets its own card: it is what the screen is for. */
.question-card { padding-block: var(--space-6); }

.question-prompt-large {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.question-card .hint { margin: 0; }

/* One answer. The whole row is the target, not a radio button inside it:
   this is answered with a thumb. The native input is still there — kept for
   the keyboard, the screen reader and the form — but drawn by the letter
   tile, which turns blue and gains a tick when the answer is chosen. Chosen is
   said by the fill, the border AND the tick, never by colour alone. */
.play-option {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 64px;
  margin-bottom: var(--space-3);
  padding: var(--space-2) var(--space-4);
  background: var(--c-surface);
  border: var(--control-border-width) solid var(--c-border);
  border-radius: var(--radius-md);
  font-size: var(--text-lg);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition);
}

.play-option:hover { border-color: var(--c-border-strong); }

/* :has, so the ring goes around the row the way the click does. */
.play-option:has(input:focus-visible) {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.play-option input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.play-option-letter {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  background: var(--c-surface-hover);
  color: var(--c-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
}

/* Several answers allowed: the tile is round-cornered less, like a box. */
.play-option:has(input[type="checkbox"]) .play-option-letter { border-radius: 6px; }

.play-option-label { flex: 1; min-width: 0; overflow-wrap: anywhere; }

.play-option-tick { flex: none; visibility: hidden; color: var(--c-primary-text); }

.play-option:has(input:checked) {
  border-color: var(--c-primary);
  background: var(--c-primary-subtle);
  font-weight: var(--weight-medium);
}

.play-option:has(input:checked) .play-option-letter {
  background: var(--c-primary);
  color: var(--c-primary-on);
}

.play-option:has(input:checked) .play-option-tick { visibility: visible; }

/* ---------- Result ---------- */

.result-hero { margin-bottom: var(--space-4); }

.result-hero-game { font-size: var(--text-sm); }

.result-hero h1 { margin-top: var(--space-1); font-size: var(--text-2xl); }

.result-card > p { margin-bottom: var(--space-2); }

.result-score {
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  line-height: 1.1;
}

.result-progress {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.result-xp {
  align-self: flex-start;
  margin: 0;
  min-height: 36px;
  font-size: var(--text-lg);
}

.result-level-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.result-level-row > .xp-bar { flex: 1; }

.result-level { margin: 0; font-weight: var(--weight-bold); white-space: nowrap; }


/* ---------- Modal refusal ----------
   Where a dialog shows why it refused to close. Pulled to the top of the body
   so the message is the first thing read, and the form under it is untouched —
   the point of staying open is that nothing typed is lost. */

.modal-alert { margin-bottom: var(--space-4); }


/* ---------- Nested setting ----------
   A checkbox that only means something while its parent is ticked. Indented
   and ruled so the dependency is visible rather than remembered. */

.nested-option {
  margin: var(--space-2) 0 0 var(--space-5);
  padding-left: var(--space-3);
  border-left: 2px solid var(--c-border);
}


/* ---------- End-of-game correction ----------
   What was asked, what the student ticked, what was expected, and why.
   Meaning is never carried by colour alone: every option also has its own
   icon, because this is precisely the screen a colour-blind student reads to
   find out what they got wrong. */

.review-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.review-item {
  padding: var(--card-padding);
  margin-bottom: var(--space-3);
  background: var(--c-surface);
  border: var(--control-border-width) solid var(--c-border);
  border-radius: var(--radius-lg);
}

/* The verdict's colour on the whole card, so a list of twenty is read at a
   glance; the badge in the head says it in words. */
.review-item.is-right   { border-color: var(--c-success-text); }
.review-item.is-partial { border-color: var(--c-warning-text); }
.review-item.is-wrong,
.review-item.is-blank   { border-color: var(--c-danger-text); }

.review-head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
}

/* The verdict first, on its own line; the number and the question under it,
   with the whole width to wrap in. */
.review-head { flex-wrap: wrap; }
.review-head .badge { flex: none; order: -1; margin-right: 100%; }

.review-options {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Every answer is a row in a box, like the answers were when they were
   asked; the right one and the wrong pick are boxed in their colour, with
   their icon and their caption. */
.review-option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 52px;
  padding: var(--space-2) var(--space-3);
  border: var(--control-border-width) solid var(--c-border);
  border-radius: var(--radius-md);
}

/* A right answer, whether or not it was found: this is what should have been
   ticked, and it is the first thing to read. */
.review-option.is-correct {
  border-color: var(--c-success-text);
  background: var(--c-success-subtle);
  font-weight: var(--weight-medium);
}

.review-option.is-correct > .icon { color: var(--c-success-text); }

.review-option.is-wrong {
  border-color: var(--c-danger-text);
  background: var(--c-danger-subtle);
}

.review-option.is-wrong > .icon { color: var(--c-danger-text); }

.review-option > .text-xs { font-size: var(--text-sm); font-weight: var(--weight-medium); }
.review-option.is-correct > .text-xs { color: var(--c-success-text); }
.review-option.is-wrong > .text-xs { color: var(--c-danger-text); }

/* An option that is neither right nor picked still needs its icon slot, or the
   labels of a list would not line up. */
.review-option-blank { opacity: 0; }

.review-explanation {
  margin: var(--space-3) 0 0;
  padding: var(--space-3) var(--space-4);
  line-height: 1.5;
  background: var(--c-bg);
  border-radius: var(--radius-md);
}


/* ---------- A scene ----------
   The shell every format played on a canvas wears, and none of them owns: the
   maze walks in it, the alpine race skis down it, and the next one will not
   have to bring its own banner and its own dialogue box.

   A screen, not a panel. The canvas takes everything and every control is laid
   over it: what makes these formats worth having is that nothing around them
   looks like a form. The scene carries aria-hidden — everything a student has
   to read and press is in the box, so a scene is answerable with a keyboard and
   a screen reader exactly as the quiz is.

   What a FORMAT decides for itself is the colour behind it, and that is the
   whole of what is keyed on the format below. */

/* Two pixel typefaces, served from static/fonts, both SIL Open Font License.
   Local copies rather than a CDN, like the icon sprite: one cacheable request,
   no third party in the path, and the game still works offline. Nothing outside
   a scene uses them, so no other page pays for them. */
@font-face {
  font-family: "VT323";
  src: url("/fonts/vt323-latin-400.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Press Start 2P";
  src: url("/fonts/press-start-2p-latin-400.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

/* The whole page becomes the scene. Fixed rather than tall: a scrollbar next
   to a torchlit corridor, or beside a piste, is the end of the illusion.

   Keyed on data-scene and not on the format: whether a format takes the screen
   is a property of having a canvas, and the day a third one does, it says so by
   setting the attribute rather than by being added to a list here. */
.play[data-scene] {
  --scene-back: #08070a;
  /* What a phone's keyboard covers, measured by play.js while a scene is up.
     0 everywhere else — on a desktop, and on a browser that will not say. */
  --scene-keyboard: 0px;
  position: fixed;
  inset: 0;
  z-index: 40;
  margin: 0;
  padding: 0;
  max-width: none;
  display: flex;
  flex-direction: column;
  background: var(--scene-back);
}

/* The one thing a format decides about the shell it is played in. */
.play[data-game-mode="ALPINE_RACE"] { --scene-back: #bcdef4; }

/* The banner a teacher testing their game must never stop seeing, and the
   alert. Both stay in the flow, above the scene: a band is what they are, and
   the heading below reads as being under them. */
.play[data-scene] #page-alert,
.play[data-scene] #preview-banner {
  flex: none;
  margin: 0;
  border-radius: 0;
}

.play[data-scene] > #scene-view {
  position: relative;
  flex: 1;
  min-height: 0;
}

.scene-stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--scene-back);
}

.scene-canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* Nearest-neighbour everywhere: a pixel drawn at 64x32 has to land on whole
     pixels, or the stones shimmer as the camera moves. */
  image-rendering: pixelated;
}

/* The way out, at the end of either format — the door of the maze or the line
   at the bottom of the piste: the light takes the screen. On the stage itself
   so it covers the controls too, and in CSS so it keeps going after the render
   loop has been told to stop. */
.scene-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  background: #fff;
  opacity: 0;
  pointer-events: none;
  /* Slow on purpose. This is the end of the game, not a page transition — and
     the renderer waits on exactly this number, so the two must agree. It then
     waits again, on a screen that is already fully white: what follows is an
     ordinary page, and arriving mid-fade is arriving too early. */
  transition: opacity 1.5s ease-in;
}

.scene-stage.is-leaving::after { opacity: 1; }

/* Walking out of a scene, which is not the same thing as finishing one: a
   second of white and then half a second held on it, and the renderer waits on
   exactly those two numbers. Faster than the fade at the line on purpose —
   that one is a reward and the player is watching it, this one is a door and
   the player has already decided to go through it. */
.scene-stage.is-quitting::after {
  opacity: 1;
  transition: opacity 1s ease-in;
}

/* Whatever ends a scene — finishing it or giving up — the controls go first.
   The box does NOT go with them: giving up has a line to say, and it is read
   while the torch is going out. It only leaves on the way out, under the
   flash. */
.scene-stage.is-ending .scene-hud {
  opacity: 0;
  transition: opacity 0.5s ease-out;
  pointer-events: none;
}

.scene-stage.is-leaving .scene-box {
  opacity: 0;
  transition: opacity 0.6s ease-out;
  pointer-events: none;
}

/* ---------- Over the scene ---------- */

.scene-hud {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  /* A scrim rather than a bar: the corridor keeps going up behind the title. */
  background: linear-gradient(to bottom, rgba(8, 7, 10, 0.85), rgba(8, 7, 10, 0));
}

/* The title of the game, in the interface's own typeface: it is the teacher's
   words about the work, not part of the fiction. */
.scene-hud-title {
  flex: 1;
  min-width: 0;
  text-align: center;
  color: #fff;
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

.scene-hud-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  color: #fff;
  background: rgba(8, 7, 10, 0.55);
  border: var(--border-width) solid rgba(255, 255, 255, 0.35);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.scene-hud-btn:hover { background: rgba(8, 7, 10, 0.85); }

.scene-hud-btn:focus-visible {
  outline: var(--focus-width) solid #fff;
  outline-offset: var(--focus-offset);
}

/* The countdown is the same node the quiz uses, moved here: one clock, one
   piece of code driving it. Only its colours change. */
.scene-hud .play-clock { color: #fff; }
.scene-hud .play-clock.is-urgent { color: var(--red-300); }

/* The coin coming out of the chest. A second of gold climbing out of the scene
   and gone — the chest itself says what happened, this says what it was worth,
   and neither of them needs the box to say it in words. */
.scene-gold {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 44%;
  opacity: 0;
  color: #ffcf4a;
  font-family: "Press Start 2P", var(--font-mono);
  /* Big enough to be the thing you look at. This is the one reward the maze
     hands out and it is on screen for a second: read at a glance or not at
     all. */
  font-size: 1.75rem;
  white-space: nowrap;
  text-shadow: 0 3px 0 rgba(0, 0, 0, 0.9), 0 0 18px rgba(255, 207, 74, 0.55);
  pointer-events: none;
  transform: translate(-50%, 0);
}

.scene-gold.is-rising { animation: scene-gold-rise 1s ease-out; }

@keyframes scene-gold-rise {
  from { opacity: 0; transform: translate(-50%, 16px) scale(0.7); }
  15% { opacity: 1; transform: translate(-50%, 0) scale(1); }
  to { opacity: 0; transform: translate(-50%, -80px) scale(1); }
}

/* ---------- The lives ---------- */

/* Pixel hearts, not an icon and not a number: how many are left has to be read
   without being counted, in the corner of the eye, while walking. */
.scene-hearts {
  flex: none;
  display: flex;
  align-items: center;
  gap: 2px;
}

.scene-heart {
  width: 20px;
  height: 20px;
  /* Same rule as the scene: a 16px sprite blown up has to land on whole pixels. */
  image-rendering: pixelated;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.9));
}

/* A heart that has gone stays in place rather than disappearing: three of five
   and three of three are not the same situation, and a row that shortened would
   say they were. */
.scene-heart.is-lost { opacity: 0.55; }

/* ---------- The dialogue box ---------- */

/* Black, a hard white border, a pixel typeface: the box a game talks in. It is
   deliberately NOT a card from the design system — everything else on this
   screen is fiction, and a rounded surface in the middle of it is the seam. */
.scene-box {
  position: absolute;
  z-index: 2;
  left: var(--space-3);
  right: var(--space-3);
  /* Lifted clear of the keyboard rather than pushed up by it: the question is
     drawn in the sky of the canvas, and a page that scrolls to show the field
     is a page that has taken the question off the screen. */
  bottom: calc(var(--space-3) + var(--scene-keyboard, 0px));
  max-width: 640px;
  margin: 0 auto;
  /* Room at the top for the counter, which sits in the border area rather than
     in the sentence's way. */
  padding: var(--space-5) var(--space-4) var(--space-3);
  color: #fff;
  background: #000;
  border: 4px solid #fff;
  font-family: "VT323", var(--font-mono);
  /* VT323 runs small for its size; this is what makes it match the interface. */
  font-size: 1.5rem;
  line-height: 1.15;
}

/* A box with nothing in it is not a box. The alpine race empties it between two
   questions, and an empty black bar sitting on the piste is a bar. */
.scene-box:empty { display: none; }

.scene-line {
  margin: 0 0 var(--space-3);
  overflow-wrap: anywhere;
}

/* Which question this is, kept out of the way of the sentence. */
.scene-count {
  position: absolute;
  top: 2px;
  right: var(--space-3);
  margin: 0;
  font-size: 1.125rem;
  color: rgba(255, 255, 255, 0.55);
}

.scene-choices {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* One row per corridor. The letter is not decoration: it is the same letter
   posted over that corridor in the scene, and it is what ties the two together
   — the arrow only says which way it turns. */
.scene-choice {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  text-align: left;
  font: inherit;
  color: #fff;
  background: transparent;
  border: var(--border-width) solid transparent;
  cursor: pointer;
}

.scene-choice:hover:not(.is-closed) { background: rgba(255, 255, 255, 0.12); }

.scene-choice:focus-visible {
  outline: none;
  border-color: #fff;
  background: rgba(255, 255, 255, 0.12);
}

.scene-choice-letter {
  flex: none;
  min-width: 1.6em;
  color: #ffe9b8;
  font-family: "Press Start 2P", var(--font-mono);
  font-size: 0.75rem;
}

.scene-choice-arrow {
  flex: none;
  color: rgba(255, 255, 255, 0.55);
}

.scene-choice-label { flex: 1; min-width: 0; }

/* ---------- The field, for a question that is typed ----------
   The alpine race asks questions the maze cannot: one word, one number. The row
   is the same black box as the answers above it, but the control inside it is
   the interface's own input — a pixel-styled text field is a text field nobody
   is sure they can type in, and this one is focused before it is looked at. */

.scene-entry {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.scene-entry-field {
  flex: 1;
  min-width: 0;
  /* Bigger than an ordinary field: it is read and typed into at speed. */
  font-size: var(--text-lg);
}

.scene-entry-send { flex: none; }

/* A phone with the keyboard up has half a screen left, and the box is standing
   in the middle of the piste rather than at the foot of it. So it gives back
   what it can — a size, some of the padding it wears for the counter — and it
   goes slightly see-through: the skier passing behind the answer is better than
   the skier hidden by it, and the border keeps the box readable either way. */
.play.is-keyboard .scene-box {
  font-size: 1.25rem;
  padding: var(--space-4) var(--space-3) var(--space-2);
  /* Clear of the keys and not flush against them: a field touching the top row
     of a keyboard is a field half of whose taps land on the keyboard. */
  margin-bottom: var(--space-3);
  background: rgba(0, 0, 0, 0.72);
  backdrop-filter: blur(2px);
}

/* The chest room, which is the one question here answered once. The box itself
   flinches — a red edge and a shake — because nothing else on the screen can
   say "that one is gone" when there is no dead end to walk into. */
.scene-box.is-wrong {
  border-color: var(--red-300);
  animation: scene-flinch 0.4s ease-out;
}

@keyframes scene-flinch {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  50% { transform: translateX(5px); }
  80% { transform: translateX(-3px); }
}

/* The answer that was given, and the one that would have opened the chest.
   Colour AND an icon, both: a green edge alone is not an answer to a
   colour-blind reader, and this is the only place in the maze that shows a
   right answer without walking down it. */
.scene-choice.is-right {
  border-color: #6ee7a8;
  background: rgba(110, 231, 168, 0.14);
}

.scene-choice.is-wrong {
  border-color: var(--red-300);
  background: rgba(248, 113, 113, 0.14);
}

.scene-choice-right { flex: none; color: #6ee7a8; }

/* A corridor already walked. Struck through rather than removed: the answer
   stays readable, and what was tried stays visible — that is the whole point of
   being sent back to the junction. */
.scene-choice.is-closed {
  cursor: not-allowed;
  opacity: 0.45;
}

.scene-choice.is-closed .scene-choice-label { text-decoration: line-through; }

.scene-choice-closed { flex: none; color: var(--red-300); }

/* A flash that long is exactly the effect somebody asked not to be shown, and
   a box that jumps is the same request. */
@media (prefers-reduced-motion: reduce) {
  .scene-stage::after { transition: none; }
  .scene-box.is-wrong { animation: none; }
  /* Still shown, and still gone after a second: what it says is the point, the
     climb is only how it says it. */
  .scene-gold.is-rising { animation: scene-gold-hold 1s steps(1); }

  @keyframes scene-gold-hold {
    from, 90% { opacity: 1; }
    to { opacity: 0; }
  }
}


/* ---------- The scoreboard ----------
   On the result screen, whatever the format was played: how a ranking is
   computed belongs to the format, how it is read does not. */

.rank-list {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.rank-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 52px;
  padding: var(--space-2) var(--space-3);
  background: var(--c-surface);
  border: var(--control-border-width) solid transparent;
  border-radius: var(--radius-md);
  font-size: var(--text-md);
}

/* The podium. A coloured disc behind the number the line already carries:
   three places are recognised before they are read, and the figure is still
   there for anyone the colours do nothing for. The medal tints are the one
   place this file names colours: gold, silver and bronze are not tokens of
   any theme. */
.rank-item.is-gold .rank-place   { background: #f5d77a; color: #4d3a00; }
.rank-item.is-silver .rank-place { background: #d9dee4; color: #333d52; }
.rank-item.is-bronze .rank-place { background: #e8b98f; color: #4d2a0c; }

/* The reader's own line. The only thing anybody looks for on a scoreboard is
   themselves, so it is marked rather than left to be found — by a box, a tint
   and the weight, never by colour alone. Last, so that it wins over a medal:
   being third matters less than being you. */
.rank-item.is-me {
  border-color: var(--c-primary);
  background: var(--c-primary-subtle);
  font-weight: var(--weight-bold);
}

/* What a game paid in coins. Shown on the result screen and on the profile, so
   the same line means the same thing in both places. */
.result-gold {
  color: #b8860b;
  font-weight: var(--weight-bold);
}

[data-theme="dark"] .result-gold { color: #e8c15c; }

.rank-place {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-pill);
  background: var(--c-surface-hover);
  color: var(--c-text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
}

.rank-score {
  flex: none;
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
}


/* ============================================================ Join a game
   The public page a scanned code lands on. Its whole job is to be recognised
   in the second before the password is typed: the format's own illustration,
   then the name the teacher gave the game, then the group the code belongs to,
   each one smaller than the last. */

.join-art {
  margin: 0 0 var(--space-4);
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* A fixed ratio: the catalogue's illustrations are not all the same shape,
     and a card that changes height with the format looks like a bug. */
  aspect-ratio: 16 / 9;
  background: var(--c-surface-sunken);
}

.join-art img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.join-title {
  margin: 0;
  font-size: var(--text-xl);
  /* The teacher's own words, and they can be long: wrapped rather than cut. */
  overflow-wrap: anywhere;
}

.join-label {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
}

.join-player {
  margin: var(--space-4) 0 var(--space-3);
  font-weight: var(--weight-bold);
}

.join-who {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--text-lg);
}


/* ============================================================ Landing page
   The logo on the night band, the promise in one line, two ways in. */

.landing-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding-top: var(--space-7);
  text-align: center;
}

.landing-mark { width: 72px; height: 72px; }

.landing-title { margin: 0; width: 100%; }

/* The wordmark in white, "School" in the lighter blue that keeps 4.5:1 on the
   night band. */
.landing-word {
  --brand-accent: var(--blue-400);

  display: block;
  width: min(100%, 300px);
  height: auto;
  aspect-ratio: 10.54;
  margin: 0 auto;
  color: var(--neutral-0);
}

.landing-hero p { max-width: 28ch; font-size: var(--text-md); }
