/* Correct the Robot — the painting is the app.
 *
 * `images/scene.webp` is the stage. Everything else floats above it as DOM,
 * and two things are pinned to the artwork's own coordinate system rather
 * than to the viewport: the robot's FACE, which has to land exactly on the
 * screen the artist painted, and — on a wide screen — the whole UI, so a
 * percentage here means the same thing it means in the image.
 *
 * Two layouts, because a phone cannot hold a 16:9 painting and nine chips:
 *
 *   portrait   the scene is a lit window across the top, the UI stacks below
 *   landscape  the scene fills the frame, the UI is anchored into the art
 *
 * The SENTENCE is the point of the game, so it is the largest text, set in
 * 楷体 — the regular script mainland primary readers are taught with, where
 * the strokes on screen are the strokes the hand makes. Every label the
 * learner is *not* being tested on is 黑体, which holds together at chip size.
 * Both faces are served from this machine and subset by scripts/build_fonts.py.
 *
 * The card carrying the sentence hangs from the bottom edge of the banner in
 * the painting and grows UPWARD, which is the whole reason for hanging it
 * that way: a second or third line must never push the microphone off the
 * desk. Type steps down in three sizes rather than fitting continuously — a
 * child should meet the same size every time a sentence is the same shape.
 */

@font-face {
  font-family: "WenKaiSC";
  src: url("../fonts/wenkai-sc.woff2") format("woff2");
  /* One file for the whole range: a static face declared across 400-700 is
     used as drawn, where a single weight would be synthetically emboldened
     and thicken exactly the strokes a learner is reading. */
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "NotoSansSC";
  src: url("../fonts/notosans-sc-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "NotoSansSC";
  src: url("../fonts/notosans-sc-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Every ink below was sampled out of the painting, so the UI and the
     artwork share one palette instead of two that nearly match. */
  --paper: #FFFCF4;
  --paper-2: #F8EAD2;
  --paper-edge: #E4C295;
  --ink: #23368C;
  --ink-soft: #5468B8;
  --ink-warm: #4A4535;

  --dusk: #24409A;
  --dusk-deep: #101B42;
  /* The strips above and below the artwork when the window is not 16:9.
     Sampled from the middle of the painting's own top and bottom edges, not
     from the whole row: averaging the edge takes in the bookshelf and the
     stack of books and comes back a flat mauve that reads as a border. */
  --sky-edge: #395CB2;
  --desk-edge: #F6B183;

  --coral: #FB5E76;
  --coral-dark: #D93E58;
  --gold: #FFCE52;
  --gold-deep: #E5A81F;
  --cyan: #8FE9F0;

  --kai: "WenKaiSC", "Kaiti SC", "KaiTi", "STKaiti", serif;
  --hei: "NotoSansSC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--dusk-deep);
  color: var(--paper);
  font: 400 16px/1.6 var(--hei);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

/* The one sizing container. Everything in the game is expressed in cqw, so
   the whole design scales as one object instead of drifting apart. */
.app {
  position: fixed;
  inset: 0;
  container-type: size;
  overflow: hidden;
  background: linear-gradient(180deg, var(--sky-edge) 0%, #1B2A66 40%, #1A2755 66%, var(--desk-edge) 100%);
}

/* Visible to a screen reader, not to the eye — the icon-only buttons, which
   have no text a sighted learner needs, and the chips' English second line,
   which has nowhere to go once the chips are a row of nine or a 3-up grid.
   Kept in the markup for a screen reader, for the adult sitting alongside,
   and for anyone reading a test failure. */
.sr-only,
.chip small {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── the painting ─────────────────────────────────────────────────────── */

.scene { position: absolute; inset: 0; overflow: hidden; }
.scene-inner { position: absolute; display: block; }
.scene-art {
  width: 100%; height: 100%;
  display: block;
  /* `fill`, not `cover`: .scene-inner already carries the artwork's aspect
     ratio, and any other value would let the browser crop it differently
     from the box the face is positioned against. */
  object-fit: fill;
}

/* The robot's screen, measured out of the artwork. The sprites carry their
   own dot-matrix screen with the same silhouette, so one stretched to exactly
   this box covers the painted face with no seam. */
.robot {
  position: absolute;
  left: 34.45%;
  top: 20.30%;
  width: 30.56%;
  height: 32.09%;
}
.robot-face { position: relative; width: 100%; height: 100%; }
.robot-face-sprite { width: 100%; height: 100%; display: block; }

/* Admiration has no sprite of its own — it shares the laughing face, and is
   told apart by the screen lighting up. drop-shadow follows the sprite's own
   silhouette, so the glow traces the robot's screen rather than boxing it, and
   it reads over the painting where a soft overlay washes out. */
.robot-face[data-mood="admire"] .robot-face-sprite {
  filter: drop-shadow(0 0 1.8cqw #FFD766DD) drop-shadow(0 0 4cqw #FFB43C88);
  animation: robot-cheer 900ms ease-out;
}
@keyframes robot-cheer {
  0% { transform: scale(1); }
  40% { transform: scale(1.045); }
  100% { transform: scale(1); }
}
.robot-face[data-mood="reading"] .robot-face-sprite { animation: robot-talk 620ms ease-in-out infinite; }
.robot-face[data-mood="dozing"] .robot-face-sprite { animation: robot-breathe 4.2s ease-in-out infinite; }
.robot-face[data-mood="listening"] .robot-face-sprite { animation: robot-listen 1.9s ease-in-out infinite; }
.robot-face[data-mood="nervous"] .robot-face-sprite { animation: robot-shake 1.4s ease-in-out infinite; }

@keyframes robot-talk { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(.982); } }
@keyframes robot-breathe { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(.7%); } }
@keyframes robot-listen { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.13); } }
@keyframes robot-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-.5%); } 75% { transform: translateX(.5%); } }

/* Sound leaving the robot while it reads. Driven from #app's state rather
   than from the face, so nothing has to reach across the DOM to start it. */
.scene-waves {
  position: absolute;
  left: 0; top: 26%;
  width: 100%; height: 22%;
  opacity: 0;
  transition: opacity .3s;
}
.app[data-state="ROBOT_PLAYING"] .scene-waves { opacity: 1; }
.scene-waves i {
  position: absolute;
  top: 50%;
  width: 1.5%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--cyan);
  box-shadow: 0 0 1.4cqw #8FE9F0AA;
  animation: robot-wave 1.1s ease-in-out infinite;
}
.scene-waves i:nth-child(1) { left: 24%; }
.scene-waves i:nth-child(2) { left: 28.5%; animation-delay: .14s; opacity: .7; }
.scene-waves i:nth-child(3) { left: 69.5%; animation-delay: .28s; opacity: .7; }
.scene-waves i:nth-child(4) { left: 74%; animation-delay: .42s; }

@keyframes robot-wave {
  0%, 100% { transform: translateY(-50%) scale(.7); opacity: .45; }
  50% { transform: translateY(-130%) scale(1.15); opacity: 1; }
}

/* ── the floating UI ──────────────────────────────────────────────────── */

.ui { position: absolute; }

/* — the heads-up row — */

.hud { display: flex; align-items: flex-start; justify-content: space-between; gap: 3cqw; }

.hud-name {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: #FFFFFFF2;
  color: var(--ink);
  border-radius: 44% 56% 52% 48% / 58% 46% 56% 42%;
  box-shadow: 0 1cqw 3cqw -1.4cqw #0A143BAA;
}
/* Two bumps turn the rounded blob into a cloud. */
.hud-name::before,
.hud-name::after {
  content: "";
  position: absolute;
  background: #FFFFFFF2;
  border-radius: 50%;
  z-index: -1;
}
.hud-name::before { width: 46%; aspect-ratio: 1; left: 8%; top: -26%; }
.hud-name::after { width: 34%; aspect-ratio: 1; right: 12%; top: -18%; }

.progress {
  font-weight: 700;
  color: var(--ink-soft);
  white-space: nowrap;
}
.pack-title {
  font-family: var(--kai);
  font-weight: 700;
  color: var(--ink);
  line-height: 1.15;
  /* Pack titles run to thirteen characters (第三关 · 听机器人的声音), which no
     cloud this size holds on one line. Two lines and then a clip: a title cut
     mid-word says less than one that wrapped. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.points {
  flex: none;
  display: flex;
  align-items: center;
  gap: .4em;
  background: #2C4FA8E0;
  border: .22cqw solid #FFFFFF6E;
  border-radius: 999px;
  color: #fff;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 1cqw 2.6cqw -1.3cqw #04091F;
}
.points-star { fill: var(--gold); width: 1.1em; height: 1.1em; flex: none; }

/* Floated against .ui in both layouts; each one says where. */
.track { position: absolute; display: flex; align-items: center; min-width: 0; }
.track::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 2px;
  background: #C6D6FF66;
  border-radius: 99px;
  transform: translateY(-50%);
}
.track-node {
  position: relative;
  flex: 1;
  display: grid;
  place-items: center;
  min-width: 0;
}
.track-node::after {
  content: "";
  width: .9cqw;
  aspect-ratio: 1;
  border-radius: 50%;
  background: #CBD9FFAA;
}
.track-node.is-done::after { background: var(--gold); }
.track-node.is-current::after {
  background: var(--gold);
  width: 1.7cqw;
  box-shadow: 0 0 0 .5cqw #FFFFFFE6, 0 0 2cqw .3cqw #FFCE5299;
}

/* — what is happening, in words — */

.status,
.hint,
.audio-controls {
  margin: 0;
  transition: opacity .25s;
}
.status:empty { display: none; }

.status, .hint {
  background: #FFFFFFE0;
  color: var(--ink);
  border-radius: 999px;
  box-shadow: 0 .6cqw 1.8cqw -.8cqw #0A143B99;
}
.hint { background: #FFF2D6EE; }

/* — the sentence — */

.sentence-card {
  position: relative;
  background: linear-gradient(180deg, var(--paper), var(--paper-2));
  border: .22cqw solid var(--paper-edge);
  box-shadow: 0 2cqw 4cqw -2.2cqw #050C24, 0 .2cqw 0 #FFFFFFCC inset;
  display: flex;
  align-items: center;
  justify-content: center;
}
.sentence {
  margin: 0;
  font-family: var(--kai);
  font-weight: 700;
  color: var(--ink);
  text-align: center;
  text-wrap: balance;
  font-size: var(--sentence-size);
  line-height: 1.5;
  letter-spacing: .03em;
}

/* The replay button lives on the card's bottom-right corner: clear of the
   chips above it and of the microphone in the middle, so the sentence keeps
   the card's full measure on every line. */
.replay {
  position: absolute;
  right: -1.6cqw;
  bottom: -1.6cqw;
  z-index: 3;
  background: radial-gradient(120% 120% at 32% 24%, #8FC0FF, #3873F6 62%, #2A57C8);
  box-shadow: 0 .8cqw 2cqw -.7cqw #10245F, 0 0 0 .5cqw #FFFFFFD6;
  color: #fff;
}

.icon-button {
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  padding: 0;
  transition: transform .14s;
}
.icon-button:hover { transform: scale(1.06); }
.icon-button svg {
  width: 52%;
  height: 52%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* — the chips — */

/* — what to do, in the order a child does it — */

.howto {
  background: #FFFFFFF2;
  color: var(--ink);
  box-shadow: 0 1cqw 2.4cqw -1.6cqw #050B20;
}
.howto-steps {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: howto-step;
  display: grid;
}
.howto-steps li {
  display: flex;
  align-items: flex-start;
  gap: .5em;
  counter-increment: howto-step;
}
.howto-steps li::before {
  content: counter(howto-step);
  flex: none;
  display: grid;
  place-items: center;
  width: 1.7em;
  height: 1.7em;
  border-radius: 50%;
  background: var(--gold);
  color: #3B2B06;
  font-size: .82em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.howto-bonus {
  display: inline-block;
  background: #FFF3D2;
  color: var(--gold-deep);
  border-radius: 999px;
  font-weight: 700;
  font-size: .86em;
  padding: 0 .7em;
  white-space: nowrap;
}
.howto-tip {
  margin: 0;
  color: var(--ink-soft);
  border-top: 1px dashed #CBD4F0;
}

.chips { display: flex; gap: 1cqw; }

.chip {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .35em;
  background: #FFFFFFEE;
  color: var(--ink);
  border: .24cqw solid transparent;
  cursor: pointer;
  font-family: var(--hei);
  font-weight: 700;
  box-shadow: 0 1cqw 2cqw -1.4cqw #050B20;
  transition: transform .13s, border-color .13s, background .13s;
  padding: 0 .35em;
}
.chip:hover:not(:disabled) { transform: translateY(-6%); }
.chip[aria-pressed="true"] {
  background: #FFF7DF;
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 .4cqw #FFCE5255, 0 1cqw 2cqw -1.4cqw #050B20;
}
.chip.correct-robot { background: #FFF9E6; border-color: #F0CE7A; }
.chip.correct-robot[aria-pressed="true"] { border-color: var(--gold-deep); }
.chip:disabled { cursor: default; opacity: .55; }
.chip-icon { width: 1.6em; height: 1.6em; flex: none; }

/* — the microphone — */

.mic {
  position: relative;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  padding: 0;
  background: radial-gradient(120% 120% at 34% 24%, #FF95A6, var(--coral) 55%, var(--coral-dark));
  box-shadow: 0 2cqw 4cqw -1.6cqw #6B0D22, 0 0 0 .7cqw #FFFFFF5C;
  display: grid;
  place-items: center;
  color: #fff;
  z-index: 4;
}
.mic svg {
  width: 44%;
  height: 44%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.mic::after {
  content: "";
  position: absolute;
  inset: -14%;
  border-radius: 50%;
  border: .4cqw solid #FF8CA0;
  opacity: 0;
}
.mic.is-listening {
  background: radial-gradient(120% 120% at 34% 24%, #FFC2CD, #FF7189 55%, #E04660);
}
.mic.is-listening::after { animation: mic-ping 1.5s ease-out infinite; }
@keyframes mic-ping {
  0% { transform: scale(.86); opacity: .85; }
  100% { transform: scale(1.3); opacity: 0; }
}

.mic-label {
  margin: 0;
  font-family: var(--kai);
  font-weight: 700;
  color: #fff;
  text-align: center;
  letter-spacing: .08em;
  text-shadow: 0 .2cqw .6cqw #0B1436, 0 0 1.8cqw #12235799;
}
/* The label belongs to the button; when there is no button there is nothing
   for it to name. Sibling rule rather than a line in the view, because the
   view already has one reason to hide the button and does not need a second. */
.mic[hidden] + .mic-label { display: none; }

/* — buttons — */

.button {
  border: none;
  border-radius: 999px;
  cursor: pointer;
  font-family: var(--kai);
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4em;
  transition: transform .14s;
}
.button:hover { transform: translateY(-4%); }
.button.primary {
  background: linear-gradient(180deg, #FF8093, var(--coral));
  color: #fff;
  box-shadow: 0 1.2cqw 2.4cqw -1.2cqw #7A1228;
}
.button.ghost {
  background: #FFFFFFEE;
  color: var(--ink);
  border: .24cqw solid var(--paper-edge);
}
.button-icon { width: 1em; height: 1em; }

/* — the result and finish sheets — */

.result,
.finished {
  background: linear-gradient(180deg, #FFFDF7, #F7EBD7);
  border: .22cqw solid var(--paper-edge);
  box-shadow: 0 3cqw 6cqw -2.8cqw #030818;
  color: var(--ink);
  display: flex;
  flex-direction: column;
}

.result-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1em; }
.result-title { margin: 0; font-family: var(--kai); font-weight: 700; }
.round-total { margin: 0; font-weight: 700; color: var(--coral-dark); font-variant-numeric: tabular-nums; }
.session-total { margin: 0; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

.result-lines { display: grid; align-content: start; gap: .9em; }
.result-row { display: grid; gap: .35em; }
.result-line { display: flex; align-items: baseline; justify-content: space-between; gap: 1em; }
.result-line h3 { margin: 0; font-size: 1em; font-weight: 700; }
.result-line h3 span { color: var(--ink-soft); font-weight: 400; font-size: .8em; }
.result-figure { margin: 0; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.result-detail { margin: 0; color: var(--ink-warm); }

.meter { position: relative; background: #E3D6BE; border-radius: 99px; height: .55em; }
.meter-fill { overflow: hidden; }
/* Where the reading has to reach. Without it a bar at 45 is just a colour;
   with it, it is short of a line. Position comes from readingGate.js. */
.meter-reading::after {
  content: "";
  position: absolute;
  left: var(--pass-mark, 60%);
  top: -40%;
  bottom: -40%;
  width: 2px;
  border-radius: 2px;
  background: var(--coral-dark);
  opacity: .5;
}
.meter-fill { display: block; height: 100%; width: 0; max-width: 100%; border-radius: 99px; transition: width .4s ease-out; }
.meter-fill.reading { background: linear-gradient(90deg, #4F8BF5, #2A57C8); }
.meter-fill.detective { background: linear-gradient(90deg, #FFD873, var(--gold-deep)); }

.robot-log {
  background: #FFFFFFB8;
  border: 1px dashed #D9BE93;
  color: var(--ink-warm);
}
.robot-log-kicker {
  display: block;
  letter-spacing: .18em;
  color: #A08A5E;
  font-weight: 700;
}
.explanation, .success-message { margin: .5em 0 0; }
.success-message { color: var(--coral-dark); font-weight: 700; }

.result-actions { display: flex; gap: .6em; align-items: center; margin-top: auto; }
.result-actions .button { flex: 1; }
.result-actions .icon-button {
  flex: none;
  background: #fff;
  color: var(--ink);
  border: .24cqw solid var(--paper-edge);
}

/* Reading again is a quiet icon until the round is asking for another go —
   then it grows into the only button on the row and says so. The label is
   there either way, for a screen reader and for the accessible name. */
.retry-label {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.result-actions .icon-button.is-primary {
  flex: 1;
  width: auto;
  height: auto;
  border: none;
  border-radius: 999px;
  grid-auto-flow: column;
  gap: .5em;
  background: linear-gradient(180deg, #FF8093, var(--coral));
  color: #fff;
  box-shadow: 0 1.2cqw 2.4cqw -1.2cqw #7A1228;
  font-family: var(--kai);
  font-weight: 700;
}
.result-actions .icon-button.is-primary .retry-label {
  position: static;
  width: auto;
  height: auto;
  clip-path: none;
}
.result-actions .icon-button.is-primary svg { width: 1.15em; height: 1.15em; }

.finished { align-items: center; justify-content: center; text-align: center; }
.finished h2 { margin: 0; font-family: var(--kai); font-weight: 700; }

/* — a load failure replaces the page — */

.fatal {
  position: absolute;
  inset: 8%;
  display: grid;
  place-content: center;
  text-align: center;
  background: var(--dusk-deep);
  border-radius: 3cqw;
  color: var(--paper);
  padding: 4cqw;
  font-size: 4cqw;
}

/* ══ PORTRAIT — the scene is a lit window, the UI stacks below ══════════ */

.scene { height: 36cqh; }
/* Zoomed to the robot's head, and dissolved into the panel below rather than
   cut off: the crop is deliberate, the seam should not be. */
.scene-inner {
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: max(100cqw, 86cqh);
  aspect-ratio: 1672 / 941;
}
.scene {
  -webkit-mask-image: linear-gradient(180deg, #000 82%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 82%, transparent 100%);
}

.ui {
  /* The phone stack is drawn in --u, not in cqw.
   *
   * Laid out purely in cqw it is right on a phone and absurd on a window that
   * is wide but short — 900x700 put a 150px microphone below the fold. The
   * stack needs about 2.2 screens of height per screen of width, so the unit
   * is capped by height at the phone's own ratio: on a phone the two agree, on
   * anything squatter the height wins and the column stays a phone-shaped
   * column in the middle of the room rather than a poster nobody can finish
   * scrolling. */
  --u: min(1cqw, .462cqh);

  top: 0;
  bottom: 0;
  left: 50%;
  right: auto;
  transform: translateX(-50%);
  width: min(100cqw, calc(106 * var(--u)));
  display: flex;
  flex-direction: column;
  gap: 1.4cqh;
  /* The stack starts below the lit window, overlapping its last few percent
     so the card sits on the seam. On the panel rather than on the card,
     because the card is not always the first thing in the column — the
     result sheet takes its place, and used to start at the top of the
     screen under the score pill. */
  padding: 26cqh calc(4.5 * var(--u)) calc(2.6cqh + env(safe-area-inset-bottom, 0px));
  overflow-y: auto;
}

.hud {
  position: absolute;
  left: calc(4.5 * var(--u));
  right: calc(4.5 * var(--u));
  top: calc(5.2cqh + env(safe-area-inset-top, 0px));
  align-items: flex-start;
}
.hud-name { padding: calc(2 * var(--u)) calc(2.8 * var(--u)); width: calc(34 * var(--u)); }
.progress { font-size: calc(2.5 * var(--u)); }
.pack-title { font-size: calc(4 * var(--u)); }
.points { padding: calc(.9 * var(--u)) calc(3 * var(--u)); font-size: calc(4 * var(--u)); }
/* In the sky above the cloud badge — the one strip of the painting with
   nothing in it at this width. */
.track {
  left: calc(4.5 * var(--u));
  right: calc(4.5 * var(--u));
  top: calc(1.2cqh + env(safe-area-inset-top, 0px));
  height: 1.8cqh;
  flex: none;
}

/* In the column rather than pinned over the scene: with the instructions
   under the card there is no longer a gap below the robot's chin to float it
   in, and a line that says what just happened belongs above the thing it
   happened to. */
.status {
  align-self: center;
  flex: none;
  padding: calc(1.2 * var(--u)) calc(3 * var(--u));
  font-size: calc(3 * var(--u));
  text-align: center;
}

/* The card overlaps the seam between the window and the panel, which is what
   ties the two halves of the phone layout together. */
.sentence-card {
  border-radius: calc(3.4 * var(--u));
  padding: calc(3.8 * var(--u)) calc(4.4 * var(--u));
  min-height: 11cqh;
  flex: none;
  --sentence-size: calc(5.6 * var(--u));
}
.sentence-card[data-fit="medium"] { --sentence-size: calc(4.9 * var(--u)); }
.sentence-card[data-fit="long"] { --sentence-size: calc(4.2 * var(--u)); }
.replay {
  width: calc(11 * var(--u));
  height: calc(11 * var(--u));
  /* The minus belongs inside calc(): `-calc(…)` is not a value, and the
     browser drops the whole declaration without a word. */
  right: calc(-2.4 * var(--u));
  bottom: calc(-4 * var(--u));
}

.hint, .audio-controls { text-align: center; flex: none; }
.hint { padding: calc(1.2 * var(--u)) calc(2.6 * var(--u)); font-size: calc(3 * var(--u)); }
.audio-controls .button { padding: calc(2 * var(--u)) calc(4 * var(--u)); font-size: calc(4 * var(--u)); }

.howto {
  flex: none;
  border-radius: calc(3.4 * var(--u));
  padding: calc(2.6 * var(--u)) calc(3.2 * var(--u));
  font-size: calc(2.8 * var(--u));
  line-height: 1.5;
}
.howto-steps { gap: calc(1.1 * var(--u)); }
.howto-tip {
  margin-top: calc(1.9 * var(--u));
  padding-top: calc(1.6 * var(--u));
  font-size: calc(2.6 * var(--u));
}

.diagnosis { flex: none; }
.chips {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: calc(2 * var(--u));
}
.chip {
  flex: none;
  border-radius: calc(3 * var(--u));
  padding: calc(1.8 * var(--u)) calc(1 * var(--u));
  font-size: calc(3.2 * var(--u));
}
.chip.correct-robot { grid-column: 1 / -1; }

.dock {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1cqh;
  margin-top: auto;
  padding-top: .6cqh;
  flex: none;
}
.mic { width: calc(17 * var(--u)); height: calc(17 * var(--u)); }
.mic-label { font-size: calc(4.4 * var(--u)); }

.result, .finished {
  border-radius: calc(4 * var(--u));
  padding: calc(4.4 * var(--u));
  gap: calc(3 * var(--u));
  font-size: calc(3.2 * var(--u));
  flex: none;
}
.result-title, .finished h2 { font-size: calc(5 * var(--u)); }
.round-total { font-size: calc(6.4 * var(--u)); }
.result-grid { display: grid; gap: calc(3 * var(--u)); }
.robot-log { border-radius: calc(2.4 * var(--u)); padding: calc(3 * var(--u)); font-size: calc(3.1 * var(--u)); line-height: 1.6; }
.robot-log-kicker { font-size: calc(2.4 * var(--u)); }
.result-actions .button,
.result-actions .icon-button.is-primary { padding: calc(2.4 * var(--u)) 0; font-size: calc(4.4 * var(--u)); }
.result-actions .icon-button { width: calc(11 * var(--u)); height: calc(11 * var(--u)); }
.finished .button { padding: calc(2.4 * var(--u)) calc(6 * var(--u)); font-size: calc(4.6 * var(--u)); }

/* ══ LANDSCAPE — the UI is anchored into the painting ═══════════════════
 *
 * Two conditions, and they guard different things. The WIDTH is chip
 * legibility: a chip is 1.18cqw, so below about 820px the row of nine stops
 * being readable and the phone's three-up grid is the better shape whatever
 * the device is. The ASPECT is the letterboxing: at 5:4 the strip above and
 * below the artwork is 14% of the height, which the gradient sampled from the
 * painting's own edges carries; much squarer than that and it stops reading
 * as sky and desk.
 */
@media (min-width: 820px) and (min-aspect-ratio: 5 / 4) {

  .scene, .ui {
    /* Reclaims each property the phone stack set, rather than assuming which
       ones the cascade happens to leave alone. */
    --u: 1cqw;
    left: 0;
    right: auto;
    bottom: auto;
    width: 100cqw;
    height: auto;
    aspect-ratio: 1672 / 941;
    top: 50%;
    transform: translateY(-50%);
    -webkit-mask-image: none;
    mask-image: none;
    display: block;
    padding: 0;
    overflow: visible;
  }
  .scene-inner { inset: 0; width: 100%; transform: none; }

  .hud {
    position: absolute;
    left: 3%;
    right: 3%;
    top: 3%;
    align-items: flex-start;
  }
  .hud-name { width: 15%; padding: 1.1% 1.4%; }
  .progress { font-size: 1cqw; }
  .pack-title { font-size: 1.5cqw; }
  .track { left: 20%; right: 14%; top: 5.4%; height: 1.6cqw; }
  .points { margin-top: 1cqw; padding: .4cqw 1.4cqw; font-size: 1.5cqw; }

  /* What just happened, then what to do about it, then the retry line: the
     left gutter is bookshelf, so this column runs down it without reaching
     the robot, the chips or the sentence. The tap-to-play fallback goes to
     the other side rather than pushing this column down into the chips. */
  .status, .hint, .howto {
    position: absolute;
    left: 3%;
    text-align: left;
    align-self: auto;
  }
  .status { top: 15.5%; width: 27%; padding: .6% 1.2%; font-size: 1.05cqw; }
  .howto {
    top: 22.5%;
    width: 26%;
    border-radius: 1cqw;
    padding: 1cqw 1.2cqw;
    font-size: 1.05cqw;
    line-height: 1.5;
  }
  .howto-steps { gap: .5cqw; }
  .howto-tip { margin-top: .8cqw; padding-top: .7cqw; font-size: .95cqw; }
  .hint { top: 42%; width: 27%; padding: .6% 1.2%; font-size: 1.05cqw; }

  .audio-controls { position: absolute; right: 3%; top: 15.5%; text-align: right; }
  .audio-controls .button { padding: .5cqw 1.4cqw; font-size: 1.3cqw; }

  .diagnosis { position: absolute; left: 4%; top: 50.2%; width: 92%; }
  /* One row of nine, back out of the phone's three-up grid. */
  .chips { display: flex; gap: .7cqw; height: 5cqw; }
  .chip { flex: 1; border-radius: .9cqw; font-size: 1.18cqw; padding: 0 .4cqw; }
  .chip.correct-robot { flex: 1.45; }
  .chip-icon { width: 1.9cqw; height: 1.9cqw; }

  .sentence-card {
    position: absolute;
    left: 17%;
    width: 66%;
    bottom: 13.5%;
    /* Tall enough to cover the banner in the painting even at one line, and
       free to grow past it. */
    min-height: 21%;
    border-radius: 1.2cqw;
    /* The bottom pad is the microphone's seat: text stops above the disc
       however many lines there are. */
    padding: 1.7cqw 2.6cqw 6.4cqw;
  }
  .sentence-card { --sentence-size: 3.2cqw; }
  .sentence-card[data-fit="medium"] { --sentence-size: 2.6cqw; }
  .sentence-card[data-fit="long"] { --sentence-size: 2.2cqw; }
  .replay { width: 5cqw; height: 5cqw; right: -1.6cqw; bottom: -1.6cqw; }

  /* display:contents lets the three pieces of the dock be placed against the
     artwork independently, while the view keeps one element to hide. */
  .dock { display: contents; }
  .mic {
    position: absolute;
    left: 50%;
    top: 80%;
    transform: translateX(-50%);
    width: 7.6cqw;
    height: 7.6cqw;
  }
  .mic-label { position: absolute; left: 0; right: 0; top: 94%; font-size: 1.9cqw; }

  /* Hung just under the robot's chin and run to the bottom of the frame: the
     sheet has more to say than anything else in the round, and it is the one
     panel that has the screen to itself. */
  .result {
    position: absolute;
    left: 19%;
    top: 53.4%;
    width: 62%;
    bottom: 2.5%;
    border-radius: 1.4cqw;
    padding: 1.3cqw 1.8cqw;
    gap: .6cqw;
    font-size: 1.08cqw;
  }
  .result-title { font-size: 1.9cqw; }
  .round-total { font-size: 2.8cqw; }
  .result-grid {
    display: grid;
    grid-template-columns: 1.05fr .95fr;
    gap: 1.4cqw;
    flex: 1;
    min-height: 0;
  }
  /* A long explanation scrolls in its own column rather than sliding under
     the buttons: whichever way the copy runs, 继续 stays where it was. */
  .result-lines, .robot-log { min-height: 0; overflow-y: auto; }
  .robot-log { border-radius: .8cqw; padding: .9cqw 1.1cqw; font-size: 1.02cqw; line-height: 1.55; }
  .robot-log-kicker { font-size: .8cqw; }
  .result-actions .button,
  .result-actions .icon-button.is-primary { padding: .5cqw 0; font-size: 1.45cqw; }
  .result-actions .icon-button { width: 3.1cqw; height: 3.1cqw; }

  .finished {
    position: absolute;
    left: 32%;
    top: 56%;
    width: 36%;
    height: 34%;
    border-radius: 1.4cqw;
    padding: 2cqw;
    gap: 1.2cqw;
    font-size: 1.3cqw;
  }
  .finished h2 { font-size: 2.4cqw; }
  .finished .button { padding: .6cqw 2.4cqw; font-size: 1.6cqw; }
  .fatal { font-size: 2cqw; }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}
