/* 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.3KB, 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, and the wordmark is now
   two names. The same game answers on two domains and calls itself Carry the
   One on one and Binary Tiles on the other, so the subset has to carry both
   plus digits. A character outside it renders blank with no error. */
@font-face {
  font-family: 'Doto';
  font-style: normal;
  font-weight: 900;
  font-display: block;
  src: url(data:font/woff2;base64,d09GMgABAAAAAATsAA4AAAAAL5AAAASVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYGYD9TVEFUQACBJhEICtQI2zgLNgABNgIkAzgEIAWEIAcgDAcbFDLIBB7I7u3N7H7Ohj4Zgf936O5g1YArqQSrVLn25n/3+1XRCCHaKx2N3WS4NxF/k29s4M20NJdI7qR879L+I3GInMNZbqJGbcIoZu/ND0UAFp1aiWjmpbSfNmW+sQ4byC3yLrBt/VbxleFCOt1SuV1SGk7pOAV1QOGC3O9j2LA5I3lINzqM69oOxjoxqJUfd71cdN0nv9uuHwInskHilwG92lYsXTLrRTraysqbBao+GjNNJ8QY69KnQrMUCs96MPIS42p0PCZ7alOJbn3FTTfGM5OlZOMjRAXV1NJMh9ZPbx7GjOvB5UGNn9ZdoB/oq/qiPqdP6zP6lD4J+uTdV0xmQlKPP55CFEkk4sJJHLE4iCSaGBIwP6E1UaTLHQURYl5EwvnKxPCdCk1SHosjgmgUkUQSQ+x6vAs3FlFEtFOBqPukH0dGEf3/N82YyTe4wBXh9Ee4LI/YshpWrVZq62ohVq92q775XqQWHrkwqPa8dsoiJGefqI4gFMGbaQEJjxa8qWVc9onkopcyLkdX4PFEtSIUQZ2x7tA5KqqnR2LUWeOU9zE/m1qSq95MC95YsjKMkj3J4fEVgdOT7AHh3ke4ipqffYLAt+hJFDD6SeO8Wi3ZK3DCrZxx2Vg6o5JReKGo8CgJXjjrhx6HJfzEOA9ftWAHVSu9V51dE15+zuiVQN76pqevKXgbaPO94sRo5J0xwl9j1GotfGvFWDoXDx94cdfFkuGDFg3Cdv3rddf8uWTrZMWsQNPjB5C+9jaKp272n0AJbDQV5WjzYikcJz/vTPk4WiN5Q6S3+dNEvA6xiraZeWdbTEXny226ClDOHywOqbfI5S/K5lkUcVClvtz59aNYq88p/SO7vkwWZ7k4m2eApFriwzf6TsDQ+UN9BMF4sug7xtNGVvwzj884POUqvISUvRDgnky8Y+qAOlT+2P4pbJ3ygcPdKP9QStLrebrIrHvaqol4kyhALs3LspJYJRdmhKt+bdC1WOV6ncEKUZjlnRnNnMNOKVhVwLHuOFYIH6wBeBMInymlf7B3SCWTlUq/VAXYKt778bfoAtQIomMAEG6g8PkgRYM5KpBd3wf5bdgi61+VMCkQ3QsXuh9/8URN+ueUxt9Lbd+Puk6tL/nR4/ZhbP8/yJVV04m9OnpG6sxq71NkPjZscmWOuZMuKboYgVi4H1ixwLkrtXRselNFiATcFpzUWRyoDF9quuqN5/i9twTxvLMkkTy1DAZwxNVEWzZSWWXZiafaimQMY+JicDNomIBYUsESWETmqdbjMIsQYXy0UE0zeVXxo8ooP28FdbRQS5DaenQTTczo9BGgCnVfM/U0MYGhDKWOeuisJUxdNf7OaioInLU3EWJIvb2RMma1TjZTx+C75OUOOxtpoiLX4G4MZwjDpnO2ZGKrdg4jwTZ/4ApShn2zj0Zk/G5kJnbNHTQSdls3RjCMEYxi8LZ/N7IpJ9Sak792GY3QWUmIQI7a6bQAKSfm7Ca60Y8O9WVUIC9rwc8QAtRRQ510epBWQtMxSp831zK0Tvr/WyKtu68+xgjZGXhT6PUXze0A) 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 ladder continued. Everything below was a loose hex somewhere in this file
     until it was named; the values are unchanged, so this bought a vocabulary and
     not a single pixel. Percentages are relative luminance, which is the axis the
     ladder above is spaced on, and the axis to keep using.

     Surfaces sit just clear of the page. Lines separate. Ink is text, five steps
     from barely-there to plain. Signals mean one thing each. */
  --surface: #1c212a;      /*  1.50%  a control at rest */
  --surface-dim: #14181f;  /*  0.90%  a control that is spent */
  --rail: #171b22;         /*  1.08%  the progress track */

  --line: #39414f;         /*  5.21%  the edge of a live control */
  --line-dim: #232936;     /*  2.21%  the edge of a spent one */

  --ink: #b6bcc7;          /* 50.04%  button text, the brightest ordinary text */
  --ink-soft: #a7adb8;     /* 41.56%  body copy, the tutorial */
  --ink-dim: #7d8593;      /* 23.24%  icons */
  --ink-quiet: #707785;    /* 18.33%  labels. The most used of these by far.
                              Was #6d7482 at 17.35%, which is 4.34:1 on this page
                              against a 4.5 bar for text this size. One step up the
                              ladder clears it at 4.53:1 and costs nothing: the next
                              token up sits at 23.24%. */
  --ink-faint: #4a515e;    /*  8.15%  text on a spent control */
  --on-accent: #0b0d11;    /*  0.40%  text on a lit button, not a grey at all */

  --good: #5fd08a;         /* 49.38%  a column that came out right */
  --bad: #c8434a;          /* 16.79%  one that did not */
  --bad-ink: #ff7a7a;      /* 36.58%  the word on a destructive control */
  /* Finishing already had a colour: the burst at the end of a board throws gold
     particles, hue 38 to 62. The win card was blue like everything else, so the
     one moment the game already celebrates in gold did not look like it. */
  --win: #ffc247;

  /* 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: 30px;
  --grid-w: 316px;
  --stage-w: 394px;

  /* 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);

  /* Three curves, by mass: heavy things overshoot less than light ones, and that
     is the only reason there is more than one. There were five, and two of them
     were within a rounding error of these. */
  --ease-out: cubic-bezier(.22, .61, .36, 1);   /* the standard exit */
  --ease-pop: cubic-bezier(.3, 1.7, .5, 1);     /* a number landing */
  --ease-rise: cubic-bezier(.18, 1.4, .4, 1);   /* a whole card arriving */
  --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: var(--rail);
  z-index: 3;
}

.progress > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width .26s var(--ease-out);
}

/* ---- 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);
}

/* line-height 1, not 0. A collapsed line box round a 14px icon gave 38px of
   height where 44 is the guidance, on a control in the worst corner of the screen
   for a thumb. */
.pause {
  display: none;
  border: 0;
  background: none;
  line-height: 1;
  padding: 15px 16px;
  cursor: pointer;
  line-height: 0;
}

.pause svg { width: 14px; height: 14px; fill: var(--ink-dim); 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;
  line-height: 1;
  padding: 14px 16px;
  cursor: pointer;
  line-height: 0;
}

.sound svg { width: 16px; height: 16px; fill: var(--ink-dim); stroke: var(--ink-dim); }
.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: var(--ink-quiet); stroke: var(--ink-quiet); }

/* 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: var(--ink);
}

/* 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: 16px 16px;
  border: 0;
  background: none;
  font: 400 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: .08em;
  color: var(--ink-quiet);
  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: var(--bad-ink); }

/* ---- 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 var(--ease-out);
}

.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;
}

/* Shown only on the lower operand, which is the row a written sum puts the
   operator beside. It sits in the leading column, not a gutter: that place is
   blank on every operand row, so the layout's dead space earns its keep. */
.plus {
  position: absolute;
  left: var(--gutter);
  width: var(--tile);
  height: var(--tile);
  display: grid;
  place-items: center;
  font: 400 calc(var(--tile) * .46)/1 ui-sans-serif, system-ui, sans-serif;
  color: var(--tile-off-edge);
  pointer-events: none;
}

/* The operands and running total are still four-bit values. Their leading
   place is a real layout slot but stays visually empty; the answer control
   supplies the visible 16 tile in that same column. */
.tile.reserved { visibility: hidden; }

/* 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 ---- */

/* ---- 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 var(--ease-pop);
}

.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. */
/* The fifth place is a value, not a command.
   It was styled as a control because it used to be one, back when a register
   sat above that column and pressing it meant "carry", an operation rather than
   a digit. With the register gone there is nothing above it to disagree with,
   so it is simply the 16s place of this row's answer. A plain tile. */

.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: var(--bad);
  box-shadow: inset 0 0 0 2px var(--bad);
}

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

/* The scrim used to belong to each menu, which meant handing over from one to
   the next tore it down and built it again: the outgoing menu vanished, the
   incoming one faded up from nothing, and for the length of that fade the only
   thing on screen was the board. Every menu-to-menu move had it, and the first
   paint was the same bug with the board playing both parts.

   So the scrim is its own thing now, owned by the layer rather than by any menu
   in it. It fades when the board is covered or uncovered, which is the only time
   the covering actually changes, and it holds still while the menus swap
   underneath it. What changes on a handover is the card, which is the only thing
   that is really changing. */
/* The radial lift is for the dead ground: measured on the win card, the band above
   the headline held a maximum luminance of 5 across 112px — not dark, uniform, and
   nothing in sixteen reference screens has a region like it. Barely a gradient; it
   just stops the top and bottom of the frame being nothing at all. */
.scrim {
  position: fixed;
  inset: 0;
  z-index: 5;
  background-image: radial-gradient(120% 65% at 50% 42%, rgba(120, 140, 190, .07), transparent 70%);
  pointer-events: none;
  opacity: 0;
  background: rgba(2, 3, 4, .86);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  transition: opacity .28s ease, background-color .28s ease, backdrop-filter .28s ease;
}

[data-overlay="1"] .scrim { opacity: 1; }

/* 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. Because this is one element being retuned
   rather than two being swapped, arriving at the menu from a summary or a loss
   eases between the two settings instead of jumping. */
[data-overlay-kind="start"] .scrim {
  background: rgba(2, 3, 4, .58);
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
}

/* The menus themselves are just the cards now, stacked above the shared scrim.
   No background and no entrance of their own: swapping one for another should
   read as the content changing, not as the whole screen being rebuilt. */
.summary, .teach, .start, .paused, .failed, .debug {
  position: fixed;
  inset: 0;
  display: flex;
  overflow-y: auto;
  padding: 24px;
}

.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. The shared scrim is below
   both, so this is the one menu that still has to bring its own. */
.debug   { z-index: 10; }
.debug {
  background: rgba(2, 3, 4, .86);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

/* 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; }


/* 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;
  /* A flex item will not shrink below its content, so without this a wide button
     row makes the card wider than the phone and the end of it is simply gone. */
  max-width: 100%;
  text-align: center;
  animation: rise .34s var(--ease-out) 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 var(--ease-pop); }

@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;
  /* Not --accent. That is the press-this colour, and on the loss card it was the
     fill of the REVIVE button 300px below an identically coloured number you
     cannot press. A score is a result, not a control. */
  color: var(--tile-on);
}

/* 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
   The most a board can sum to is 705, not the 930 that 31 rows of at most 30
   suggests, because the accumulator drops after every carry and cannot sit at
   15. So the top score is 7050 plus a 12000 bonus, 19050, which needs fifteen
   bits. Sixteen because the strip groups in fours and fifteen would leave a
   group of three, which reads as broken. A cell that never lights costs
   nothing; a short group costs the shape. */
.bits {
  display: flex;
  justify-content: center;
  gap: 3px;
  margin: 12px 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 var(--ease-pop); }

/* Carries the result to a screen reader without putting it on screen twice. Not
   display:none, which would take it out of the accessibility tree along with
   everything else. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- the win card ---- */

/* Grouped rather than stacked. Seven elements at near-uniform 10 to 16px gaps
   gave the eye nothing to chunk, and the score, the one thing anyone actually
   reads, had less room around it than the buttons did. The card came to 369px of
   content floating in an 896px screen with 527px of it empty, which is the
   cramped feeling exactly: not short of space, just refusing to use any.

   Three groups now, with the gaps carrying the structure. What happened, what it
   was worth, what to do next. Tight within a group, generous between them, and
   the space taken from the emptiness that was already there. */
/* The card had no width at all, so it was a flex item shrink-wrapped to its
   widest child — which was the button row. Its width was two buttons plus a gap,
   299px, at every viewport from a phone to a desktop. Nothing was aligned to
   anything because nothing had an edge. */
.summary .card { width: min(100%, 26rem); }
.summary .row-btns .btn { flex: 1; }

.summary .win-title { margin-bottom: 24px; }
.summary .bits { margin-top: 24px; }
.summary .best { margin-top: 12px; }

.summary .stats { margin: 24px auto 0; }

.summary .row-btns { margin-top: 48px; }




/* Everything gold, so a finished board does not arrive wearing the same blue as
   the menu, the pause and the loss. The score keeps its size and face; only the
   colour says which of the two endings this is. */
.summary .score {
  color: var(--win);
  /* Enough to read as lit rather than printed, and no more: the face is a dot
     grid and a heavy glow closes the gaps between the dots. */
  text-shadow: 0 0 28px rgba(255, 194, 71, .45);
}

.summary .tally b,
.summary .best.new { color: var(--win); }

.win-title {
  display: block;
  margin-bottom: 12px;
  /* Was 12px, identical to the button labels, so the one moment the game
     celebrates was set at the same size as the word MENU. Results screens in
     shipped games run their headline between 6% and 14% of frame width; this was
     1.6%. It does not need their cartoon shout, but it has to outrank a button. */
  /* Clamped because at a flat 28px the fourteen tracked characters wrapped to
     two lines on every phone at 414px and below. */
  font: 700 clamp(18px, 5.6vw, 28px)/1.1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  letter-spacing: .16em;
  text-indent: .16em;   /* the tracking is trailing space; this recentres it */
  color: var(--win);
  animation: win-in .5s var(--ease-out) both;
}

/* The `to` frame has to match the rule's own letter-spacing. It said .3em while
   the rule said .16em, and `both` retains the last frame, so the animated title
   rendered at .3em and only a reduced-motion user ever saw .16em. Two trackings
   for two users, and the text-indent that recentres the trailing space was
   compensating for the one nobody got. */
@keyframes win-in {
  from { opacity: 0; transform: translateY(-8px); letter-spacing: .3em }
  to   { opacity: 1; transform: none; letter-spacing: .16em }
}

/* A board takes two minutes and most attempts do not survive one. The card that
   says you got through it should not glide in the way the pause menu does. */
.summary .card { animation: win-rise .52s var(--ease-rise) both; }

@keyframes win-rise {
  from { opacity: 0; transform: translateY(22px) scale(.9) }
  to   { opacity: 1; transform: none }
}



.score-label,
.stats span,
.best {
  font-size: 11px;
  letter-spacing: .22em;
  color: var(--ink-quiet);
}

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

/* Stacked rows, label left and value right, at the card's full width. Three
   centred columns is the one layout that appears in none of sixteen shipped
   results screens: they all stack, because a portrait phone is tall and narrow
   and a row of columns is neither. Centred columns also made the figures a 200px
   strip in the middle of a 500px card, and left the value "1" floating over a
   55px label, because each column was sized by its own text.

   Label first in the markup too, so a screen reader stops saying "372, SUM". */
/* Held to a column rather than run to the card's edges.
   A label at one edge and its value at the other reads as a pair only while
   there is not too much between them, and these rows were taking whatever width
   the card happened to be. On the loss card at 390px that is 342px, while every
   other thing on it sits in a column half that wide: the score, the bit strip,
   the working, and the buttons, which wrap and centre at about 230. The
   stats were the only element touching the edges, so they read as a table
   someone had left in rather than as part of the card.

   240 is not a new number. It is exactly the width of the pause card, whose
   stats are the one set already reading correctly, so the pause card is
   untouched by this and the other two catch up with it. */
.stats {
  display: grid;
  gap: 12px;
  max-width: 240px;
  margin: 32px auto 0;
  text-align: left;
}

.stats div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

/* The bonus is the first thing added to the total above it, so it leads the list
   and takes a rule to separate it from the run's own figures. */
.stats .tally { padding-bottom: 12px; border-bottom: 1px solid var(--line-dim); }

.stats b {
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  /* Was --tile-on at 90.5% luminance, against a score at 60.3%. The three
     throwaway figures were half again brighter than the number the card exists
     to show: the score won on size and lost on brightness, which is the axis that
     survives a phone at arm's length in daylight. They are evidence, not the
     result. */
  color: var(--ink);
}

/* 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 span {
  font-size: 11px;
  letter-spacing: .22em;
  color: var(--ink-quiet);
}

.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: 16px; }
.best.new { color: var(--accent); }

/* Three states, because there are three answers to "can this browser install",
   and only one of them arrives in time for the first paint.

   Default is out of the flow entirely: a browser that can never offer an install
   should not pay a button's height in empty space for one it will never get. */
.btn.install { display: none; }

/* Chromium can offer, but tells us so after the paint. The button takes its grid
   row and its gap from the start and simply has no ink in it, so the event lands
   into a space already the right size and nothing moves. */
.install-maybe .btn.install { display: block; visibility: hidden; }
.install-maybe .btn.install.ready { visibility: visible; }

/* Safari has no event at all, so the answer is already final. */
.install-apple .btn.install { display: block; }

/* Shown only where there is no prompt to open, which in practice means Safari.
   One line, no diagram: the share control is somewhere different on every iOS
   version and describing it precisely would age worse than not describing it. */
.install-note {
  margin: 8px 0 0;
  font-size: 12px;
  letter-spacing: .06em;
  color: var(--ink-quiet);
}

.install-note[hidden] { display: none; }

/* ---- 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);
}

/* The 16 on its way out.
   Pinned lit for the whole flight. solve() clears the answer and re-renders the
   input a frame after the lift starts, so without this the block went dark
   almost the moment it began rising and what you saw leave was an empty tile.
   It lights, it holds, then it goes.

   Last in the file so it beats .itile and .itile.on on order rather than on a
   specificity fight. */
.itile.lifting {
  background: var(--tile-on);
  /* Low, so the squash compresses onto the row instead of shrinking in place. */
  transform-origin: 50% 88%;
  z-index: 2;
  /* --carry-lift is written by game.js from CARRY_LIFT_MS, so the light and the
     movement cannot drift apart. The fallback is only for the first paint. */
  animation: carry-glow var(--carry-lift, 370ms) linear both;
}

/* The glow used to be one static value for the whole trip, which lit the block
   evenly whether it was crouched or leaving. It is the launch now: dim while it
   loads, blown out at the moment it goes, gone by the time it clears the frame.
   Light is the cheapest way to say that something happened, and this is the only
   thing left saying a carry did. */
@keyframes carry-glow {
  0%   { box-shadow: 0 0 calc(var(--tile) * .2) rgba(244, 244, 245, .3); }
  24%  { box-shadow: 0 0 calc(var(--tile) * .5) rgba(244, 244, 245, .7); }
  40%  { box-shadow: 0 0 calc(var(--tile) * .95) rgba(255, 255, 255, .95); }
  100% { box-shadow: 0 0 calc(var(--tile) * .3) rgba(244, 244, 245, 0); }
}

.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: 16px 32px;
  font: 600 12px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  letter-spacing: .16em;
  color: var(--ink);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--line);
  cursor: pointer;
  transition: transform .09s ease, color .12s ease, background .12s ease;
}

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

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

/* Spent, not gone. A revive that vanishes once used takes the evidence of its own
   existence with it, and the card changes height between a first loss and a
   second for no reason the player can see. Greyed out it keeps saying you had one
   and it has been spent, and the layout stops moving. */
.btn:disabled {
  color: var(--ink-faint);
  background: var(--surface-dim);
  box-shadow: inset 0 0 0 1px var(--line-dim);
  cursor: default;
}

.btn:disabled:active { transform: none; }

.col-btns { display: grid; gap: 12px; justify-content: center; margin-top: 32px; }
.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: 32px;
}

/* REVIVE gets its own line above the row. Four buttons abreast do not fit a
   phone, and the line is the honest layout anyway: it is the offer, and the three
   below are ways of declining it. Same width as the menu's primary buttons, which
   is the established size for the one thing on a card worth pressing. Full width
   was reaching for prominence it already has from being alone and coloured. */
.btn.revive {
  display: block;
  min-width: 210px;
  margin: 32px auto 0;
}

/* The row supplies its own top margin on every other card; here the revive has
   already opened the gap, so what is left between them is the button gap. */
.btn.revive + .row-btns { margin-top: 12px; }

/* ---- 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: 8px;
  margin-top: 24px;
}

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

.debug-grid .btn[data-stage="wipe"] { color: var(--bad-ink); }

/* ---- 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); }

/* No tracking on the second line, and none wanted.
   Doto is monospaced: every glyph advances the same, measured at 54.98px per
   character at 100px. So "Binary" over "Tiles" already lines up column for
   column, the second line simply being one column shorter, which is what a
   monospaced face is for. An earlier version tracked the short line out to make
   both edges flush, on the strength of a measurement that had read the ink
   bounding box rather than the advance width. That broke the grid to fix a
   difference that was not there. */

#start-best { margin-top: 12px; }
.bytes { margin-top: 4px; color: var(--ink-quiet); }

/* ---- 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) + 16px) 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: 8px 0 0;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -.02em;
}

.teach-body {
  margin: 12px 0 0;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--ink-soft);
  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 + 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: var(--ink-quiet);                      /* the board's own "+" */
  text-align: center;
}

.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; }

/* Mirrors the empty leading operand slot on the full-size board. */
.sum-cell.reserved { visibility: hidden; }

/* 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: var(--good); box-shadow: none; }
/* Red fill AND a bright ring. The fill alone was colour-only information, and
   the colour inverts: --bad sits at 16.79% luminance, nearer an unlit tile at
   2.91% (2.75:1) than a lit one at 90.53% (4.38:1). You only ever die by lighting
   a bit the answer wants dark, so the marked cell is always one you turned ON —
   and a protanope reads it as off, sees a row that matches, and is told they were
   right. The ring says "flagged" at any colour vision. */
.sum-cell.wrong {
  background: var(--bad);
  box-shadow: inset 0 0 0 2px var(--tile-on);
}

/* 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: 8px; }
  .teach-bar { padding-bottom: 8px; }
  .sum { margin-top: 12px; }
}

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

  /* Was only .bits.landed and the win card. That exempted a strip of 10px squares
     while leaving the 74px number beside it scaling at 5.9Hz, and exempted the
     card players see least while leaving the loss card — the one most runs end on
     — still rising. `.card` covers all six overlays. */
  .bits.landed,
  .score.ticking,
  .score.landed,
  .win-title,
  .card { animation: none; }
}
