/* Doto, at 900, subset to just the glyphs this page sets and inlined.
   Subsetting is what makes this reasonable: the whole face is about 30KB and
   this is 1.2KB, so there is no extra request, no flash of the wrong face
   before it lands, and no reason to draw the mark as paths to get it early.

   It is a dot matrix face on a board made of big squares, which is the point:
   the name is drawn out of the same thing the game is played on.

   Two things wear it: the wordmark and the big score. Nothing else, and the
   reason is size. Doto is a grid of dots, so it needs height before the dots
   resolve into a shape. The stat row at 20px and the labels at 11px went muddy,
   which is a worse trade than losing the styling.

   So the subset is the wordmark plus digits, and the comma in case the score
   ever gets thousands separators. Anything outside that renders blank with no
   error, so widen it here and in tools/README.md together. */
@font-face {
  font-family: 'Doto';
  font-style: normal;
  font-weight: 900;
  font-display: block;
  src: url(data:font/woff2;base64,d09GMgABAAAAAASwAA4AAAAAKDAAAARYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYGYD9TVEFUQACBIBEICsVAy0QLLgABNgIkAzAEIAWEIAcgDAcbEirIBB7K7ut1z3zOLH3Cwr6V5yRFROVmKkdgBKXX/7umtnM1dhDjx27OE2QEqAr4k1GSAbBxDLJaT/nLqfTGcm9KbhvOLhM11ksNGwRpX3QgAjDplUpEOy2ge7su27T12EBuldeB7dvHinckCOl0S8PtklJzSscxmAVkVqKfRnx8aRK3GcQS7aKyg7ZeRHfS/e7ni7/ZpA869ELgRLaw/9o0rK6xrra4JG2LJk2eL1jl3uhJ2sFHW588FqqlMPgsBw2LtbuB/uhsLw3Fu+OLm0FEk0ERdfQzgcnMZC7zWaJU4UtGE3+/Rv1Ur+EF6qY6p06oY+q4OqQOqsPqYOaxMGUgs+cEYOKHL04ceOGJHYPn9UrhhTBFm4WX2edzge/yfiG6FL544I0nOp744Isnnph44YELAx9/9s/ohTemrvvMXrF2l4dzwMNl9oqta2D1GsPYtkaINWvcxjTcPjYPJucmAKYI5FICwXSBCQTOnc+Lbs5TfaPhHthaAUhAQ+oEYNOgm31Fn0D9XqOJbsJzNixfe9riPE0w9fuYoo0GWxM1/LDJrBLUPm+FTjmMoDfIK9dnk5z7ghp1F1WuiUvSFzJSlHSokCBCUW8q16sLXYgdog1fUb/pXQF1F9UFApL+3KFPHxBJBzX1++wJGBiN0aOOk9GrdeH7AqByjk8Ye3r36e6E6Opoplz8Um1o25LNvJh0ndUeMR1G/vqYf+uQMb1YIK+SCCjpYwo6iebvc25Y+fqDKZ4HeWP24O4Mr1lX5PVGnk7qmZo1UMOHiJtd0kJ/AbaLBgy/lfdslbhvMD4vQpNam2HCRb9zj2CMUoGHkqj3PLZAA8lMWy2q+GermeWorv4U2L5LI0dwFP5cSYyz7csDcRmjsvyOPHBgCu8f1YvyaVmJK6A1nFCXGEwOMBuM8GRDUIsxrGtBUwufXMDaJf1i+UeRjPvz6CApMaFa+qGG9Vrp1xXk4x8FN9JiGNVA5SxFDalJ2wDXoeJnv5fQwKcixtllxcwyRP/YeW1vf/Z6Zf9zSu3brboPB13Htid8HHJ1P7b/z+W4f6hh+7/SEhIn/vvNSrZhwybHIZxnHVL8JZHRiI0pW7edoRzjuegOFBMI4MntrMxkz3/hcnVXs/IUX45I4M9TJPHkPtIYw4GhOhvZCGU1suPPdORJKqkhPriJHiTAl1BAAhPPNKPzdChmAhPpZwHTmU8rE5jCJCbDUY3MYgEzGc/MZuA85lHY3o/FNNbR85nNPDKJI45ZjdYJzGRix3Im85nOFKyr1nlMILbZOpdJFHf2z2cWMZmktYzb5zKPKaj+g0gglvi50x5ndUpLSQKP+IBGPIn76n7mMkMPwkXcVS9hrmQXDSKReBJJJmbfexBNTGZCxwlfWs9caJ/KBCxEaQELAE+W5vB5DGKUHsonMYV50gIGiMViFjO0E68PvFDSeI7il9UziWvi0f+VjOtvqQ6RSJ2+l4XacFrfAQAA) format('woff2');
}

:root {
  /* Value ladder, spaced by measured luminance rather than by eye. Everything
     used to sit inside 0.56%-3.04% relative luminance, which is the band phone
     OLEDs crush and ambient light erases first. Page drops to near-black (free
     on OLED, and it buys headroom) and the rest spreads up into the range that
     was going unused.

       page   0.15%   unlit tile 2.91%   border 11.81%   lit tile 90.5%

     The art ceiling sits at 3.8%, barely clear of the unlit tile at 2.91%,
     only 1.11:1 between them. So the border is what separates a tile from the
     art behind it, at 1.91:1 against the art and 2.13:1 against the tile body,
     and it is boosted well past where a border would normally sit. */
  --bg: #040506;
  --tile-off: #29303c;
  --tile-off-edge: #546179;
  --tile-on: #f4f4f5;
  --accent: #64d2ff;

  /* The display face, used only where there is size to carry it: the wordmark
     and the big score. Doto is a dot grid, so small text turns to mush. */
  --font-lcd: 'Doto', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Sized by game.js on load and resize. */
  --tile: 56px;
  --gap: 9px;
  --gutter: 39px;
  --grid-w: 251px;
  --stage-w: 329px;
  --bank-cell: 18px;
  --bank-gap: 3px;

  /* One corner for the whole design, taken from the tile. Anything square uses
     18% of its own side; anything oblong uses 18% of its short side, so every
     corner reads as the same curve regardless of the shape it belongs to. */
  --radius: calc(var(--tile) * .18);
  --sum-cell: 22px;
  --hud-h: 88px;
  --fade-start: 88px;
  --fade-end: 150px;

  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

body {
  background: var(--bg);
  color: var(--tile-on);
  font: 500 16px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* ---- progress hairline ---- */

.progress {
  position: fixed;
  inset: 0 0 auto;
  height: 2px;
  background: #171b22;
  z-index: 3;
}

.progress > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width .26s cubic-bezier(.22, .61, .36, 1);
}

/* ---- build stamp ---- */

/* ---- hud ---- */

/* Only the children take pointer events, so the bar never swallows a tap meant
   for the board. */
.hud {
  position: fixed;
  top: env(safe-area-inset-top);
  left: env(safe-area-inset-left);
  right: env(safe-area-inset-right);
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 8px;
  pointer-events: none;
}

.hud > * { pointer-events: auto; }

/* Closes the score row off from the board. Follows the same visibility rule as
   the score itself. There is no row to close off on the menu, and no board
   under it while a modal is up. */
.hud::after {
  content: "";
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--tile-off);
}

.pause {
  display: none;
  border: 0;
  background: none;
  padding: 13px 15px;
  cursor: pointer;
  line-height: 0;
}

.pause svg { width: 14px; height: 14px; fill: #7d8593; transition: fill .12s ease; }
.pause:active svg { fill: var(--accent); }

/* Always reachable, including from the menu, since deciding about sound before
   you start is the sensible moment to do it. */
.sound {
  margin-left: auto;
  border: 0;
  background: none;
  padding: 13px 15px;
  cursor: pointer;
  line-height: 0;
}

.sound svg { width: 16px; height: 16px; fill: #7d8593; stroke: #7d8593; }
.sound:active svg { fill: var(--accent); stroke: var(--accent); }

.sound .cross { display: none; }
.sound[aria-pressed="false"] .wave { display: none; }
.sound[aria-pressed="false"] .cross { display: inline; }
.sound[aria-pressed="false"] svg { fill: #5c636f; stroke: #5c636f; }

/* Centred on the screen rather than in the leftover flex space, so the pause
   button on the left does not push it off true. */
.hud-score {
  display: none;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font: 600 15px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: .04em;
  color: #b6bcc7;
}

/* Score and pause belong to a board in progress; on the menu only the build
   stamp is meaningful. */
:root[data-mode="play"] .pause,
:root[data-mode="tutorial"] .pause,
:root[data-mode="play"] .hud::after,
:root[data-mode="tutorial"] .hud::after { display: block; }
:root[data-mode="play"] .hud-score { display: block; }
:root[data-overlay="1"] .pause,
:root[data-overlay="1"] .hud-score,
:root[data-overlay="1"] .hud::after { visibility: hidden; }

/* Menu only. It is a deploy check and a manual reload, neither of which is
   wanted mid-board, and the game screen has better uses for its corners.
   Above the menu overlay, or it could be seen but not tapped. Padding buys a
   real tap target around a deliberately small label, since the page has no
   scroll and so no pull-to-refresh to fall back on. */
.build {
  display: none;
  position: fixed;
  left: env(safe-area-inset-left);
  bottom: env(safe-area-inset-bottom);
  z-index: 10;
  padding: 15px 17px;
  border: 0;
  background: none;
  font: 400 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: .08em;
  color: #5c636f;
  cursor: pointer;
  transition: color .12s ease;
}

:root[data-mode="start"] .build { display: block; }

.build:active { color: var(--accent); }

/* the hash could not be read, so treat what you're looking at as unknown */
.build.unknown { color: #ff7a7a; }

/* ---- generated background ---- */

#bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* ---- stage ---- */

.stage {
  position: relative;
  z-index: 1;
  width: var(--stage-w);
  height: 100vh;
  height: 100dvh;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
}

/* ---- board ---- */

/* Two fades, doing different jobs. The colour ramp in updateView runs the full
   height of the board and takes each row's greys toward the page. That one
   keeps tiles opaque so the backdrop cannot show through them. This mask is a
   second, much shorter one over the top, clearing the queue away before it
   reaches the score.
   It runs from the hairline under the score row to one tile below it, so rows
   emerge out of that edge rather than out of the top of the screen.
   It masks .board only. The generated backdrop is painted on a canvas behind
   .stage, so it stays at full strength right up to the top of the screen and
   only the tiles are taken away. */
.board {
  position: relative;
  flex: 1;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, transparent var(--fade-start), #000 var(--fade-end));
  mask-image: linear-gradient(to bottom, transparent var(--fade-start), #000 var(--fade-end));
}

.stack {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0;
  transition: transform .26s cubic-bezier(.22, .61, .36, 1);
}

.row {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--tile);
  display: flex;
  gap: var(--gap);
  padding: 0 var(--gutter);
}

/* --t-* are set per row by updateView: the tile colours already blended
   toward the page background by depth, so a queued row reads dimmer without
   ever going translucent and letting the backdrop through. */
.tile {
  flex: 1;
  border-radius: var(--radius);
  background: var(--t-off, var(--tile-off));
  box-shadow: inset 0 0 0 1px var(--t-edge, var(--tile-off-edge));
}

.tile.on {
  background: var(--t-on, var(--tile-on));
  box-shadow: none;
}

/* the two operand rows read a touch brighter than the queue above them */
.row.op .tile.on { box-shadow: 0 0 calc(var(--tile) * .3) rgba(244, 244, 245, .28); }

/* ---- accumulator ---- */

/* Deliberately outside .stack. It holds the running total, so it must stay put
   while the queue scrolls past it, and your answer has to land somewhere that
   does not then slide away. */
.accum {
  position: relative;
  height: var(--tile);
  display: flex;
  gap: var(--gap);
  padding: 0 var(--gutter);
  margin-top: var(--gap);
}

/* No colour transition: the swap happens at the midpoint of the flip, where
   the tile is edge-on and the change is invisible. Fading it would smear
   through the flip instead of being hidden by it. */
.accum .tile {
  background: var(--tile-off);
  box-shadow: inset 0 0 0 1px var(--tile-off-edge);
}

.accum .tile.on {
  background: var(--tile-on);
  box-shadow: none;
}

/* ---- banked carries ---- */

/* Every carry off the left edge is worth 16, so counting them in binary makes
   the column the high end of the same number the accumulator holds the low end
   of. Read the column bottom to top, then the accumulator left to right, and it
   is one unbroken number.

   So it belongs to the accumulator, not to the board, and it hangs off that
   row: the 16s cell sits level with the total it continues, and the rest climb
   from there. Being outside .board also keeps it clear of the fade layer, which
   should not touch a running count.

   column-reverse puts the first cell, the 16s, at the bottom.

   Same tiles as the board: white is 1, dark is 0, and the unset places are
   drawn rather than left empty, for the same reason the board draws its zeros. */
.bank {
  position: absolute;
  left: 0;
  /* centres the 16s cell on the accumulator row rather than on its bottom edge */
  bottom: calc((var(--tile) - var(--bank-cell)) / 2);
  width: var(--gutter);
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: var(--bank-gap);
  pointer-events: none;
}

.bank i {
  width: var(--bank-cell);
  height: var(--bank-cell);
  border-radius: calc(var(--bank-cell) * .18);
  background: var(--tile-off);
  box-shadow: inset 0 0 0 1px var(--tile-off-edge);
  flex: none;
}

.bank i.on {
  background: var(--tile-on);
  box-shadow: none;
}

/* ---- rule ---- */

.rule {
  width: var(--grid-w);
  height: 2px;
  margin: calc(var(--tile) * .3) auto calc(var(--tile) * .3);
  border-radius: 2px;
  background: var(--tile-on);
  opacity: .8;
}

/* ---- input ---- */

.input {
  display: flex;
  gap: var(--gap);
  padding: 0 var(--gutter) calc(var(--tile) * .42 + env(safe-area-inset-bottom));
}

.itile {
  flex: 1;
  height: var(--tile);
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: var(--tile-off);
  box-shadow: inset 0 0 0 2px var(--tile-off-edge);
  cursor: pointer;
  /* Release only. The press itself must not transition, see .press. */
  transition: transform .13s cubic-bezier(.34, 1.3, .64, 1);
}

.itile.on {
  background: var(--tile-on);
  box-shadow: inset 0 0 0 2px var(--tile-on);
}

/* Driven by pointerdown, not :active. On touch the browser withholds :active
   while it decides whether the gesture is a scroll, which read as lag on the
   game's most repeated interaction. The class lands on the same event that
   toggles the bit, and transition:none makes the squash instant. Only the
   spring back on release is animated. */
.itile.press {
  transform: scale(.9);
  transition: none;
}

/* the tile that ended the run, held long enough to be seen before the card */
.itile.wrong {
  background: #c8434a;
  box-shadow: inset 0 0 0 2px #c8434a;
}

/* ---- overlays: summary, tutorial, start ---- */

.summary, .teach, .start, .paused, .failed, .debug {
  position: fixed;
  inset: 0;
  display: flex;
  overflow-y: auto;
  padding: 24px;
  background: rgba(2, 3, 4, .86);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  animation: fade .28s ease both;
}

.summary { z-index: 6; }
.failed  { z-index: 6; }
.paused  { z-index: 7; }
.teach   { z-index: 8; }
.start   { z-index: 9; }
/* Above everything, including the card it just staged. */
.debug   { z-index: 10; }

/* The menu sits over a live board, so its scrim has to let that board read.
   At the shared .86 the art, which peaks at 3.8% luminance, was reduced to
   nothing, and the decorative board behind might as well not have been drawn.
   More blur instead of more black: the lit tiles smear into a soft field that
   the type still sits cleanly on. */
.start {
  background: rgba(2, 3, 4, .58);
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
}

/* The rule above sets display on all of these, which beats the UA stylesheet's
   [hidden] { display: none }. So every overlay listed there must be listed here
   too, or it can never be hidden at all. Adding one to the first list and not
   this one is exactly how the debug menu shipped stuck open. */
.summary[hidden], .teach[hidden], .start[hidden],
.paused[hidden], .failed[hidden], .debug[hidden] { display: none; }

@keyframes fade { from { opacity: 0 } to { opacity: 1 } }

/* margin:auto rather than centring the flex container, which clips the top of
   anything taller than the screen instead of letting it scroll */
.card {
  margin: auto;
  text-align: center;
  animation: rise .34s cubic-bezier(.22, .61, .36, 1) both;
}

@keyframes rise {
  from { opacity: 0; transform: translateY(14px) scale(.97) }
  to   { opacity: 1; transform: none }
}

/* Beats while a number is climbing, then lands with one bigger pop. The tick is
   small on purpose: it has to survive being on screen for two full seconds. */
.score.ticking { animation: tick .17s ease-in-out infinite; }
.score.landed { animation: land .4s cubic-bezier(.3, 1.7, .5, 1); }

@keyframes tick {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.035); }
}

@keyframes land {
  0%   { transform: scale(1); }
  32%  { transform: scale(1.18); }
  100% { transform: scale(1); }
}

.score {
  display: block;
  font-family: var(--font-lcd);
  font-size: clamp(3.2rem, 18vw, 5.4rem);
  font-weight: 900;
  /* Doto is drawn on a dot grid with its own spacing built in, so the tightening
     that suited a proportional face now closes the gaps between the dots. */
  letter-spacing: .01em;
  line-height: .95;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* The score again, in the only notation the game actually speaks. Lit is 1,
   high bit on the left, so it reads the same direction as the digits above it
   and as the board.

   Sixteen is not a round number picked for looks, it is the real width. A board
   is 31 rows of at most 30, so the most you can sum is 930, worth 9300, and the
   time bonus caps at 12000. Top score is 21300. That needs fifteen bits, so
   sixteen is the smallest honest size with anything spare. It also means the
   strip can never overflow and quietly start lying. */
.bits {
  display: flex;
  justify-content: center;
  gap: 3px;
  margin: 13px 0 0;
}

.bits i {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  background: var(--tile-off);
  box-shadow: inset 0 0 0 1px var(--tile-off-edge);
}

/* Deliberately no transition. During the count these flip several times a
   second, and easing between the two colours would park the low bits on a
   permanent grey instead of showing them switch. Instant reads as a register
   running, which is what it is. */
.bits i.on {
  background: var(--tile-on);
  box-shadow: none;
}

/* Grouped in fours, the width of the board, so a nibble is one glance. */
.bits i:nth-child(4n + 1):not(:first-child) { margin-left: 8px; }

.bits.landed { animation: land .4s cubic-bezier(.3, 1.7, .5, 1); }

.score-label,
.stats span,
.best {
  font-size: 11px;
  letter-spacing: .22em;
  color: #6d7482;
}

.score-label { display: block; margin-top: 10px; }

.stats {
  display: flex;
  justify-content: center;
  gap: clamp(20px, 8vw, 40px);
  margin: 30px 0 26px;
}

.stats div { display: grid; gap: 7px; }

.stats b {
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--tile-on);
}

/* Counts down to nothing while the score counts up by the same amount, so the
   bonus is visibly poured into the total rather than just being announced. It
   stays on the card afterwards reading 0, since a line that removes itself is
   one more thing moving on a screen whose whole job is numbers settling. */
.tally {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 12px;
  margin-top: 16px;
}

.tally span {
  font-size: 11px;
  letter-spacing: .22em;
  color: #6d7482;
}

.tally b {
  font: 700 18px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.best { min-height: 13px; margin-top: 20px; }
.best.new { color: var(--accent); }

/* ---- focus ---- */

/* There was none of this at all, so a keyboard player had no idea where they
   were. :focus-visible rather than :focus, so a tap never lights anything up.
   Offset rather than inset, so it reads as a ring around the thing rather than
   a change to the thing. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius);
}

.itile:focus-visible { outline-offset: 2px; }
.pause:focus-visible,
.build:focus-visible { outline-offset: -2px; border-radius: 6px; }

/* ---- buttons ---- */

.btn {
  appearance: none;
  border: 0;
  border-radius: var(--radius);
  padding: 15px 30px;
  font: 600 12px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  letter-spacing: .16em;
  color: #b6bcc7;
  background: #1c212a;
  box-shadow: inset 0 0 0 1px #39414f;
  cursor: pointer;
  transition: transform .09s ease, color .12s ease, background .12s ease;
}

.btn.primary {
  color: #0b0d11;
  background: var(--accent);
  box-shadow: none;
}

.btn:active { transform: scale(.96); }

.col-btns { display: grid; gap: 12px; justify-content: center; margin-top: 34px; }
.col-btns .btn { min-width: 210px; }

/* Wraps so a third button drops to its own line rather than squeezing the
   other two. */
.row-btns {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
  margin-top: 34px;
}

/* ---- debug ---- */

/* Reached by a long press on the build stamp, which is only on screen on the
   menu anyway. Nothing here is hidden behind a flag: it stages screens the game
   can already produce, and the one destructive button says so. */
.debug-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-top: 22px;
}

.debug-grid .btn {
  width: 100%;
  padding-inline: 10px;
  font-size: 11px;
}

.debug-grid .btn[data-stage="wipe"] { color: #ff7a7a; }

/* ---- start ---- */

/* The break is the joke: the name says carry the one, and "One" is what gets
   carried onto the next line. Accent on the word that moved. */
/* inline-block so the block still centres on the card while its two lines share
   a left edge. Centred lines gave Carry and the One different starts, which is
   what stopped it reading as one mark. */
.logo {
  display: inline-block;
  text-align: left;
  margin: 0;
  font-family: var(--font-lcd);
  font-size: clamp(2.9rem, 15vw, 4.6rem);
  font-weight: 900;
  letter-spacing: .01em;
  line-height: 1;
}

.logo span { color: var(--accent); }

#start-best { margin-top: 14px; }
.bytes { margin-top: 6px; color: #565d69; }

/* ---- tutorial ---- */

/* Not a modal, and not floating over the board either. It is a flex item in
   the stage, so .board simply gets the height that is left. Overlaying it
   would have collided with the queue on a short screen; this way the rows sit
   below the lesson by construction, at any size.

   Solving the problem is what advances it. There is no button to acknowledge
   a lesson, because acknowledging one teaches nothing. */
.teach-bar {
  flex: none;
  padding: calc(var(--hud-h) + 18px) 0 16px;
}

.teach-bar[hidden] { display: none; }

.teach .card { max-width: 21rem; }

.teach-step {
  display: block;
  font-size: 11px;
  letter-spacing: .22em;
  color: var(--accent);
}

.teach-title {
  margin: 9px 0 0;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -.02em;
}

.teach-body {
  margin: 14px 0 0;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.6;
  color: #a7adb8;
  text-align: left;
}

/* The working, in mini board tiles so it reads as the same object as the board
   below. Same colours, same corner, same gap ratio, same rule, and the same
   order: addend on top, running total beneath it carrying the "+". */
.sum {
  display: grid;
  gap: calc(var(--sum-cell) * .16);   /* the board's own gap ratio */
  justify-content: center;
  margin-top: 16px;
  font-variant-numeric: tabular-nums;
}

.sum-line { display: flex; gap: calc(var(--sum-cell) * .16); align-items: center; }

/* leading slot keeps the +, the carry-out and the blanks in one column */
.sum-mark {
  width: 16px;
  font: 400 calc(var(--sum-cell) * .68)/1 ui-sans-serif, system-ui, sans-serif;
  color: #76797e;                      /* the board's own "+" */
  text-align: center;
}

.sum-mark.lit {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: calc(var(--sum-cell) * .5);
  color: var(--accent);
}

.sum-cell {
  width: var(--sum-cell);
  height: var(--sum-cell);
  border-radius: calc(var(--sum-cell) * .18);
  background: var(--tile-off);
  box-shadow: inset 0 0 0 1px var(--tile-off-edge);
}

.sum-cell.on { background: var(--tile-on); box-shadow: none; }

/* On the fail card the two bottom rows need no labels: green is the answer, and
   the one red tile is the tap that ended the run. Colour says which is which
   faster than a word beside it would. */
.sum-cell.ok { background: #5fd08a; box-shadow: none; }
.sum-cell.wrong { background: #c8434a; box-shadow: none; }

/* the carry row above the sum: a tick only where one actually lands */
.sum-carry {
  height: 13px;
  width: var(--sum-cell);
  font: 600 11px/13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--accent);
  text-align: center;
}

/* same part as the board's rule, at the example's scale */
.sum-rule {
  height: 2px;
  border-radius: 2px;
  background: var(--tile-on);
  opacity: .8;
  margin: 1px 0;
}

/* ---- particle overlay ---- */

/* above the summary, so the finishing burst plays over it */
#fx {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 11;
}

/* On a short screen the lesson bar and its worked example leave the board very
   little; trimming the example and the prose buys back a row rather than
   shrinking the board everywhere. */
@media (max-height: 660px) {
  :root { --sum-cell: 18px; }
  .teach-body { font-size: 13px; margin-top: 10px; }
  .teach-bar { padding-bottom: 10px; }
  .sum { margin-top: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .stack, .tile, .progress > i { transition-duration: .01ms; }
  .bits.landed { animation: none; }

}
