/* Ruta Robot — all the styles for the game.

   This is the "Rovi" concept skin (`assets/rovi-concept.png`): a voxel world, dark navy
   chrome, a cream and cyan robot, grass and stone blocks.

   THE BOARD IS NOT STYLED HERE. It is a single canvas — the ground, the walls, the flag,
   the batteries and the robot are all boxes drawn under one camera, in src/voxel.js. The
   only rule that matters about it is that there is exactly one camera; the way that gets
   broken is by drawing "just this one thing" some other way, in CSS, on top.

   What is left in this file is the chrome around the board, and it fakes depth the way a
   flat interface can: a light top edge and a dark bottom edge on every panel, chip and
   button, which is what --face-lit and --face-dark are for.

   COLOURS ARE THE ONE PLACE THIS BRANCH BREAKS THE HOUSE RULE. The rest of the repo
   resolves every colour through the `--rb-*` tokens copied from ~/marca. The concept
   arrives with its own palette, sampled from the artwork below, and it is not brand
   material yet — so it lives here as `--rovi-*` instead of being written into the manual
   on the strength of one image. If the concept is adopted, these eight swatches move to
   ~/marca/assets/tokens.css as their own family and this block turns back into aliases.

   Every pair below was measured; the ones that matter:
     hueso on noche 8.66:1 · blanco on pizarra 11.8:1 · noche on sol 6.33:1
     noche on agua-claro 5.38:1 · noche on pasto 4.38:1 · cian on noche 8.88:1
   The raw concept blue (#2f91b6) only reaches 3.52:1 under the block text, which is why
   the blocks are painted in its lighter face and outlined in the raw one.

   The page has one fixed appearance and does not follow prefers-color-scheme. It is
   played on a room full of laptops and projected at the front, and a child whose machine
   is in dark mode must see the same board as everyone else. That is why nothing here
   uses the semantic --rb-fondo / --rb-texto roles, which flip. */

/* Nunito, self-hosted: rounded and made for children's reading, and the closest thing
   to how a Scratch block feels. It is a variable font, so this one 39 KB file covers
   every weight. Only the latin subset ships — the unicode-range below sends anything
   outside it to the fallback stack instead of downloading more.

   Self-hosted and not from a CDN on purpose: this game has to open from a pendrive in a
   school with no connection. Licence in assets/fonts/OFL.txt (SIL OFL). */
@font-face {
  font-family: 'Nunito';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url('assets/fonts/nunito-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}

/* Silkscreen, the concept's pixel type — it is all but the specimen printed on the
   concept sheet. It is for names and labels only: titles, buttons, level numbers, the
   counter. Sentences a child has to read stay in Nunito, because the concept sets its
   own body copy in a rounded sans for the same reason and a pixel font costs an eight
   year old real reading speed over a full line.

   Chosen over Pixelify Sans, which was tried first and looks closer to the concept's
   title lettering, for one reason: its 5 is shaped like an S and its 7 like a 1. The
   numbers on this page are the level a child is asked to open and the count inside a
   `repetir`, and neither can be a guess. Silkscreen's lowercase is small caps, so
   everything set in it comes out in capitals — which is what the concept does anyway.

   Same latin subset treatment as Nunito, 7 KB, same offline reason. */
@font-face {
  font-family: 'Silkscreen';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('assets/fonts/silkscreen-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}

:root {
  /* The eight swatches of the concept's "paleta de colores", sampled from the sheet. */
  --rovi-hueso: #e2d4bb;      /* the robot's shell */
  --rovi-piedra: #878782;     /* stone */
  --rovi-noche: #233542;      /* the darkest navy — the page itself */
  --rovi-pizarra: #263a48;    /* the navy the panels are cut from */
  --rovi-agua: #2f91b6;       /* the concept blue */
  --rovi-sol: #efab17;        /* amber: batteries, stars, rewards */
  --rovi-tierra: #a36a29;     /* dirt */
  --rovi-pasto: #75a63b;      /* grass */

  /* Read off the artwork rather than the swatch row: the sky behind the world, the cyan
     of Rovi's eyes and antenna, and the lighter face of each voxel block. */
  --rovi-cielo: #4789c1;
  --rovi-cian: #54ecf8;
  --rovi-agua-claro: #58b4d8;
  --rovi-pasto-claro: #8cbf4c;
  /* The concept has no orange between its amber and its brown, and `repetir` needs one:
     painting it in the amber would make the loop block the same colour as a battery, a
     star and a finished level, and yellow on this page means reward. Mixed rather than
     invented, so it stays inside the palette. Pizarra on it measures 4.74:1. */
  --rovi-naranja: color-mix(in srgb, var(--rovi-sol) 62%, var(--rovi-tierra));
  --rovi-hueso-sombra: #c3b394;
  --rovi-tinta: #16232c;      /* the screen Rovi's face is drawn on */
  --rovi-borde: #3c5b70;      /* the lit edge of a navy panel */
  --rovi-humo: #b9c6d1;       /* secondary text on navy — 6.77:1 */

  --game-font-block: 'Nunito', var(--rb-font-titulo);
  --game-font-pixel: 'Silkscreen', var(--rb-font-titulo);

  --game-ink: var(--rovi-noche);        /* text that sits on a light or coloured face */
  --game-paper: var(--rovi-hueso);      /* text that sits on navy */
  --game-muted: var(--rovi-humo);

  --game-page: var(--rovi-noche);
  --game-surface: var(--rovi-pizarra);
  --game-well: #1c2c37;                 /* pizarra pushed further down, for inner wells */
  --game-line: var(--rovi-borde);
  --game-chrome: var(--rovi-noche);
  --game-chip: #32536a;

  --game-move: var(--rovi-agua-claro);  /* movement blocks */
  --game-loop: var(--rovi-naranja);     /* repeat blocks */

  /* Each piece is outlined in its own colour taken down towards the navy. Derived with
     color-mix instead of two more hand-written values, so the outline follows the block
     colour. A browser too old for color-mix simply draws no outline, which is a fine
     thing to lose. */
  --edge-move: color-mix(in srgb, var(--rovi-agua) 76%, var(--rovi-noche));
  --edge-loop: color-mix(in srgb, var(--rovi-naranja) 70%, var(--rovi-noche));

  /* The ground, the walls and everything else on the board are painted by voxel.js,
     which reads the --rovi-* tokens above directly. They have no --game-* alias because
     nothing in this file paints them any more — but they are still in use, and deleting
     one because CSS never mentions it would blank part of the world. */
  --game-spark: var(--rovi-sol);
  --game-go: var(--rovi-pasto);

  --game-topbar: 3.5rem;
  --game-shadow: 0 2px 0 rgba(0, 0, 0, .22), 0 10px 24px rgba(0, 0, 0, .28);

  /* A voxel block is drawn, not modelled: a lit top edge and a shaded bottom edge on a
     flat face is all it takes to read as a cube from above. Every block on this page
     uses these two, at whatever depth suits its size. */
  --face-lit: rgba(255, 255, 255, .22);
  --face-dark: rgba(0, 0, 0, .26);

  /* Puzzle-piece geometry. `notch-top` and `notch-bottom` are the left edge of the
     bump; they differ only on a `repetir` head, whose bump has to line up with the
     blocks indented inside it. */
  --notch-h: 8px;
  --notch-top: 14px;
  --notch-bottom: 14px;
  --piece-radius: 10px;

  /* `polygon()` draws straight lines only, so a rounded corner is three extra points
     along the quarter circle — at 22.5°, 45° and 67.5°. These are the offsets from the
     corner, as fractions of the radius: 1-cos(a) and 1-sin(a). Used in pairs, one as x
     and the other as y, they trace an arc smooth enough that nobody sees the seams. */
  --r1: calc(var(--piece-radius) * .0761);
  --r2: calc(var(--piece-radius) * .2929);
  --r3: calc(var(--piece-radius) * .6173);
}

* { box-sizing: border-box; }

html, body { height: 100%; margin: 0; }

/* The page is the night side of the concept sheet: navy, with the faintest voxel grid
   ruled over it so the background reads as a world made of cubes rather than as a dark
   screen. */
body {
  display: flex;
  flex-direction: column;
  background-color: var(--game-page);
  background-image:
    linear-gradient(rgba(255, 255, 255, .022) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, .022) 1px, transparent 1px);
  background-size: 2rem 2rem, 2rem 2rem;
  color: var(--game-paper);
  font-family: var(--rb-font-texto);
  font-size: var(--rb-text-base);
  line-height: var(--rb-leading-texto);
  -webkit-font-smoothing: antialiased;
}

h2, h3 {
  font-family: var(--rb-font-titulo);
  line-height: var(--rb-leading-titulo);
  margin: 0;
}

/* ---------- top bar ---------- */

.topbar {
  flex: none;
  height: var(--game-topbar);
  display: flex;
  align-items: center;
  gap: var(--rb-space-3);
  padding: 0 var(--rb-space-3);
  background: var(--game-chrome);
  border-bottom: 3px solid var(--rovi-borde);
  color: var(--game-paper);
}

.brand { display: flex; align-items: center; gap: var(--rb-space-1); }

.brand-mark {
  width: 2rem;
  height: 2rem;
  --game-ink: var(--rovi-tinta);
  --game-paper: var(--rovi-cian);
}

.brand-name {
  font-family: var(--game-font-pixel);
  font-size: var(--rb-text-xl);
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--rb-blanco);
}

.levels { display: flex; flex-wrap: wrap; gap: .375rem; margin-left: auto; }

/* Every button on this page is a little block: flat face, lit top, and a hard edge
   underneath that it sinks into when pressed. No blur — a voxel has no soft shadow.

   The number is in Nunito and not in the pixel face, and that is the rule for the whole
   page: SILKSCREEN NEVER SETS A DIGIT THE CHILD HAS TO ACT ON. Its 4 has an open top
   that reads as a blob and its 5 reads as an S. "Abre el nivel 4" has to be the same 4
   the child sees. The same applies to the count inside a `repetir` and to the block
   counter; in a level's own name the number is incidental, so the chip keeps its pixel
   type. */
.level-btn {
  width: 2rem;
  height: 2rem;
  border: 0;
  border-radius: 5px;
  background: var(--game-chip);
  box-shadow: inset 0 2px 0 var(--face-lit), 0 3px 0 rgba(0, 0, 0, .35);
  color: var(--rb-blanco);
  font-family: var(--game-font-block);
  font-size: var(--rb-text-base);
  font-weight: 800;
  cursor: pointer;
}

.level-btn:hover:not(:disabled) { filter: brightness(1.2); }
.level-btn:active:not(:disabled) { transform: translateY(2px); box-shadow: inset 0 2px 0 var(--face-lit), 0 1px 0 rgba(0, 0, 0, .35); }
.level-btn.is-done { background: var(--rovi-agua); color: var(--rb-blanco); }
/* Yellow means reward, so it belongs to the levels finished with all three batteries. */
.level-btn.is-perfect { background: var(--game-spark); color: var(--game-ink); }
.level-btn.is-current { outline: 3px solid var(--rovi-cian); outline-offset: 2px; }
.level-btn.is-locked { opacity: .35; cursor: not-allowed; }

/* ---------- layout ---------- */

.stage {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 13rem 20rem 1fr;
  gap: var(--rb-space-2);
  padding: var(--rb-space-2);
}

.panel {
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--rb-space-1);
  background: var(--game-surface);
  border: 2px solid var(--game-line);
  border-radius: 14px;
  padding: var(--rb-space-2);
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .07), var(--game-shadow);
}

.panel-title {
  display: flex;
  align-items: center;
  gap: var(--rb-space-1);
  font-size: var(--rb-text-lg);
}

/* The concept labels every panel with a small plate sitting on it. Here it is a rounded
   navy tile, one shade up from the panel, with the name in pixel type. It never wraps:
   a two-line plate stops looking like a label. */
.chip {
  white-space: nowrap;
  padding: .25rem .7rem .3rem;
  border-radius: 8px;
  background: var(--game-chip);
  box-shadow: inset 0 2px 0 var(--face-lit), 0 2px 0 rgba(0, 0, 0, .3);
  color: var(--rb-blanco);
  font-family: var(--game-font-pixel);
  font-size: var(--rb-text-base);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.counter {
  margin-left: auto;
  white-space: nowrap;
  font-family: var(--game-font-block);   /* it counts blocks: see .level-btn */
  font-size: var(--rb-text-sm);
  font-weight: 700;
  color: var(--game-muted);
}

.counter.is-full { color: var(--game-loop); font-weight: 700; }

/* Battery pips beside the level name: three empty sockets that fill as they are
   collected. They have to read as "three to find" from across the room, so the empty
   state is a visible socket, not a faded icon. */
.batteries { margin-left: auto; display: flex; gap: .375rem; }

.pip-slot {
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  border-radius: 6px;
  background: var(--game-well);
  box-shadow: inset 0 2px 3px rgba(0, 0, 0, .4);
  transition: background .25s ease, transform .25s ease;
}

/* The empty pip is a pale grey battery, not a dimmed yellow one: on a dark socket a
   faded icon disappears altogether, and the whole point of the three sockets is that a
   child can see from across the room how many are still out there. */
.pip-slot .pip {
  width: 1.5rem;
  height: 1.5rem;
  display: block;
  opacity: .8;
  filter: grayscale(1) brightness(1.7);
  transition: opacity .25s ease, filter .25s ease;
}

.pip-slot.is-on {
  background: color-mix(in srgb, var(--game-spark) 24%, var(--game-well));
  box-shadow: inset 0 2px 0 var(--face-lit), 0 0 10px rgba(239, 171, 23, .45);
  transform: scale(1.06);
}

.pip-slot.is-on .pip { opacity: 1; filter: none; }

/* The pips are the last drawing of a battery left in the page: on the board it is three
   boxes under the camera now, but the three sockets beside the level name are flat SVG
   and need their own fills. */
.pip .battery-body { fill: var(--game-spark); }
.pip .battery-shade { fill: color-mix(in srgb, var(--game-spark) 72%, var(--rovi-tierra)); }
.pip .battery-cap, .pip .battery-bolt { fill: var(--rovi-tinta); }

/* ---------- blocks ---------- */

.palette { display: flex; flex-direction: column; gap: .625rem; }

/* Palette pieces are loose parts, not a stack: they keep their bump but not the
   negative margin that makes blocks in the program click into each other. */
.palette .block { margin-bottom: 0; }

/* Every block is cut to a puzzle-piece silhouette: a notch on top and a bump below,
   so a stack reads as pieces that click together the way Scratch does. The bump lives
   inside the box (that is what the extra bottom padding is for) and the negative
   bottom margin pulls the next block up until its notch swallows it.

   The concept is voxel and these are not, on purpose: the children move on to Scratch
   after this workshop, and the shape and the two family colours are what they carry
   over. The skin changes around them.

   The shape and its colour live on the `.piece` layers behind the text, not on the block
   itself. A `border` cannot work here — clip-path cuts it off along with everything else
   — so the outline is four chained drop-shadows that follow the cut silhouette.
   `z-index: 0` gives the block its own stacking context so the piece stays above the
   panel background instead of falling behind it. */
.block {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  padding: .5rem .625rem calc(.5rem + var(--notch-h));
  margin-bottom: calc(var(--notch-h) * -1);
  border: 0;
  background: transparent;
  --piece-color: var(--game-move);
  --piece-edge: var(--edge-move);
  color: var(--game-ink);
  font-family: var(--game-font-block);
  font-size: var(--rb-text-base);
  font-weight: 800;
  line-height: 1.2;
  text-align: left;
  cursor: grab;
  touch-action: none;              /* pointer events drive the drag, not scrolling */
  user-select: none;
}

/* The outline layer. CSS applies clip-path *after* filter, so these have to be two
   elements: put both on one and the silhouette clips away its own outline. */
.piece {
  position: absolute;
  inset: 0;
  z-index: -1;
  filter:
    drop-shadow(1.5px 0 0 var(--piece-edge))
    drop-shadow(-1.5px 0 0 var(--piece-edge))
    drop-shadow(0 1.5px 0 var(--piece-edge))
    drop-shadow(0 -1.5px 0 var(--piece-edge));
}

/* The lit top and shaded bottom that every other block on the page has. It is a
   gradient rather than two insets because clip-path would cut an inset shadow off at
   the silhouette's straight bounding box. */
.piece-face {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      var(--face-lit) 0 3px,
      transparent 3px calc(100% - var(--notch-h) - 4px),
      var(--face-dark) calc(100% - var(--notch-h) - 4px) 100%),
    var(--piece-color);
}

/* The head and the foot are two ends of one C, so neither draws the edge that faces the
   body — otherwise a dark line cuts across the middle of the piece. */
.block-head > .piece {
  filter:
    drop-shadow(1.5px 0 0 var(--piece-edge))
    drop-shadow(-1.5px 0 0 var(--piece-edge))
    drop-shadow(0 -1.5px 0 var(--piece-edge));
}

.block-foot > .piece {
  filter:
    drop-shadow(1.5px 0 0 var(--piece-edge))
    drop-shadow(-1.5px 0 0 var(--piece-edge))
    drop-shadow(0 1.5px 0 var(--piece-edge));
}

/* The head is the top of the C, so it keeps the lit edge and drops the shaded one; the
   foot is the bottom and does the opposite. */
.block-head > .piece > .piece-face {
  background: linear-gradient(var(--face-lit) 0 3px, transparent 3px) var(--piece-color);
}

.block-foot > .piece > .piece-face {
  background:
    linear-gradient(
      transparent 0 calc(100% - var(--notch-h) - 4px),
      var(--face-dark) calc(100% - var(--notch-h) - 4px) 100%)
    var(--piece-color);
}

.block > .piece > .piece-face {
  clip-path: polygon(
    /* top-left corner */
    0 var(--piece-radius),
    var(--r1) var(--r3),
    var(--r2) var(--r2),
    var(--r3) var(--r1),
    var(--piece-radius) 0,
    /* the notch this block's neighbour above drops its bump into */
    var(--notch-top) 0,
    calc(var(--notch-top) + 4px) var(--notch-h),
    calc(var(--notch-top) + 18px) var(--notch-h),
    calc(var(--notch-top) + 22px) 0,
    /* top-right corner */
    calc(100% - var(--piece-radius)) 0,
    calc(100% - var(--r3)) var(--r1),
    calc(100% - var(--r2)) var(--r2),
    calc(100% - var(--r1)) var(--r3),
    100% var(--piece-radius),
    /* bottom-right corner */
    100% calc(100% - var(--notch-h) - var(--piece-radius)),
    calc(100% - var(--r1)) calc(100% - var(--notch-h) - var(--r3)),
    calc(100% - var(--r2)) calc(100% - var(--notch-h) - var(--r2)),
    calc(100% - var(--r3)) calc(100% - var(--notch-h) - var(--r1)),
    calc(100% - var(--piece-radius)) calc(100% - var(--notch-h)),
    /* the bump */
    calc(var(--notch-bottom) + 22px) calc(100% - var(--notch-h)),
    calc(var(--notch-bottom) + 18px) 100%,
    calc(var(--notch-bottom) + 4px) 100%,
    var(--notch-bottom) calc(100% - var(--notch-h)),
    /* bottom-left corner */
    var(--piece-radius) calc(100% - var(--notch-h)),
    var(--r3) calc(100% - var(--notch-h) - var(--r1)),
    var(--r2) calc(100% - var(--notch-h) - var(--r2)),
    var(--r1) calc(100% - var(--notch-h) - var(--r3)),
    0 calc(100% - var(--notch-h) - var(--piece-radius))
  );
}

.block-loop { --piece-color: var(--game-loop); --piece-edge: var(--edge-loop); }
.block:active { cursor: grabbing; }
.block.is-source:hover { filter: brightness(1.06); }

.block-icon {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.block-label { flex: 1; }
.block-label-tail { flex: none; }

.block-remove {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(18, 24, 29, .22);
  color: inherit;
  font-size: var(--rb-text-sm);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
}

.block:hover > .block-remove,
.block:hover > .block-head > .block-remove { opacity: 1; }
.block-remove:hover { background: rgba(18, 24, 29, .38); }

/* `repeat` is drawn as a head, an indented body and a foot — the C shape of Scratch.
   The head's bump is pushed right so it lands over the blocks nested inside. */
.block-container {
  display: block;
  padding: 0;
  margin-bottom: calc(var(--notch-h) * -1);
  background: transparent;
}

/* The C is three pieces, so the shape layers belong to the head and the foot. The
   container itself draws nothing — and must not swallow their `absolute` positioning. */
.block-container > .piece { display: none; }

.block-head {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem .625rem;
  background: transparent;
  font-family: var(--game-font-block);
  font-weight: 800;
}

.block-head > .piece > .piece-face {
  clip-path: polygon(
    0 var(--piece-radius),
    var(--r1) var(--r3),
    var(--r2) var(--r2),
    var(--r3) var(--r1),
    var(--piece-radius) 0,
    var(--notch-top) 0,
    calc(var(--notch-top) + 4px) var(--notch-h),
    calc(var(--notch-top) + 18px) var(--notch-h),
    calc(var(--notch-top) + 22px) 0,
    calc(100% - var(--piece-radius)) 0,
    calc(100% - var(--r3)) var(--r1),
    calc(100% - var(--r2)) var(--r2),
    calc(100% - var(--r1)) var(--r3),
    100% var(--piece-radius),
    100% 100%,
    0 100%
  );
}

/* The body is a plain rectangle, so here the outline can be a real border. Only the
   sides: the head and the foot close the shape above and below. */
.block-body {
  min-height: 2.5rem;
  padding: .5rem .25rem .25rem 1rem;
  background: var(--piece-color);
  border-left: 1.5px solid var(--piece-edge);
  border-right: 1.5px solid var(--piece-edge);
  display: flex;
  flex-direction: column;
}

.block-foot {
  position: relative;
  z-index: 0;
  height: 1.125rem;
  background: transparent;
}

.block-foot > .piece > .piece-face {
  clip-path: polygon(
    0 0,
    100% 0,
    100% calc(100% - var(--notch-h) - var(--piece-radius)),
    calc(100% - var(--r1)) calc(100% - var(--notch-h) - var(--r3)),
    calc(100% - var(--r2)) calc(100% - var(--notch-h) - var(--r2)),
    calc(100% - var(--r3)) calc(100% - var(--notch-h) - var(--r1)),
    calc(100% - var(--piece-radius)) calc(100% - var(--notch-h)),
    calc(var(--notch-bottom) + 22px) calc(100% - var(--notch-h)),
    calc(var(--notch-bottom) + 18px) 100%,
    calc(var(--notch-bottom) + 4px) 100%,
    var(--notch-bottom) calc(100% - var(--notch-h)),
    var(--piece-radius) calc(100% - var(--notch-h)),
    var(--r3) calc(100% - var(--notch-h) - var(--r1)),
    var(--r2) calc(100% - var(--notch-h) - var(--r2)),
    var(--r1) calc(100% - var(--notch-h) - var(--r3)),
    0 calc(100% - var(--notch-h) - var(--piece-radius))
  );
}

.block-empty, .program-empty {
  margin: 0;
  padding: .25rem .5rem;
  color: var(--game-ink);
  font-family: var(--game-font-block);
  font-size: var(--rb-text-xs);
  font-weight: 600;
  opacity: .7;
}

.program-empty { color: var(--game-muted); opacity: 1; }

.times {
  flex: none;
  display: flex;
  align-items: center;
  gap: .25rem;
  padding: .125rem;
  border-radius: 6px;
  background: rgba(255, 255, 255, .42);
}

.times-btn {
  width: 1.375rem;
  height: 1.375rem;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: rgba(255, 255, 255, .8);
  color: var(--game-ink);
  font-size: var(--rb-text-base);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}

.times-btn:hover { background: var(--rb-blanco); }

.times-value {
  min-width: 1.25rem;
  text-align: center;
  font-family: var(--game-font-block);   /* a digit the child sets: see .level-btn */
  font-size: var(--rb-text-lg);
  font-weight: 800;
}

/* ---------- program ---------- */

.program {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: .5rem .5rem calc(.5rem + var(--notch-h));
  border-radius: 10px;
  background: var(--game-well);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, .4);
}

.program.is-locked, .palette.is-locked { opacity: .8; pointer-events: none; }
.block.is-dragging { opacity: .3; }

/* clip-path cuts off outlines and box-shadows, so the running piece is marked with a
   drop-shadow halo, which follows the silhouette instead of the box.

   The halo is cyan and not the yellow it used to be: a `repetir` block is amber-orange,
   and a yellow glow around it is a yellow glow around yellow — invisible on exactly the
   block a child most needs to watch. Cyan is the concept's own colour for something
   powered up, and it carries over both block families. */
@keyframes piece-glow {
  0%, 100% { filter: drop-shadow(0 0 2px var(--rovi-cian)) drop-shadow(0 0 5px var(--rovi-cian)); }
  50% { filter: drop-shadow(0 0 3px var(--rovi-cian)) drop-shadow(0 0 10px var(--rovi-cian)) brightness(1.08); }
}

.block.is-running { animation: piece-glow .9s ease-in-out infinite; }
.block.is-running > .block-head { filter: brightness(1.08); }

.drag-ghost {
  position: fixed;
  z-index: 20;
  width: 11rem;
  margin: 0;
  transform: translate(-50%, -50%) rotate(-3deg);
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, .5));
  pointer-events: none;
  opacity: .95;
}

/* Negative margins cancel its own height: the indicator marks a position without
   taking up space, so nothing below it shifts while the block is being dragged. */
.drop-line {
  height: 6px;
  margin: -3px 0;
  border-radius: 3px;
  background: var(--rovi-cian);
}

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

.board-panel { gap: var(--rb-space-2); }

/* The world sits under the concept's sky, with five voxel clouds ruled into it. Each
   cloud is a flat `linear-gradient` layer — a hard-edged rectangle, because a blurred
   radial one would be the only soft thing on the page. */
.board-wrap {
  flex: 1;
  min-height: 0;
  display: grid;
  padding: var(--rb-space-2);
  border-radius: 10px;
  background-color: var(--rovi-cielo);
  background-image:
    linear-gradient(rgba(255, 255, 255, .55) 0 0),   /* cloud 1, lower slab */
    linear-gradient(rgba(255, 255, 255, .55) 0 0),   /* cloud 1, upper slab */
    linear-gradient(rgba(255, 255, 255, .4) 0 0),    /* cloud 2 */
    linear-gradient(rgba(255, 255, 255, .4) 0 0),
    linear-gradient(rgba(255, 255, 255, .32) 0 0),   /* cloud 3, the far one */
    linear-gradient(180deg, rgba(255, 255, 255, .28), transparent 55%);
  background-repeat: no-repeat;
  background-size: 72px 16px, 40px 16px, 56px 14px, 32px 14px, 44px 12px, 100% 100%;
  background-position: 9% 22%, 13% 13%, 82% 30%, 86% 20%, 47% 9%, 0 0;
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, .3);
}

/* There is nothing left to style here. The board is a single canvas: the ground, the
   walls, the flag, the batteries and the robot are all boxes drawn under one camera —
   see src/voxel.js. It has no CSS size of its own on purpose, because `fit()` measures
   this element to decide how big a square can be. */
.board { position: relative; width: 100%; height: 100%; }

.board-canvas { display: block; }

/* ---------- controls ---------- */

.controls { display: flex; gap: var(--rb-space-1); flex-wrap: wrap; }

/* Same block as every other button: flat face, lit top edge, hard shadow underneath
   that the button drops into when it is pressed. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  padding: .7rem .8rem;
  border: 0;
  border-radius: 8px;
  background: var(--game-chip);
  box-shadow: inset 0 2px 0 var(--face-lit), 0 4px 0 rgba(0, 0, 0, .38);
  color: var(--rb-blanco);
  font-family: var(--game-font-pixel);
  /* A pixel face is drawn on a grid, so it wants whole pixels: 16px is one row per
     pixel and stays crisp, 18px lands between rows and goes fuzzy. The five controls
     also have to sit on one line — a wrapped row costs the board its height. */
  font-size: var(--rb-text-base);
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}

.btn:hover:not(:disabled) { filter: brightness(1.15); }

.btn:active:not(:disabled) {
  transform: translateY(3px);
  box-shadow: inset 0 2px 0 var(--face-lit), 0 1px 0 rgba(0, 0, 0, .38);
}

.btn:disabled { opacity: .4; cursor: not-allowed; box-shadow: 0 4px 0 rgba(0, 0, 0, .25); }

.btn-icon { width: 1.125rem; height: 1.125rem; flex: none; fill: currentColor; }
#run-btn .btn-icon { fill: var(--game-ink); }
#stop-btn:not(:disabled) .btn-icon { fill: var(--rovi-sol); }
/* The reset arrow is drawn as a stroke, and the fill above would flood the arc. */
#reset-btn .btn-icon { fill: none; }

.btn-run {
  flex: 1;
  background: var(--game-go);
  color: var(--game-ink);
}

.btn-quiet {
  align-self: flex-start;
  padding: .3rem .7rem;
  background: transparent;
  box-shadow: none;
  font-size: var(--rb-text-base);
  color: var(--game-muted);
}

.btn-quiet:hover:not(:disabled) { color: var(--rb-blanco); filter: none; }
.btn-quiet:active:not(:disabled) { transform: none; box-shadow: none; }

.status {
  min-height: 2.75rem;
  margin: 0;
  padding: .5rem .75rem;
  border-radius: 8px;
  background: var(--game-well);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, .35);
  color: var(--game-muted);
  font-size: var(--rb-text-sm);
}

.status-warn {
  background: color-mix(in srgb, var(--rovi-sol) 20%, var(--game-well));
  color: var(--rovi-sol);
  font-weight: 600;
}

.status-win {
  background: color-mix(in srgb, var(--rovi-pasto) 24%, var(--game-well));
  color: var(--rovi-pasto-claro);
  font-weight: 600;
}

/* ---------- hint ---------- */

/* The concept's "misión actual" card: a darker plate on the panel, with the label in
   pixel type and the sentence itself in Nunito. */
.hint {
  margin-top: auto;
  padding: var(--rb-space-1);
  border-radius: 10px;
  background: var(--game-well);
  border-left: 4px solid var(--game-spark);
}

.hint-title {
  font-family: var(--game-font-pixel);
  font-size: var(--rb-text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--game-spark);
}

.hint-text {
  margin: .25rem 0 0;
  font-family: var(--game-font-block);
  font-size: var(--rb-text-sm);
  color: var(--game-paper);
}

/* ---------- win dialog ---------- */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  padding: var(--rb-space-2);
  background: rgba(10, 18, 24, .72);
}

.overlay[hidden] { display: none; }

.dialog {
  width: min(26rem, 100%);
  padding: var(--rb-space-4);
  border: 3px solid var(--game-line);
  border-radius: 14px;
  background: var(--game-surface);
  box-shadow: inset 0 3px 0 rgba(255, 255, 255, .08), var(--game-shadow);
  text-align: center;
}

.dialog-title {
  font-family: var(--game-font-pixel);
  font-size: var(--rb-text-2xl);
  color: var(--rovi-cian);
}

.stars { display: flex; justify-content: center; gap: .5rem; margin: var(--rb-space-2) 0; }
.star { width: 3rem; height: 3rem; fill: var(--game-well); }
.star.is-on { fill: var(--game-spark); filter: drop-shadow(0 0 8px rgba(239, 171, 23, .6)); }

.dialog-text { margin: 0 0 var(--rb-space-3); color: var(--game-muted); }
.dialog-actions { display: flex; gap: var(--rb-space-1); justify-content: center; }

/* ---------- narrow screens ---------- */

/* Tablets, and phones held sideways: two columns. The blocks and the program share
   the left one, the board takes the right one, so the robot is never below the fold
   while the child builds. The page scrolls if the screen is short. */
@media (max-width: 60rem) {
  body { height: auto; min-height: 100%; }

  .topbar { height: auto; min-height: var(--game-topbar); flex-wrap: wrap; row-gap: .25rem;
            padding: .5rem var(--rb-space-2); }
  /* One row that scrolls sideways instead of wrapping into three. The padding keeps
     the current level's outline and the buttons' hard shadow from being clipped. */
  .levels { flex-wrap: nowrap; overflow-x: auto; max-width: 100%; padding: 5px 5px 8px;
            scrollbar-width: none; -webkit-overflow-scrolling: touch; }
  .levels::-webkit-scrollbar { display: none; }
  .level-btn { flex: none; }

  .stage {
    grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
    grid-template-areas: "palette board" "program board";
    grid-template-rows: auto 1fr;
    min-height: calc(100dvh - var(--game-topbar));
  }
  .palette-panel { grid-area: palette; }
  .program-panel { grid-area: program; }
  .board-panel { grid-area: board; }
  .palette { flex-direction: row; flex-wrap: wrap; }
  .palette .block { width: auto; flex: 1 1 7rem; }
  .program { min-height: 9rem; }
  .board-wrap { min-height: 15rem; }
  .counter { font-size: var(--rb-text-base); }
  .panel-title { flex-wrap: wrap; }

  .controls { flex-wrap: nowrap; }
  /* The secondary buttons keep their icon and lose the word, so the five fit on one
     line beside a board that is narrower than on a computer. The word stays in the
     accessibility tree: a screen reader still says «Paso», «Detener», «Reiniciar». */
  .controls .btn:not(.btn-run) .btn-label {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
}

/* Phones held sideways: the screen is short, so the board stays in view while the
   left column scrolls, and the controls go to the bar at the bottom (below). */
@media (max-width: 60rem) and (max-height: 30rem) {
  .board-panel { position: sticky; top: var(--rb-space-1); align-self: start; }
  /* 100dvh less the bar, the sticky offset, the panel's title, status and padding. */
  .board-wrap { min-height: 0; height: calc(100dvh - 13.5rem); }
  .status { min-height: 2.25rem; padding-block: .35rem; }
  .stage { min-height: 0; }
}

/* Phones held upright: one column, board first, then the program and the blocks under
   it, so a tapped block lands in plain sight just above the thumb. The controls are
   pinned to the bottom of the screen so «Ejecutar» is always under the thumb. */
@media (max-width: 40rem) {
  .topbar { padding: .5rem var(--rb-space-1) .25rem; }
  .brand-mark { width: 1.75rem; height: 1.75rem; }
  .brand-name { font-size: var(--rb-text-lg); }
  .levels { width: 100%; margin-left: 0; }

  .stage {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "board" "program" "palette";
    grid-template-rows: auto;
    min-height: 0;
    gap: var(--rb-space-1);
    padding: var(--rb-space-1) var(--rb-space-1) 0;
  }
  .panel { padding: .75rem; border-radius: 12px; }
  .panel-title { font-size: var(--rb-text-base); }
  .chip { font-size: var(--rb-text-sm); }
  .board-panel { gap: var(--rb-space-1); scroll-margin-top: var(--rb-space-1); }
  .board-wrap { min-height: 0; aspect-ratio: 5 / 4; padding: var(--rb-space-1); }
  .status { min-height: 2.5rem; }
  .program { min-height: 7rem; }

}

/* The controls bar, pinned to the bottom of the screen on phones either way up. */
@media (max-width: 40rem), (max-width: 60rem) and (max-height: 30rem) {
  .controls {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 10;
    gap: .375rem;
    padding: .5rem max(var(--rb-space-1), env(safe-area-inset-right)) calc(.5rem + env(safe-area-inset-bottom)) max(var(--rb-space-1), env(safe-area-inset-left));
    background: var(--game-chrome);
    border-top: 3px solid var(--rovi-borde);
  }
  /* Room at the end of the page so the fixed bar never covers the program. */
  body { padding-bottom: calc(4.25rem + env(safe-area-inset-bottom)); }
  .controls .btn { min-width: 0; padding: .7rem .6rem; font-size: var(--rb-text-sm); }
  .controls .btn-speed { font-size: var(--rb-text-xs); padding-inline: .5rem; }
}

/* Nothing on the board is listed here, and it is not an oversight: board.js checks this
   query itself and builds the level, walks and turns with every animation switched off.
   CSS cannot reach inside a canvas. */
@media (prefers-reduced-motion: reduce) {
  .pip-slot { transition-duration: .01ms; }
  .block.is-running { animation: none; }
  .block.is-running { filter: drop-shadow(0 0 4px var(--rovi-cian)); }
}
