/* lab.css — Builder Lab (the Blockly + Monaco game editor inside the Account app).

   Loaded AFTER account.css and only ever ADDS to it: every colour, font, radius and shadow here
   comes from the account.css design tokens (--blue / --grad-blue / --grad-panel / --panel-1|2 /
   --fg / --muted / --green / --red / --f-ui / --f-display / --f-mono / --shadow-btn) and its
   rgba(43,140,255,0.NN) border idiom. Nothing new is invented palette-wise.

   Structure (matching #tab-lab in index.html):
     shell breakout ▸ first run ▸ toolbar ▸ studio primitives ▸ view tabs ▸ body/panes/divider
     ▸ blocks pane ▸ sprites manager ▸ code pane ▸ validation bar ▸ run overlay
     ▸ history drawer ▸ preview bar ▸ modals ▸ responsive ▸ motion + focus */

/* ============================ shell breakout ============================
   The Lab is a focused workspace: it fills the viewport under the app bar and scrolls INSIDE its
   panes rather than scrolling the page. Driven purely by :has() so no JS class is needed on <body>.
   (The app bar and section tabs are full width for every tab — see account.css — so only the height
   behaviour and the tighter padding are Lab-specific.) */
.workspace:has(> #tab-lab:not([hidden])) {
  padding:16px 18px 0;
  display:flex; flex-direction:column; min-height:0;
}
/* Panels are capped to a readable column by default; a workspace wants the whole width. */
.tab-panel.studio { max-width:none; margin:0; }
/* …but the first-run and template screens are ordinary pages, not a workspace, so they sit in the
   same column as Creator Hub. Restoring the standard workspace padding while the editor is hidden
   means the two line up EXACTLY at every width, with no per-breakpoint fudge factor to keep in
   sync — the tighter 18px gutter above exists for the editor alone. */
.workspace:has(#tab-lab:not([hidden]) #lab-editor[hidden]) { padding:26px 22px 0; }
.workspace:has(> #tab-lab:not([hidden])) .tabs { margin-bottom:14px; }
/* the page footer would force the page to scroll; the Lab owns the full height instead */
main:has(#tab-lab:not([hidden])) .legal-footer { display:none; }

.tab-panel.studio { position:relative; }               /* run overlay + drawer anchor here */
.tab-panel.studio:not([hidden]) {
  display:flex; flex-direction:column; flex:1; min-height:0;
  padding-bottom:16px;
}

/* ============================ first run / templates ============================ */
/* Same column as every other .tab-panel, so "Builder Lab" starts at the same x as "Creator Hub". */
.lab-firstrun { max-width:1180px; margin:0 auto; width:100%; }
.lab-firstrun[hidden] { display:none; }
.lab-crumbs { display:flex; align-items:center; gap:9px; font-size:13px; color:var(--muted); margin-bottom:18px; }
/* Keep each crumb on one line — without this the flex items shrink and "Creator Hub" wraps to two. */
.lab-crumbs > * { flex:none; white-space:nowrap; }
.linkbtn {
  background:none; border:none; padding:4px 2px; margin:0; cursor:pointer;
  font-family:var(--f-ui); font-size:13px; font-weight:600; color:var(--blue);
}
.linkbtn:hover { text-decoration:underline; }
/* Carries .page-head too, so the title/sub spacing is Creator Hub's, not a second variant. */
.lab-firstrun-head { margin-bottom:24px; }

.starter-grid { display:grid; grid-template-columns:1fr 1fr; gap:18px; align-items:stretch; }
.starter-grid-3 { grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); }
.starter-card {
  position:relative; display:flex; flex-direction:column; gap:12px; align-items:flex-start;
  padding:22px; border-radius:20px; border:1px solid rgba(43,140,255,0.22);
  background:var(--grad-panel); box-shadow:0 12px 40px rgba(0,0,0,0.35);
}
.starter-card > button { margin-top:auto; }
/* A template's cover art, full-bleed across the top of its card. Square and un-cropped on purpose:
   this is the exact picture the new game is created with, so the picker shows the whole of it. */
.starter-shot {
  align-self:stretch; width:calc(100% + 44px); margin:-22px -22px 0; display:block;
  aspect-ratio:1/1; object-fit:cover; border-radius:20px 20px 0 0;
  background:rgba(43,140,255,0.06); image-rendering:pixelated;
}
.starter-shot[hidden] { display:none; }
.starter-icon {
  width:44px; height:44px; border-radius:13px; display:flex; align-items:center; justify-content:center;
  font-size:22px; background:rgba(43,140,255,0.1); border:1px solid rgba(43,140,255,0.25);
}
/* A template card carries its cover instead of an emoji, and only falls back to one if that cover
   fails to load — so the badge has to actually disappear when hidden (display:flex ignores [hidden]). */
.starter-icon[hidden] { display:none; }
.starter-title { font-family:var(--f-display); font-weight:700; font-size:19px; margin:0 0 6px; }
.starter-desc { font-size:13.5px; color:var(--muted); line-height:1.5; margin:0; }

/* "Continue working on" — the Creator Hub's own cards, made clickable end to end. */
.lab-recent { margin-top:34px; }
.lab-recent[hidden] { display:none; }
.lab-recent .section-title { margin-bottom:14px; }
.lab-recent-card {
  position:relative; display:block; width:100%; padding:0; text-align:left; overflow:hidden;
  font-family:var(--f-ui); transition:border-color .15s, transform .15s;
}
.lab-recent-card:hover { border-color:rgba(43,140,255,0.55); transform:translateY(-2px); }
/* The card is a div now (the bin can't live inside a button), so the opening half is its own
   button filling the card — the whole thing stays clickable and keyboard-reachable as before. */
.lab-recent-open { display:block; width:100%; padding:0; margin:0; border:0; background:none;
  color:inherit; font:inherit; text-align:left; cursor:pointer; }
.lab-recent-open:focus-visible { outline:2px solid var(--blue); outline-offset:-2px; border-radius:inherit; }
/* Sits over the cover, top right. Always there rather than hover-only: it has to be reachable on a
   touchscreen, where there is no hover to reveal it. */
.lab-recent-del { position:absolute; top:10px; right:10px; z-index:1;
  background:rgba(8,8,24,0.72); backdrop-filter:blur(6px); }
.lab-recent-card .game-card-foot { display:flex; align-items:center; justify-content:space-between; gap:10px; }

/* ============================ studio primitives ============================ */
.iconbtn {
  display:inline-flex; align-items:center; justify-content:center; flex:none;
  width:36px; height:36px; padding:0; border-radius:10px;
  background:transparent; border:1px solid rgba(43,140,255,0.22); color:var(--muted-2);
  font-family:var(--f-ui); font-size:15px; line-height:1; cursor:pointer;
  transition:border-color .15s, color .15s, background .15s;
}
.iconbtn:hover:not(:disabled) { color:var(--blue); border-color:rgba(43,140,255,0.6); background:rgba(43,140,255,0.08); }
.iconbtn:disabled { opacity:.35; cursor:not-allowed; }
.lab-toolbar-spacer { flex:1 1 auto; min-width:0; }
.lab-toolbar-rule { flex:none; width:1px; height:22px; background:rgba(43,140,255,0.2); }
.primary.sm { padding:8px 14px; border-radius:10px; font-size:13px; }
.pill.is-running { color:var(--green); }
.pill.is-running::before {
  content:""; width:7px; height:7px; border-radius:50%; background:var(--green);
  box-shadow:0 0 8px var(--green); animation:wbpulsedot 1.4s ease-in-out infinite;
}

/* ==================== somebody else's game, and somebody in yours ====================
   Deliberately not the editor's own blue: an adult looking at a child's work should be able to
   tell at a glance that this screen is not their own game, before reading a word of it. */
.lab-review {
  display:flex; align-items:center; gap:14px; flex-wrap:wrap; flex:none;
  padding:9px 14px; margin-bottom:10px; border-radius:14px;
  border:1px solid rgba(190,140,255,0.4); background:rgba(140,90,230,0.14);
}
.lab-review[hidden] { display:none; }
.lab-review-back {
  display:inline-flex; align-items:center; gap:6px; padding:0; border:0; background:none;
  cursor:pointer; font-size:13px; font-weight:600; color:#d9c4ff;
}
.lab-review-back:hover { text-decoration:underline; }
.lab-review-who { margin:0; flex:1 1 220px; font-size:13.5px; color:#efe7ff; }
.lab-review-who strong { color:#fff; }
.lab-review-tag { font-size:12px; color:#c3aee8; }

/* The child's side of the same lock. Amber, because nothing is broken — they just have to wait. */
.lab-locked {
  flex:none; padding:9px 14px; margin-bottom:10px; border-radius:14px;
  border:1px solid rgba(255,190,90,0.4); background:rgba(255,190,90,0.1);
  font-size:13.5px; line-height:1.5; color:#f0dcb8;
}
.lab-locked[hidden] { display:none; }
.lab-locked strong { color:#ffe9c2; }

/* ============================ toolbar ============================
   One row: back · name + save status · game code chip · spacer · actions.
   Hierarchy is load-bearing: Run = green primary, Save = outline, everything else ghost. */
/* margin-bottom:24px, not the usual 12px — .lab-savestate (below) floats in this gap when it has
   something to say, and 12px was not tall enough to hold its own line without touching the canvas.
   Always 24px, never conditional on save state, is what keeps the canvas a constant height (see the
   note on .lab-savestate). */
.lab-toolbar {
  display:flex; align-items:center; gap:10px; flex:none; flex-wrap:nowrap;
  padding:14px; margin-bottom:24px; border-radius:16px;
  border:1px solid rgba(43,140,255,0.2); background:var(--grad-panel);
  box-shadow:0 10px 32px rgba(0,0,0,0.32);
}
/* Nowrap, so the toolbar never breaks into three ragged rows when the window narrows. Below the
   floor the whole editor scrolls sideways as one piece (see the small-screens section) — the
   toolbar goes with it rather than reflowing into something worse. The buttons hold their size so
   nothing gets crushed; only the spacer gives. */
.lab-toolbar > :not(.lab-toolbar-spacer) { flex:none; }
.lab-back { font-size:17px; }

/* .lab-identity's own box holds ONLY the name now — see .lab-savestate below for where the status
   line went and why. A single flow child sizes the box to exactly that child, so
   .lab-toolbar's align-items:center centers the name the same way it centers every other
   single-line control beside it — no compensating padding needed. */
.lab-identity { position:relative; display:flex; flex-direction:column; min-width:0; }
.lab-name {
  width:auto; min-width:0; max-width:280px; padding:5px 9px; border-radius:9px;
  background:transparent; border:1px solid transparent;
  font-family:var(--f-display); font-weight:700; font-size:17px; color:var(--fg);
}
.lab-name:hover { border-color:rgba(43,140,255,0.25); background:rgba(5,3,15,0.4); }
.lab-name:focus { border-color:rgba(43,140,255,0.7); background:rgba(5,3,15,0.7); box-shadow:0 0 0 3px rgba(43,140,255,0.15); }
/* Autosave is silent BY DESIGN, and there is now a Save button with its own "Saved ✓" reply — so
   this line has nothing left to say while everything is fine, and saying it anyway ("All changes
   saved", every time, forever) was exactly the passive chrome the redesign was trying to remove.
   It still exists for the states that ARE worth interrupting for: mid-edit ("Unsaved changes",
   "Saving…" — a "something is happening" signal — see is-dirty/is-saving below) and every failure,
   where it is also the retry affordance, which is why its text is a real button underneath.

   position:absolute, floating below the name rather than reserving flow space for it — the row
   used to reserve this line's height at all times so that hiding its CONTENT (visibility:hidden,
   not display:none) never shrank the toolbar, because a shrink here once left the trashcan
   stranded below the workspace's new, smaller bottom edge (Blockly computes its position once,
   early, against whatever canvas height there was THEN — see e2e/regressions.spec.js "the
   trashcan must stay inside the visible canvas", which caught exactly this). Taking the line out
   of flow entirely is the more robust fix for the same problem: the toolbar's height, and so the
   canvas below it, is now constant REGARDLESS of this line's content, because nothing about this
   line can ever push on layout at all. When it does have something to say it prints into the
   toolbar's own bottom margin, which is why that margin (.lab-toolbar's margin-bottom) is taller
   than this line — a few px of visible overlap into that gap costs far less than the tall reserve
   this replaced. */
.lab-savestate {
  position:absolute; top:100%; left:0; z-index:2;
  display:flex; align-items:center; gap:7px; margin-top:2px; padding-left:9px;
  font-size:12px; color:var(--muted); white-space:nowrap;
}
.lab-savestate.is-saved { visibility:hidden; }
.lab-savestate .txt {
  appearance:none; background:none; border:none; padding:0; font:inherit; color:inherit;
  text-align:left; cursor:default;
}
.lab-savestate.is-failed .txt,
.lab-savestate.is-signedout .txt { cursor:pointer; text-decoration:underline; }
.lab-savestate .dot { flex:none; width:7px; height:7px; border-radius:50%; background:var(--green); box-shadow:0 0 7px rgba(70,224,138,0.7); }
.lab-savestate.is-dirty .dot { background:var(--muted); box-shadow:none; }
.lab-savestate.is-saving .dot { background:var(--blue); box-shadow:0 0 7px rgba(43,140,255,0.7); animation:wbpulsedot 1s ease-in-out infinite; }
/* `is-failed`/`is-signedout` are what renderSaveState actually emits — an earlier draft styled
   `is-error`, which nothing sets, so a failed save wore the same green dot as a healthy one. */
.lab-savestate.is-failed, .lab-savestate.is-signedout { color:var(--red); }
.lab-savestate.is-failed .dot,
.lab-savestate.is-signedout .dot { background:var(--red); box-shadow:0 0 7px rgba(255,93,108,0.7); }
/* Waiting on a teacher's lock is not a fault — amber, and no underline, because there is nothing
   to tap. It saves itself the moment they close it. */
.lab-savestate.is-waiting { color:#f0c88a; }
.lab-savestate.is-waiting .dot { background:#f0c88a; box-shadow:0 0 7px rgba(240,200,138,0.6); }
/* toast(text) in lab.js builds one of these and appends it to #tab-lab — with no rule anywhere in
   this file, before Phase 0. It never actually was visible: no position, no z-index, no background,
   so it sat wherever normal document flow put an unstyled div and was indistinguishable from
   nothing. Still used for the small confirmations Save/Keep don't already answer for themselves
   (e.g. deleting or renaming a kept save) — the Saved ✓ popover further down is Save's own reply.

   position:fixed is safe here, not scoped to the pane: #tab-lab itself carries [hidden] the moment
   the Lab tab is not the active one, and a display:none ancestor stops every descendant rendering
   regardless of its own position — so this can never show outside the Lab tab in practice. */
.lab-toast {
  position:fixed; left:50%; bottom:32px; z-index:200;
  transform:translateX(-50%);
  background:var(--grad-panel); border:1px solid var(--line-strong);
  color:var(--fg); font-size:13.5px; font-weight:600;
  padding:10px 20px; border-radius:999px;
  box-shadow:0 10px 28px rgba(0,0,0,0.4);
  pointer-events:none;
  animation:lab-toast-in .18s ease-out;
}
.lab-toast[hidden] { display:none; }
@keyframes lab-toast-in {
  from { opacity:0; transform:translateX(-50%) translateY(10px); }
  to { opacity:1; transform:translateX(-50%) translateY(0); }
}
@media (prefers-reduced-motion:reduce) { .lab-toast { animation:none; } }


/* Run — the one green button on the page. Green analogue of account.css's .primary recipe. */
/* ONE rule for both places a child can press Play: the toolbar button, and the pill that appears
   over the game preview on hover. They are the same action, so they are the same button — and they
   were not: the overlay was a blue fully-round pill against the bar's green 12px one, which read as
   a different control doing a different thing. Sharing the selector is what stops them drifting
   again; two copies of a gradient eventually disagree, and nothing fails when they do. */
.lab-run, .lab-gameview-hit-pill {
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  padding:13px 24px; border-radius:12px; border:1px solid rgba(70,224,138,0.6);
  background:linear-gradient(135deg,#5ceb9c,#1fb968); color:#04210f;
  font-family:var(--f-ui); font-weight:800; font-size:15px; cursor:pointer;
  box-shadow:0 6px 18px rgba(31,185,104,0.35), inset 0 1px 0 rgba(255,255,255,0.28);
  transition:filter .15s, transform .05s;
}
.lab-run:hover:not(:disabled) { filter:brightness(1.07); }
.lab-run:active:not(:disabled) { transform:translateY(1px); }
.lab-run:disabled {
  background:rgba(138,147,173,0.12); border-color:transparent; color:#5a627a;
  box-shadow:none; cursor:not-allowed; filter:none;
}
/* The Save/Your-saves pill — one grouped control replacing what used to be two separate ghost
   buttons. Both halves share a pill outline; the divider between them is the only border in the
   middle. Save is deliberately quieter than Run but louder than the ghosts either side of it — the
   outline/secondary treatment the old checkpoint button had. */
.lab-savegroup {
  display:inline-flex; align-items:stretch; height:38px; border-radius:12px; overflow:hidden;
  border:1px solid rgba(43,140,255,0.22);
}
/* Matches .ghostbtn's own quiet default (muted text, brightening to blue only on hover) — Save,
   Ask mebo and Details sit in the same row and read as one family of equal-weight actions, not
   three different colour schemes competing for the eye. */
.lab-save, .lab-saves {
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  padding:0 14px; background:transparent; border:none; color:var(--muted-2);
  font-family:var(--f-ui); font-weight:600; font-size:14px; cursor:pointer; line-height:1;
  transition:color .15s;
}
.lab-save:hover, .lab-saves:hover { background:rgba(43,140,255,0.1); color:var(--blue); }
/* The divider between the two halves — a single 1px border shared by both buttons would double up
   under the pill's own outer border, so it lives on the left button only. */
.lab-save { border-right:1px solid rgba(43,140,255,0.22); }
/* A label that is not a WORD, so it can never be misread as the verb ("Save 3"?) — visible on
   touch, where hover never reveals a title. ≥44px wide even with no count showing: the clock icon
   alone is still a real target, not a sliver next to Save. */
.lab-saves { min-width:44px; }
.lab-saves-count {
  font-size:12px; font-weight:700; color:var(--blue); background:rgba(43,140,255,0.16);
  border-radius:999px; padding:1px 7px; min-width:18px; text-align:center;
}
.lab-saves-count[hidden] { display:none; }

/* The Saved ✓ popover (Save's own reply) and the Keep popover share this shell — same idea as the
   mebo callout below: fixed, placed under its anchor button in JS (placeUnder in lab.js), because
   the toolbar clips overflow. Neither is modal: Escape or a click outside just closes it. */
.lab-savedpop, .lab-keeppop {
  position:fixed; z-index:60; padding:14px 16px; border-radius:14px;
  border:1px solid rgba(43,140,255,0.45); background:var(--panel, #131a2b);
  box-shadow:0 16px 44px rgba(0,0,0,0.5); animation:mb-note-in 0.18s ease-out;
}
.lab-savedpop[hidden], .lab-keeppop[hidden] { display:none; }
@media (prefers-reduced-motion: reduce) { .lab-savedpop, .lab-keeppop { animation:none; } }
/* No fixed width, unlike .lab-keeppop below — it sizes to its own short content, "Saved ✓" or
   "⚑ Keep this one" in a single line. A guessed fixed width (260px) clipped the Keep button by a
   couple of px on real fonts; placeUnder (lab.js) now measures the rendered width instead of
   assuming one. */
.lab-savedpop { display:flex; align-items:center; gap:12px; white-space:nowrap; }
.lab-savedpop-txt { font-size:15px; font-weight:800; color:var(--green); }
.lab-keeppop { width:320px; }
.lab-keeppop-title { margin:0 0 10px; font-size:15px; font-weight:800; color:var(--fg); }
.lab-keeppop input[type="text"] {
  width:100%; padding:10px 12px; border-radius:10px; border:1px solid rgba(43,140,255,0.3);
  background:rgba(5,3,15,0.4); color:var(--fg); font-family:var(--f-ui); font-size:16px;
}
.lab-keeppop input[type="text"]:focus {
  border-color:rgba(43,140,255,0.7); box-shadow:0 0 0 3px rgba(43,140,255,0.15);
}
.lab-keeppop-err { margin:8px 0 0; font-size:12.5px; color:var(--red); }
.lab-keeppop-err[hidden] { display:none; }
.lab-keeppop-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:12px; }

/* "⚑ Keep a save of now", at the head of the drawer — a real button, not a link, since it starts
   the same deliberate act the popover elsewhere on screen does. */
.lab-keepnow {
  display:flex; align-items:center; justify-content:center; gap:8px; width:100%;
  margin:10px 0 6px; padding:10px 14px; border-radius:10px;
  border:1px dashed rgba(43,140,255,0.4); background:rgba(43,140,255,0.06);
  color:var(--blue); font-family:var(--f-ui); font-weight:700; font-size:13.5px; cursor:pointer;
}
.lab-keepnow:hover { background:rgba(43,140,255,0.14); border-style:solid; }

/* ============================ view tabs (narrow screens only) ============================ */
.lab-viewtabs {
  display:none; gap:8px; flex:none; padding:5px; margin-bottom:10px; border-radius:13px;
  background:rgba(12,18,48,0.5); border:1px solid rgba(43,140,255,0.16);
}
.lab-viewtab {
  flex:1; padding:10px 14px; border-radius:9px; background:transparent; border:none;
  color:var(--muted); font-family:var(--f-ui); font-size:13.5px; font-weight:600; cursor:pointer;
}
.lab-viewtab:hover { color:var(--fg); }
.lab-viewtab.is-active { background:rgba(7,104,255,0.18); color:var(--blue); }

/* ============================ editor body / panes / divider ============================ */
.lab-editor { display:flex; flex-direction:column; flex:1; min-height:0; }
.lab-editor[hidden] { display:none; }
.lab-body { display:flex; flex:1; min-height:0; min-width:0; }
.lab-pane {
  display:flex; flex-direction:column; min-height:0; min-width:0; overflow:hidden;
  border-radius:18px; border:1px solid rgba(43,140,255,0.2); background:var(--grad-panel);
  box-shadow:0 12px 40px rgba(0,0,0,0.35);
}
/* An author `display` always beats the UA's [hidden] rule, so a class that sets one must restate it
   — without this, hiding the blocks pane at level 2 would do nothing at all. (Same trap as
   .field[hidden] in account.css, which once exposed the admin-only game-code field to everyone.) */
.lab-pane[hidden] { display:none; }

/* Toolbar buttons share one height — a FIXED one, not a minimum. A colour emoji carries a taller
   line box than the text beside it, so a min-height still let one button grow past its neighbours;
   the clock is an SVG now, and the fixed height means no future glyph can do it again. */
.lab-toolbar .ghostbtn, .lab-toolbar .lab-run {
  display:inline-flex; align-items:center; justify-content:center; gap:7px;
  height:38px; padding-top:0; padding-bottom:0; line-height:1;
}
/* Nothing in this file styled a disabled toolbar ghost button — the same invisible-state trap as
   the unstyled toasts (see docs/builder-lab-saving-plan.md Phase 0). Setting .disabled on the
   checkpoint button while previewing did nothing a child could see without this. */
.lab-toolbar .ghostbtn:disabled { opacity:.4; cursor:not-allowed; }
.btnicon { flex:none; }

/* ---- the toolbar sheds its labels before it sheds the panel ----
   The row is nowrap and every button is fixed-size, so its width is simply the sum of what is in
   it — and that sum outgrew the panel. At its longest it wanted about 1390px inside a panel whose
   floor is 1040, so on any ordinary laptop the buttons ran out past the rounded border. That was
   true before the Build Challenge button and it only became visible with it.
   Widening the floor would have been the wrong fix: it would put a horizontal scrollbar under a
   window that is perfectly big enough to work in. Instead the least important LABELS go, one band
   at a time, leaving the icon — each button keeps a title and an aria-label, so hiding the text
   costs a screen reader nothing and a mouse one hover. Run keeps its words at every width; it is
   the primary action and reading "▶" alone is a guess.
   Save keeps its word at every width too, now — the same exemption Run has. On a 1366px Chromebook,
   the hardware most of this audience uses, this band used to be the FIRST to drop, which meant
   nothing on the screen said "Save" on exactly the machines most children are on. #lab-history-open
   has no .lbl of its own any more (it is the clock half of the pill, a count rather than a word), so
   it is not in this list to begin with. */
.lab-toolbar .lbl { display:inline; }
@media (max-width:1400px) {
  /* First to go: rarely pressed, and its icon is unambiguous. */
  #lab-details-open .lbl { display:none; }
}
@media (max-width:1160px) {
  /* Then the competition button — the trophy carries it, and it is not an everyday action. */
  #lab-enter-open .lbl { display:none; }
}
@media (max-width:1060px) {
  /* Last: mebo's name. His face is the most recognisable thing in the row, so this is the label
     that survives longest and still loses nothing when it goes. */
  #lab-help-open .lbl { display:none; }
}

/* ============================ small screens ============================
   Builder Lab has a FIXED minimum width rather than a fluid collapse. Two panes, a toolbar and a
   code surface have a size below which they stop being usable, and reflowing them into something
   narrower produces a worse answer than simply keeping the layout and letting the page scroll to
   it. 1040px is the floor: it is the width the PANES need, and they are comfortable there.

   **iPad landscape does NOT clear it, and this comment used to say it did.** iPad landscape is
   1024 CSS px against a floor of 1040 — the claim was 16px wrong from the day it was written, and
   being wrong about the main target device is exactly what stopped anybody checking. It got worse
   afterwards rather than better: the Save/Keep control took the toolbar's own content from 1076px
   to 1111, so ▶ Play sat at a right edge of 1050 in LANDSCAPE and 990 in portrait, and Save at 837
   in portrait — both of the two controls the editor cannot be used without, off the screen, on the
   hardware this is for. Confirmed on a real iPad.

   The floor stays, because the trade behind it is still right: a two-pane editor reflowed into 768px
   is worse than one you scroll. What changes is that the TOOLBAR stops being governed by it (see
   the sticky rule below) — the panes scroll sideways, the controls do not go with them.

   The scroll lives on the workspace, so the app bar and section tabs stay put and only the editor
   moves. The floor applies ONLY while the editor is open — the first-run and template screens are
   ordinary pages that stay responsive down to a phone, so scoping this to a visible #lab-editor
   keeps them out of it. */
.workspace:has(#tab-lab:not([hidden]) #lab-editor:not([hidden])) { overflow-x:auto; }
.tab-panel.studio:not([hidden]):has(#lab-editor:not([hidden])) { min-width:1040px; }

/* Below the floor, the toolbar is the thing that fits.
 *
 * `position:sticky; left:0` inside the workspace's own horizontal scrollport pins it to the LEFT
 * EDGE OF WHAT YOU CAN SEE, so scrolling the panes sideways no longer takes the controls with them;
 * and a width measured from the viewport rather than from the 1040px panel gives it a real width to
 * wrap into, which `flex-wrap:wrap` then uses. The spacer takes the rest of the first line, so the
 * break lands where it was always going to: identity above, actions below. That reads as a layout
 * somebody meant, which two orphans hanging off a gap does not.
 *
 * Deliberately NOT the alternative fixes: lowering the floor to 1024 no longer helps (the toolbar
 * wants 1111), and shedding the Save label at another band would re-create the exact fault
 * lab.css:382 describes — nothing on the screen saying "Save" on the hardware most of this audience
 * uses. Nothing here is a label; every word survives.
 *
 * 36px is .workspace's own 18px gutters while the editor is open (see the top of this file). The
 * toolbar's height changing between one row and two is safe: it is constant for a given viewport,
 * which is what .lab-savestate's note actually requires, and Blockly re-measures on resize. */
@media (max-width:1040px) {
  .lab-toolbar {
    position:sticky; left:0; z-index:3;
    width:calc(100vw - 36px);
    flex-wrap:wrap; row-gap:10px;
  }
  /* A wrapping toolbar can wrap far enough to leave the panes NOTHING. Measured at a 400px-wide
     viewport: the controls take 207px of a 291px editor and the blocks canvas computes to a height
     of exactly 0 — a child gets a toolbar and no editor underneath it, and because the page is
     pinned at height:100dvh/overflow:hidden while the editor is open, there is nowhere to scroll to
     go and find it. `min-height:0` on .lab-body is what permits the collapse; it is the right
     default when the space exists, so the FLOOR goes here rather than there.
     Real iPads never reach this: the body measures 605px in portrait and 349px in landscape, both
     clear of the floor, so this rule changes nothing about the device it was written for. It is the
     narrow end — a phone, a split-screen pane, a very small window — that had no editor at all. */
  .lab-body { min-height:280px; }
  /* And the floor is only worth having if it can be reached. .workspace is already the sideways
     scroller for the whole editor; this lets it scroll the other way too, on the same condition. */
  .workspace:has(#tab-lab:not([hidden]) #lab-editor:not([hidden])) { overflow-y:auto; }
}

/* And the PAGE must not scroll vertically while the editor is open.
   Every box from body down is already `flex:1; min-height:0`, which is right — but body carried
   `min-height:100vh`, an "at least", so the panes' own minimum content pushed the document 115px
   past the window and the whole editor slid under the fold. Selecting a block near the bottom of
   the workspace then highlighted its lines in a part of Monaco that was off-screen: the feature
   worked and you could not see it work.
   `height` rather than `min-height` gives the flex chain a definite size to divide, so the panes
   shrink and scroll INSIDE themselves — which is what a two-pane editor is supposed to do, and what
   makes "click a block, see its code" true at any window size. Scoped to the open editor, so every
   ordinary page on the site keeps scrolling normally. The horizontal scroll above is unaffected. */
/* dvh, not vh — and this is the rule where it matters most.
   On iOS Safari `100vh` is the LARGE viewport: the height the page would have if the browser chrome
   were hidden, which is 60-90px taller than what you can actually see. Everywhere else on the site
   that only makes a page very slightly scrollable. HERE it is a definite height that is too tall
   with `overflow:hidden` on top of it, so the excess is UNREACHABLE — the bottom of the editor is
   cut off and nothing can scroll to it. `dvh` tracks the chrome as it shows and hides, which is
   right for a persistent layout; `svh` is the conservative swap if the tracking ever reads as jumpy.
   The plain vh line stays first as the fallback for anything that has not heard of dvh.
   public/index.html already knew this — it uses 100svh, with a comment. The app never got it. */
body.app:has(#tab-lab:not([hidden]) #lab-editor:not([hidden])) {
  height:100vh;
  height:100dvh;
  overflow:hidden;
}

/* ---- new file / new folder ---- */
.lab-kinds { display:grid; grid-template-columns:repeat(2, 1fr); gap:8px; margin-bottom:14px; }
.lab-kind {
  display:flex; flex-direction:column; align-items:flex-start; gap:2px; cursor:pointer;
  padding:10px 12px; border-radius:11px; text-align:left;
  background:rgba(5,3,15,0.4); border:1px solid var(--line-soft); color:var(--muted);
  transition:border-color .12s ease, background .12s ease;
}
.lab-kind:hover { border-color:var(--line); background:rgba(43,140,255,0.07); }
.lab-kind.is-active {
  border-color:rgba(43,140,255,0.5); background:rgba(43,140,255,0.11); color:var(--fg);
}
.lab-kind-icon { display:flex; margin-bottom:2px; color:var(--muted-2); }
.lab-kind.is-active .lab-kind-icon { color:var(--blue); }
.lab-kind-name { font-size:13.5px; font-weight:600; color:var(--fg); }
.lab-kind-what { font-size:11.5px; line-height:1.35; color:var(--muted); }

/* The extension lives in the field, so the whole filename is visible as you type it. */
.lab-nameinput { display:flex; align-items:stretch; }
.lab-nameinput input { border-top-right-radius:0; border-bottom-right-radius:0; }
.lab-nameext {
  display:flex; align-items:center; flex:none; padding:0 12px; border-radius:0 11px 11px 0;
  border:1px solid rgba(43,140,255,0.3); border-left:none; background:rgba(43,140,255,0.09);
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:13px;
  color:var(--blue);
}
.lab-nameext[hidden] { display:none; }

/* Download sits with the files rather than in the toolbar — it acts on them, and it is a rare
   thing to want next to Run, which is not. */
.lab-files-foot { flex:none; border-top:1px solid rgba(43,140,255,0.14); }
.lab-files-foot .lab-filetree-note { border-top:none; }
.lab-downloadlink {
  display:block; width:100%; padding:9px 12px; cursor:pointer; text-align:left;
  background:none; border:none; border-top:1px solid rgba(43,140,255,0.1);
  font-family:var(--f-ui); font-size:12px; color:var(--muted);
  transition:color .12s ease, background .12s ease;
}
.lab-downloadlink:hover { color:var(--blue); background:rgba(43,140,255,0.07); }

/* ---- level 2: open tabs, and the preview surface ---- */
/* flex-direction is load-bearing: the base tab stacks a name over its caption for level 1, and
   without resetting it here the close button sat UNDER the filename instead of beside it. */
.lab-editor.is-code .lab-file {
  display:inline-flex; flex-direction:row; align-items:center; gap:7px; cursor:pointer;
}
.lab-file-x {
  appearance:none; border:none; background:none; cursor:pointer; padding:0 2px; line-height:1;
  font-size:11px; color:var(--muted); border-radius:4px; opacity:0; transition:opacity .12s, color .12s;
}
.lab-file:hover .lab-file-x, .lab-file.is-active .lab-file-x { opacity:.7; }
.lab-file-x:hover { opacity:1; color:var(--red); background:rgba(239,68,68,0.12); }

/* A picture or a sound opens as itself. Showing a PNG as bytes would be a worse answer than
   showing the PNG. */
.lab-preview {
  flex:1; min-height:0; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:14px; padding:24px; background:rgba(5,3,15,0.8);
}
.lab-preview[hidden] { display:none; }
.lab-preview-stage {
  display:flex; align-items:center; justify-content:center; max-width:100%; max-height:70%;
  padding:18px; border-radius:14px; border:1px solid rgba(43,140,255,0.18);
  background:repeating-conic-gradient(rgba(255,255,255,0.045) 0% 25%, transparent 0% 50%) 50% / 18px 18px;
}
/* Pixel art must not be smoothed — a 16x16 sprite blown up should look like a 16x16 sprite. */
.lab-preview-stage img {
  max-width:min(420px, 60vw); max-height:46vh; image-rendering:pixelated;
}
.lab-preview-meta {
  margin:0; font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size:12px; color:var(--muted);
}
.lab-preview-none { margin:0; font-size:13px; color:var(--muted); }

/* ---- level 2: the file tree ---- */
 /* A starting width, not a fixed one — the divider overrides flex-grow to resize it. Using a
    flex-basis here (rather than 0 0 210px) is what lets that work. */
.lab-pane-files { flex:0.19 1 0; min-width:150px; }
.lab-filetree { display:flex; flex-direction:column; gap:2px; padding:10px; overflow-y:auto; flex:1; }
.lab-filetree-group {
  margin:10px 0 4px; padding:0 6px; font-size:10.5px; font-weight:700; letter-spacing:.4px;
  text-transform:uppercase; color:var(--muted);
}
.lab-filetree-item {
  display:flex; align-items:center; gap:8px; width:100%; padding:7px 9px; border-radius:8px;
  border:1px solid transparent; background:none; cursor:pointer; text-align:left;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:12.5px;
  color:var(--muted); transition:background .12s ease, color .12s ease;
}
.lab-filetree-item:hover { background:rgba(43,140,255,0.08); color:var(--fg); }
.lab-filetree-item.is-active {
  background:rgba(43,140,255,0.14); border-color:rgba(43,140,255,0.3); color:var(--fg);
}
/* Shipped with every game and not the creator's to edit — listed so they know it is there, but it
   does not pretend to be openable. */
.lab-filetree-item.is-provided { cursor:default; opacity:.55; }
.lab-filetree-item.is-provided:hover { background:none; color:var(--muted); }
/* Line glyphs, not emoji: they take the row's own colour, so an icon dims with a provided file and
   lifts with a selected one instead of shouting in full colour on every row. */
.lab-fileicon { flex:none; opacity:.75; }
.lab-filetree-item.is-active .lab-fileicon { opacity:1; color:var(--blue); }
.lab-filetree-folder .lab-fileicon { opacity:.6; }
/* Folders are folders: nested, indented by depth, and each one a place things can be dropped. */
.lab-filetree-folder {
  display:flex; align-items:center; gap:7px; padding:6px 9px; border-radius:8px; width:100%;
  border:1px dashed transparent; background:none; cursor:pointer; text-align:left;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:12.5px;
  color:var(--muted-2);
}
.lab-filetree-folder:hover { color:var(--fg); }
/* A real chevron rather than a ▾ glyph: at the size this needs to be, a text arrow rendered as a
   couple of grey pixels and was easy to miss entirely. */
.lab-filetree-twist {
  flex:none; color:var(--muted-2); transition:transform .12s ease;
}
.lab-filetree-folder:hover .lab-filetree-twist { color:var(--blue); }
.lab-filetree-folder.is-shut .lab-filetree-twist { transform:rotate(-90deg); }
@media (prefers-reduced-motion:reduce) { .lab-filetree-twist { transition:none; } }
.lab-filetree-folder, .lab-filetree-item { padding-left:calc(9px + var(--depth, 0) * 13px); }
.lab-filetree-folder.is-dropping {
  border-color:rgba(70,224,138,0.6); background:rgba(70,224,138,0.1); color:#7ee08a;
}
/* One + per folder, so where a new thing lands is answered by which one you press. Revealed on
   hover so the tree stays quiet when you are only reading it. */
.lab-filetree-label { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; }
.lab-filetree-plus {
  flex:none; display:inline-flex; align-items:center; justify-content:center;
  width:18px; height:18px; margin-left:auto; border-radius:5px; cursor:pointer;
  font-size:13px; line-height:1; color:var(--muted); opacity:0; transition:opacity .12s, color .12s;
}
.lab-filetree-folder:hover .lab-filetree-plus,
.lab-filetree-group:hover .lab-filetree-plus,
.lab-filetree-plus:focus-visible { opacity:1; }
.lab-filetree-plus:hover { color:var(--blue); background:rgba(43,140,255,0.14); }
/* :focus-visible is not a touch state, so on a tablet this was invisible — and in code mode the
   only other way to add a file is the file tree's drag-and-drop, which is the unreliable one from
   the Files app. The discoverable route cannot be the hidden one. */
@media (hover: none) { .lab-filetree-plus { opacity:1; } }
.lab-filetree-group { display:flex; align-items:center; }
.lab-newfile-kinds { margin-bottom:12px; }

.lab-filetree-drophint {
  margin:6px 0 0; padding:0 9px; font-size:11px; line-height:1.4; color:var(--muted); opacity:.8;
}
.lab-filetree-note {
  flex:none; margin:0; padding:10px 12px; font-size:11.5px; line-height:1.45; color:var(--muted);
  border-top:1px solid rgba(43,140,255,0.14);
}
.lab-pane-blocks { flex:1.15 1 0; }
.lab-pane-code { flex:1 1 0; }
.lab-graduation[hidden] { display:none; }
.lab-pane-head {
  display:flex; align-items:center; gap:10px; flex:none; flex-wrap:wrap;
  padding:10px 14px; border-bottom:1px solid rgba(43,140,255,0.14);
}
.lab-pane-title { font-family:var(--f-display); font-weight:600; font-size:15px; color:var(--fg); }
.lab-pane-hint { font-size:12px; color:var(--muted); }
.lab-pane-tools { display:flex; align-items:center; gap:6px; margin-left:auto; }
.lab-pane-tools[hidden] { display:none; }
.lab-modeswitch { flex:none; gap:6px; }
.lab-modeswitch .seg-btn { flex:none; padding:8px 13px; font-size:13px; border-radius:10px; }
.lab-gradswitch { flex:none; gap:6px; }
.lab-gradswitch .seg-btn { flex:none; padding:6px 11px; font-size:12px; border-radius:9px; cursor:default; }
.seg-btn.is-locked { color:#5a627a; border-color:rgba(138,147,173,0.25); background:rgba(138,147,173,0.06); }

/* draggable divider: 6px of visible rail, a wider invisible hit area, a visible grip */
.lab-divider {
  position:relative; flex:none; width:6px; margin:0 5px; cursor:col-resize;
  display:flex; align-items:center; justify-content:center; border-radius:999px;
  background:transparent; transition:background .15s;
  /* Without this it computes to `auto`, which means the browser gets to decide a finger dragging
     here is a scroll — fire pointercancel, stop sending pointermove, and the pane never resizes.
     preventDefault in the handler cannot take that back, because the decision is made before any
     handler runs. This is the one fix in the iPad pass that emulation cannot verify: a synthetic
     pointer event bypasses gesture arbitration entirely, so the drag "worked" either way. */
  touch-action:none;
}
/* The visible line is 6px; this widens what a finger has to hit. 22px on a mouse, 44 on touch —
   Apple's own floor, and this is a precision drag with nothing else nearby to compete for the
   press, so the wider grab costs nothing. */
.lab-divider::before { content:""; position:absolute; top:0; bottom:0; left:-8px; right:-8px; }
@media (hover: none) { .lab-divider::before { left:-19px; right:-19px; } }
/* `lab-resizing` is added to <body> for the length of a drag (lab.js) and had NO RULE ANYWHERE, so
   it read as if it did something and did nothing. The usual intent, and genuinely useful for a
   touch drag: keep the resize cursor while the pointer wanders off the 6px line, and stop the drag
   selecting the text in either pane as it crosses them. */
body.lab-resizing { cursor:col-resize; -webkit-user-select:none; user-select:none; }
.lab-divider span {
  width:4px; height:52px; border-radius:999px; background:rgba(43,140,255,0.3);
  transition:background .15s, height .15s;
}
.lab-divider:hover span, .lab-divider.is-dragging span { background:var(--blue); height:76px; }
.lab-divider.is-dragging { background:rgba(43,140,255,0.08); }

/* ============================ blocks pane ============================ */
.lab-workspace {
  flex:1; min-height:0; width:100%; position:relative;
  background:
    radial-gradient(circle at center, rgba(43,140,255,0.14) 1px, transparent 1.2px) 0 0/22px 22px,
    linear-gradient(180deg, rgba(5,3,15,0.55), rgba(5,3,15,0.8));
}
.lab-workspace[hidden] { display:none; }
.lab-workspace.is-readonly { opacity:.55; filter:grayscale(.35); }
/* previewVersion/endPreview/doRestore (lab.js) toggle .is-previewing on #lab-editor — this is the
   half of that state nothing styled: the class existed, set ws.options.readOnly, and otherwise did
   nothing a child could see or feel. .lab-workspace holds Blockly's toolbox and flyout as well as
   the canvas, so pointer-events:none here silences the palette too, not just dragging a block. */
#lab-editor.is-previewing .lab-workspace { opacity:.55; filter:grayscale(.35); pointer-events:none; }
.lab-workspace-empty {
  position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:8px; pointer-events:none; color:var(--muted); font-size:14px; text-align:center; padding:20px;
}

/* Toolbox category swatches. The Blockly workspace itself is themed in JS — these are the
   swatch colours (design doc §7) exposed both as tokens and as a ready-made dot class. */
.lab-pane-blocks {
  --cat-game:#b794ff; --cat-sprites:#78b4ff; --cat-input:#4fd6c4; --cat-collisions:#fb8ba0;
  --cat-world:#7ee08a; --cat-sound:#f79bd6; --cat-logic:#fbbf24; --cat-loops:#fb923c;
  --cat-variables:#818cf8; --cat-numbers:#38bdf8;
}
.lab-cat-swatch {
  display:inline-block; flex:none; width:12px; height:12px; border-radius:4px;
  background:var(--cat, var(--blue)); box-shadow:0 0 8px color-mix(in srgb, var(--cat, var(--blue)) 60%, transparent);
}
.lab-cat-swatch[data-cat="game"]       { --cat:var(--cat-game); }
.lab-cat-swatch[data-cat="sprites"]    { --cat:var(--cat-sprites); }
.lab-cat-swatch[data-cat="input"]      { --cat:var(--cat-input); }
.lab-cat-swatch[data-cat="collisions"] { --cat:var(--cat-collisions); }
.lab-cat-swatch[data-cat="world"]      { --cat:var(--cat-world); }
.lab-cat-swatch[data-cat="sound"]      { --cat:var(--cat-sound); }
.lab-cat-swatch[data-cat="logic"]      { --cat:var(--cat-logic); }
.lab-cat-swatch[data-cat="loops"]      { --cat:var(--cat-loops); }
.lab-cat-swatch[data-cat="variables"]  { --cat:var(--cat-variables); }
.lab-cat-swatch[data-cat="numbers"]    { --cat:var(--cat-numbers); }

/* an invalid block: red ring + pulse (class applied to the block's SVG group by studio.js) */
@keyframes lab-invalid-pulse {
  0%,100% { filter:drop-shadow(0 0 0 rgba(255,93,108,0.55)); }
  50%     { filter:drop-shadow(0 0 9px rgba(255,93,108,0.95)); }
}
.lab-invalid-block { animation:lab-invalid-pulse 1.3s ease-in-out infinite; }
.lab-invalid-block .blocklyPath { stroke:var(--red); stroke-width:3px; }

/* ============================ assets manager ============================ */
.lab-assets { flex:1; min-height:0; overflow-y:auto; padding:18px; }
.lab-assets[hidden] { display:none; }
.lab-assets-title { font-family:var(--f-display); font-weight:600; font-size:16px; margin:0 0 6px; }
.lab-assets-help { font-size:13px; color:var(--muted); line-height:1.5; margin:0 0 22px; max-width:560px; }
.lab-assets-help strong { color:var(--fg); font-weight:600; }

/* One TAB PANEL per kind — the three used to be stacked sections separated by a rule, and that rule
   had to go with them: a hidden sibling still counts for `+`, so the visible panel inherited a
   divider above it with nothing on the other side. Only one is ever displayed. */
.asset-section[hidden] { display:none; }

/* The tab row. `.seg` gives it the pill styling every other segmented control in the Lab uses; all
   this adds is room for the count and a full-width stretch, because three words in a row that stops
   halfway across reads as a stray toolbar rather than the navigation for the pane below it. */
.lab-assets-tabs { display:flex; width:100%; margin:0 0 20px; }
.lab-assets-tabs .seg-btn { flex:1; display:flex; align-items:center; justify-content:center; gap:7px; }
/* The count is the point of the tabs: "Sounds 0" is the most useful thing this pane can say, and it
   is exactly what the old stacked layout buried under a sprite list that grows without limit. So it
   never hides at zero — it goes quiet instead. */
.asset-tab-count {
  min-width:19px; padding:1px 5px; border-radius:9px; font-size:11px; font-weight:700;
  background:rgba(43,140,255,0.16); color:var(--muted);
}
.lab-assets-tabs .seg-btn.is-active .asset-tab-count { background:rgba(255,255,255,0.22); color:inherit; }
.asset-section-note { font-size:12.5px; color:var(--muted); line-height:1.5; margin:0 0 14px; max-width:560px; }
.asset-section-note strong { color:var(--fg); font-weight:600; }
/* Which files will be accepted — said before the picker opens, not after it's rejected. */
.asset-types { color:var(--muted-2); font-weight:600; }

/* A sound or a picture: one file, one name, one row. */
.asset-row {
  display:flex; align-items:center; gap:12px; padding:10px 12px; margin-bottom:8px;
  border-radius:13px; border:1px solid rgba(43,140,255,0.2); background:rgba(7,104,255,0.05);
}
.asset-row-icon {
  flex:none; width:38px; height:38px; border-radius:11px; display:flex; align-items:center;
  justify-content:center; font-size:18px;
  background:rgba(43,140,255,0.1); border:1px solid rgba(43,140,255,0.25);
}
.asset-row-thumb {
  flex:none; width:38px; height:38px; border-radius:11px; overflow:hidden;
  background:rgba(43,140,255,0.06); border:1px solid rgba(43,140,255,0.25);
}
.asset-row-thumb img { width:100%; height:100%; object-fit:contain; image-rendering:pixelated; }
.asset-row-id { display:flex; flex-direction:column; gap:2px; min-width:0; margin-right:auto; }
.asset-row-name { font-family:var(--f-display); font-weight:600; font-size:14px; color:var(--fg); }
.asset-row-file {
  font-family:var(--f-mono); font-size:10.5px; letter-spacing:.5px; color:var(--muted);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

/* What kinds of file this dialog will take, sitting just above the picker button. */
.lab-asset-types { font-size:12px; color:var(--muted-2); font-weight:600; margin:0 0 4px; }

.sprite-card {
  border-radius:16px; border:1px solid rgba(43,140,255,0.2); background:rgba(7,104,255,0.05);
  padding:14px 16px; margin-bottom:14px;
}
.sprite-card-head { display:flex; align-items:center; gap:10px; margin-bottom:12px; }
.sprite-name {
  width:auto; max-width:220px; padding:5px 9px; border-radius:9px; background:transparent;
  border:1px solid transparent; font-family:var(--f-display); font-weight:600; font-size:15px; color:var(--fg);
}
.sprite-name:hover { border-color:rgba(43,140,255,0.25); }
.sprite-count { font-size:12px; color:var(--muted); }
.sprite-card-actions { display:flex; align-items:center; gap:6px; margin-left:auto; }

.sprite-states { display:flex; gap:12px; overflow-x:auto; padding-bottom:6px; }
.sprite-state, .sprite-state-add {
  flex:none; width:96px; border-radius:13px; display:flex; flex-direction:column;
  align-items:center; gap:6px; padding:9px; text-align:center;
}
.sprite-state { position:relative; border:1px solid rgba(43,140,255,0.22); background:rgba(5,3,15,0.6); }
/* The thumbnail IS the swap button — clicking a picture to change it is the guess a child makes
   first, so it should be the thing that works. A real <button>, hence the appearance reset. */
.sprite-state-thumb {
  width:100%; aspect-ratio:1/1; border-radius:9px; background-size:contain;
  background-position:center; background-repeat:no-repeat;
  background-color:rgba(43,140,255,0.06); image-rendering:pixelated;
  appearance:none; border:none; padding:0; position:relative; cursor:pointer; overflow:hidden;
}
.sprite-state-swap {
  position:absolute; inset:auto 0 0 0; padding:3px 0; font-size:10.5px; font-weight:700;
  font-family:var(--f-ui); color:#eaf2ff; background:rgba(43,140,255,0.85);
  opacity:0; transition:opacity .12s;
}
.sprite-state-thumb:hover .sprite-state-swap,
.sprite-state-thumb:focus-visible .sprite-state-swap { opacity:1; }
/* Where there is no hover, the word is the only thing that says a picture can be replaced at all —
   and the thumbnail IS the button, so without it the control works and nobody knows it is there.
   `aria-hidden` on the span means a screen reader was never the one missing out; this is for the
   sighted child. Reachable, undiscoverable is still a fault. */
@media (hover: none) { .sprite-state-swap { opacity:1; } }
.sprite-state-thumb:focus-visible { outline:2px solid var(--blue); outline-offset:2px; }
/* The look's name, and the button that renames it. It reads as a label until a pointer is over it,
   the same bargain .sprite-name makes one row up: the name is the useful thing on a 96px tile, and
   an always-visible outline on every look would turn a row of pictures into a row of form controls.
   Explicitly reset from the browser's button defaults — it used to be a <span>, and inheriting
   font-family/appearance is exactly what a bare <button> does not do. */
.sprite-state-label {
  display:inline-flex; align-items:center; gap:4px; max-width:100%;
  padding:2px 6px; border-radius:7px; border:1px solid transparent; background:transparent;
  font-family:inherit; font-size:12px; font-weight:600; color:var(--fg);
  cursor:pointer; text-align:center;
}
.sprite-state-label:hover { border-color:rgba(43,140,255,0.25); }
.sprite-state-label:focus-visible { outline:2px solid var(--blue); outline-offset:1px; }
/* The pencil is the affordance, so it may not vanish on the touch screens that have no hover — it
   sits at low opacity always and comes up when a pointer or keyboard reaches it. */
.sprite-state-label .sprite-pencil { opacity:.45; font-size:10px; }
.sprite-state-label:hover .sprite-pencil, .sprite-state-label:focus-visible .sprite-pencil { opacity:1; }
/* Renaming a look happens on a 96px tile, so the input has to fit it rather than the card. */
.sprite-rename.is-look { width:100%; max-width:88px; font-size:12px; padding:3px 6px; }

/* The look's name inside the pixel editor's heading. It has to read as part of the <h3> — a child
   should see "Drawing: player · idle", not a heading with a form control bolted on — so it inherits
   the heading's own type and only reveals itself as pressable on hover, the same bargain the tile's
   label and the sprite name both make. */
.lab-paint-rename {
  display:inline-flex; align-items:center; gap:5px;
  padding:1px 7px; border-radius:8px; border:1px solid transparent; background:transparent;
  font:inherit; color:inherit; cursor:pointer; vertical-align:baseline;
}
.lab-paint-rename[hidden] { display:none; }
.lab-paint-rename:hover { border-color:rgba(43,140,255,0.35); }
.lab-paint-rename:focus-visible { outline:2px solid var(--blue); outline-offset:1px; }
.lab-paint-rename-input { max-width:150px; font-size:15px; vertical-align:baseline; }
.sprite-state-file { font-family:var(--f-mono); font-size:10px; letter-spacing:.5px; color:var(--muted); word-break:break-all; }
.sprite-state.is-uploading { opacity:.6; }
.sprite-state.is-failed { border-color:rgba(255,93,108,0.5); }

/* dashed dropzones: add-a-state tile + the new-sprite card */
.sprite-state-add, .sprite-new {
  border:1.5px dashed rgba(43,140,255,0.4); background:rgba(43,140,255,0.04);
  color:var(--muted-2); cursor:pointer; font-family:var(--f-ui);
  transition:border-color .15s, background .15s, color .15s;
}
.sprite-state-add { justify-content:center; min-height:120px; font-size:12.5px; font-weight:600; }
.sprite-state-add:hover, .sprite-new:hover,
.sprite-state-add.is-dropping, .sprite-new.is-dropping {
  border-color:var(--blue); background:rgba(43,140,255,0.1); color:var(--blue);
}
.sprite-new {
  display:flex; align-items:center; gap:16px; text-align:left; width:100%;
  padding:20px; border-radius:16px; margin-top:4px;
}
.sprite-new-icon {
  flex:none; width:48px; height:48px; border-radius:14px; display:flex; align-items:center; justify-content:center;
  font-size:22px; background:rgba(43,140,255,0.1); border:1px solid rgba(43,140,255,0.25); color:var(--blue);
}
.sprite-new-title { font-family:var(--f-display); font-weight:600; font-size:15px; margin:0 0 4px; color:var(--fg); }
.sprite-new-desc { font-size:12.5px; color:var(--muted); line-height:1.5; margin:0; }

/* Sprites are made group-first: this button makes an empty group, then you add looks into it. */
.sprite-group-add {
  display:flex; align-items:center; gap:10px; width:100%; padding:12px 16px; margin:0 0 10px;
  border:1.5px dashed rgba(43,140,255,0.4); background:rgba(43,140,255,0.04); border-radius:12px;
  color:var(--muted-2); cursor:pointer; font-family:var(--f-ui); font-weight:600; font-size:13.5px;
  transition:border-color .15s, background .15s, color .15s;
}
.sprite-group-add:hover { border-color:var(--blue); background:rgba(43,140,255,0.1); color:var(--blue); }
.sprite-group-add-icon {
  flex:none; width:26px; height:26px; border-radius:8px; display:flex; align-items:center; justify-content:center;
  font-size:17px; background:rgba(43,140,255,0.1); border:1px solid rgba(43,140,255,0.25); color:var(--blue);
}
/* The inline namer that opens under the button when you're naming a new group. */
.sprite-group-namer { display:flex; gap:8px; align-items:center; margin:0 0 10px; }
.sprite-group-namer input {
  flex:1; padding:9px 12px; border-radius:10px; border:1.5px solid var(--blue);
  background:var(--bg); color:var(--fg); font-family:var(--f-ui); font-size:16px;
}
/* A just-made group has no looks yet — a small nudge to add the first one. */
.sprite-empty-note { font-size:12.5px; color:var(--muted); margin:0 0 8px; }

/* "Browse the gallery" — pull shared assets in. Sits under the assets intro line. */
.lab-gallery-open {
  display:inline-flex; align-items:center; gap:7px; margin:2px 0 14px; padding:9px 14px;
  border-radius:10px; border:1px solid rgba(43,140,255,0.35); background:rgba(43,140,255,0.08);
  color:var(--blue); cursor:pointer; font-family:var(--f-ui); font-weight:600; font-size:13px;
  transition:background .15s, border-color .15s;
}
.lab-gallery-open:hover { background:rgba(43,140,255,0.16); border-color:var(--blue); }

/* The gallery browser modal (reuses the .lab-modal-wrap scrim). Wider than the naming dialog. */
.lab-gallery {
  position:relative; z-index:1; width:min(760px, 94vw); max-height:88vh; display:flex; flex-direction:column;
  background:var(--panel); border:1px solid var(--line); border-radius:18px; padding:20px 22px;
  box-shadow:0 24px 60px rgba(0,0,0,0.5);
}
.lab-gallery-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.lab-gallery-head h3 { font-family:var(--f-display); font-size:19px; margin:0; color:var(--fg); }
.lab-gallery-sub { font-size:13px; color:var(--muted); margin:3px 0 0; }
/* The search box. 16px on the input is not a style choice — anything smaller makes iOS zoom the
   page on focus and never zoom back, which is the trap e2e/ipad.spec.js exists to catch. */
.lab-gallery-search {
  display:flex; align-items:center; gap:8px; margin:14px 0 0; padding:0 12px;
  border-radius:12px; border:1px solid var(--line); background:rgba(3,2,10,0.45);
}
.lab-gallery-search:focus-within { border-color:var(--blue); background:rgba(43,140,255,0.06); }
.lab-gallery-search-icon { flex:none; font-size:14px; opacity:0.75; }
#lab-gallery-q {
  flex:1; min-width:0; padding:11px 0; border:0; background:transparent; color:var(--fg);
  font-family:var(--f-ui); font-size:16px;
}
#lab-gallery-q::placeholder { color:var(--muted); }
#lab-gallery-q:focus { outline:none; }
/* The browser's own clear affordance is a tiny grey x with no hit area worth the name. */
#lab-gallery-q::-webkit-search-cancel-button { display:none; }
.lab-gallery-search-x {
  flex:none; width:30px; height:30px; border:0; border-radius:8px; cursor:pointer;
  background:transparent; color:var(--muted); font-size:13px;
}
.lab-gallery-search-x:hover { background:rgba(43,140,255,0.12); color:var(--fg); }
.lab-gallery-search-x[hidden] { display:none; }

/* Breadcrumb: All › Category › Sprite — the way down the tree when nobody is typing. */
.lab-gallery-crumbs { display:flex; flex-wrap:wrap; align-items:center; gap:4px; margin:14px 0 12px; }
.gallery-crumb {
  padding:4px 9px; border-radius:8px; border:none; background:transparent; cursor:pointer;
  color:var(--blue); font-family:var(--f-ui); font-size:13px; font-weight:600;
}
.gallery-crumb[data-crumb]:hover { background:rgba(43,140,255,0.1); }
.gallery-crumb.is-current { color:var(--fg); cursor:default; }
.gallery-crumb-sep { color:var(--muted); font-size:13px; }
.lab-gallery-grid {
  overflow-y:auto; display:grid; gap:12px; padding:2px;
  grid-template-columns:repeat(auto-fill, minmax(112px, 1fr));
}
.gallery-card {
  display:flex; flex-direction:column; align-items:center; gap:6px; padding:10px 8px;
  border:1px solid var(--line); border-radius:12px; background:var(--bg); cursor:pointer; text-align:center;
  transition:border-color .12s, background .12s, transform .06s;
}
.gallery-card:hover { border-color:var(--blue); background:rgba(43,140,255,0.06); }
.gallery-card:active { transform:scale(0.97); }
.gallery-card:focus-visible { outline:2px solid var(--blue); outline-offset:2px; }
.gallery-thumb {
  position:relative; width:64px; height:64px; border-radius:9px; display:flex; align-items:center; justify-content:center;
  /* a light checker so transparent PNGs read clearly on any theme */
  background-color:#c8c8c8;
  background-image:linear-gradient(45deg,#a9a9a9 25%,transparent 25%,transparent 75%,#a9a9a9 75%),
                   linear-gradient(45deg,#a9a9a9 25%,transparent 25%,transparent 75%,#a9a9a9 75%);
  background-size:14px 14px; background-position:0 0,7px 7px;
}
.gallery-thumb img { max-width:100%; max-height:100%; image-rendering:pixelated; }
.gallery-thumb.is-sound { background:rgba(43,140,255,0.1); font-size:26px; }

/* Four examples on a category card instead of one. "Objects" is a word that means nothing to a
   child until they can see it holds barrels and keys — one thumbnail was as informative as a
   folder icon. Laid out as a 2x2 so the card keeps the same footprint as an item card. */
.gallery-peeks {
  display:grid; grid-template-columns:1fr 1fr; gap:3px;
  aspect-ratio:1; padding:6px; border-radius:10px; background:rgba(3,2,10,0.5);
}
.gallery-peek {
  display:flex; align-items:center; justify-content:center; overflow:hidden;
  border-radius:6px; background:rgba(255,255,255,0.03);
}
.gallery-peek img { max-width:100%; max-height:100%; image-rendering:pixelated; }
.gallery-peek.is-sound { font-size:15px; }
.gallery-play {
  position:absolute; right:-4px; bottom:-4px; width:22px; height:22px; border-radius:50%;
  border:none; background:var(--blue); color:#fff; font-size:10px; cursor:pointer; line-height:1;
}
/* Playing right now. One sound plays at a time, so this is the whole state there is to show — and it
   has to be visible at a glance on a grid of sixty identical speaker tiles, which is why it is the
   colour that changes and not just the glyph. */
.gallery-play.is-playing, .asset-play.is-playing {
  background:var(--ok, #22c55e); color:#fff; animation:asset-pulse 1.1s ease-in-out infinite;
}
.asset-play.is-playing { border-color:transparent; }
@keyframes asset-pulse { 0%, 100% { opacity:1; } 50% { opacity:0.55; } }
@media (prefers-reduced-motion: reduce) { .gallery-play.is-playing, .asset-play.is-playing { animation:none; } }
.gallery-card-name { font-size:12.5px; font-weight:600; color:var(--fg); word-break:break-word; }
.gallery-card-meta { font-size:11px; color:var(--muted); }
@media (max-width:560px) {
  .lab-gallery-grid { grid-template-columns:repeat(auto-fill, minmax(92px, 1fr)); }
}

/* Blockly ships its own font stack, so the category list down the left (Game, Sprites, …) rendered
   in a system sans while everything around it is Nunito. The toolbox is OUR chrome, not part of the
   canvas, so it takes the site's display face. Block labels are left alone deliberately: Blockly
   measures those to lay blocks out, and changing the face there moves every block's geometry. */
/* The workspace bin: Blockly draws it dim (0.4). Make it more present, so it's an obvious place to
   drop a block you don't want. Its position is centred under the zoom controls by studio.js. */
.lab-workspace .blocklyTrash { opacity:0.9 !important; }

/* The "what is this category for" note, shown while a block flyout is open. Top-right so it never
   sits under the left-hand flyout; non-interactive so it never eats a drag. */
.lab-catnote {
  position:absolute; top:12px; right:14px; z-index:30;
  max-width:min(360px, 48%); margin:0; padding:10px 13px;
  background:rgba(10,14,38,0.95); border:1px solid rgba(122,182,255,0.32);
  border-radius:12px; box-shadow:0 8px 24px rgba(0,0,0,0.38);
  color:#cdd6ef; font-size:12.5px; line-height:1.45; pointer-events:none;
  animation:lab-catnote-in 140ms ease-out;
}
.lab-catnote[hidden] { display:none; }
.lab-catnote b { color:#fff; font-weight:700; }
.lab-catnote em { color:#7ab6ff; font-style:normal; font-weight:600; }
@keyframes lab-catnote-in { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:none; } }

/* The "Create variable…" button that heads the Variables flyout. Blockly draws it as a pale grey
   pill that looks pasted-on over the dark canvas — restyle it to our blue action colour. These are
   SVG nodes, so we set fill/stroke, not background. */
.lab-workspace .blocklyFlyoutButtonShadow { display:none; }
.lab-workspace .blocklyFlyoutButtonBackground {
  fill:var(--blue, #2b8cff); stroke:rgba(255,255,255,0.22); stroke-width:1px;
}
.lab-workspace .blocklyFlyoutButton:hover .blocklyFlyoutButtonBackground { fill:#4a9cff; }
.lab-workspace .blocklyFlyoutButton .blocklyText { fill:#fff; font-weight:600; }

/* The little text box that opens when you click a number/text field on a block. Blockly colours it
   from the block, which on our dark blocks comes out dark-on-dark — you can't see what you type. Pin
   it to a readable dark box with white text and a blue caret. Not scoped to .lab-workspace because
   Blockly appends this widget to <body>. */
.blocklyHtmlInput {
  background:#0a0e26 !important; color:#ffffff !important;
  border:1.5px solid var(--blue, #2b8cff) !important; border-radius:6px !important;
  box-shadow:0 6px 18px rgba(0,0,0,0.45) !important; caret-color:var(--blue, #2b8cff);
}

.blocklyToolboxDiv, .blocklyTreeLabel, .blocklyToolboxCategory, .blocklyFlyoutLabelText {
  font-family:var(--f-display) !important;
}
.blocklyTreeLabel { font-size:13.5px !important; font-weight:600; letter-spacing:.1px; }

/* ---- the code + who-can-play chip, in the toolbar ---- */
/* padding:9px, not the original 6px — once "Game code" moved off its own stacked line and into
   this chip (see renderStatusChip in lab.js), the chip became the only single-line item left in
   the row shorter than the 38px pills either side of it; the taller padding lines its own height
   up with theirs. */
.lab-statuschip {
  display:inline-flex; align-items:center; gap:6px; cursor:pointer; font:inherit; font-size:12.5px;
  padding:9px 11px; border-radius:9px; white-space:nowrap;
  background:rgba(43,140,255,0.08); border:1px solid var(--line); color:var(--fg);
  transition:border-color .12s ease, background .12s ease;
}
.lab-statuschip:hover { border-color:var(--line-strong); background:rgba(43,140,255,0.14); }
/* The icon takes the chip's colour, so each state is said once — by the glyph and the words
   together — rather than by an emoji insisting on its own palette. */
.lab-statuschip-text { display:inline-flex; align-items:center; gap:6px; }
/* The code itself, when there is one to show: the old chip's mono, spaced-out treatment, so it is
   read letter by letter — that is how it gets typed into a mebobox. */
.lab-statuschip-code { font-family:var(--f-mono); font-weight:600; font-size:14px; letter-spacing:2.5px; }
.lab-statuschip-sep { opacity:.45; }
.lab-statuschip-copy { font-size:12px; opacity:.7; }
.lab-statuschip.copied { background:rgba(70,224,138,0.22); border-color:rgba(70,224,138,0.6); }
/* Not built yet is the call to action, in the same amber the Details note uses, so the two agree at
   a glance; friends-only reads as fine, public as a real green light. */
.lab-statuschip[data-state="draft"]   { color:#f7d774; border-color:rgba(234,179,8,0.32); background:rgba(234,179,8,0.08); }
.lab-statuschip[data-state="draft"]:hover { border-color:rgba(234,179,8,0.6); background:rgba(234,179,8,0.14); }
.lab-statuschip[data-state="code"]    { color:var(--green); border-color:rgba(70,224,138,0.4); background:rgba(70,224,138,0.1); }
.lab-statuschip[data-state="code"]:hover { border-color:rgba(70,224,138,0.7); background:rgba(70,224,138,0.18); }
.lab-statuschip[data-state="waiting"] { color:#f7d774; border-color:rgba(234,179,8,0.32); background:rgba(234,179,8,0.08); }
.lab-statuschip[data-state="public"]  { color:#7ee08a; border-color:rgba(70,224,138,0.32); background:rgba(70,224,138,0.08); }

/* ---- game details ---- */
.lab-details {
  position:relative; z-index:1; width:min(560px, calc(100vw - 32px)); max-height:calc(100dvh - 64px);
  overflow-y:auto; padding:20px; border-radius:16px; background:var(--panel-1);
  border:1px solid var(--line); box-shadow:0 24px 60px rgba(0,0,0,0.5);
}
.lab-details h3 {
  margin:0 0 14px; font-family:var(--f-display); font-weight:700; font-size:18px; color:var(--fg);
}
.lab-details .field { margin-bottom:16px; }
.lab-details textarea {
  width:100%; resize:vertical; min-height:66px; font:inherit; padding:9px 11px; border-radius:10px;
  background:rgba(5,3,15,0.5); border:1px solid var(--line); color:var(--fg);
}
.lab-details-count { margin:4px 0 0; text-align:right; font-size:11.5px; color:var(--muted); }
.lab-details-hint { margin:4px 0 0; font-size:12px; line-height:1.45; color:var(--muted); }

.lab-cover { display:flex; gap:14px; align-items:flex-start; margin-top:8px; }
.lab-cover-preview {
  flex:none; width:132px; aspect-ratio:4/3; display:flex; align-items:center; justify-content:center;
  overflow:hidden; border-radius:10px; background:rgba(5,3,15,0.6);
  border:1px dashed rgba(43,140,255,0.35);
}
.lab-cover-preview img { width:100%; height:100%; object-fit:cover; }
.lab-cover-empty { font-size:11.5px; color:var(--muted); text-align:center; padding:6px; }
.lab-cover-actions { display:flex; flex-direction:column; gap:8px; align-items:flex-start; }
.ghostbtn.sm { padding:8px 14px; border-radius:10px; font-size:13px; }
.ghostbtn.sm.danger { color:var(--red); border-color:rgba(239,68,68,0.35); }

.lab-visibility { border:none; margin:0 0 16px; padding:0; }
.lab-visibility legend {
  padding:0; margin-bottom:8px; font-size:13px; font-weight:600; color:var(--fg);
}
.lab-vis-option {
  display:flex; gap:10px; align-items:flex-start; padding:10px 11px; margin-bottom:8px;
  border-radius:10px; background:rgba(5,3,15,0.4); border:1px solid var(--line-soft); cursor:pointer;
}
.lab-vis-option:has(input:checked) {
  border-color:rgba(43,140,255,0.45); background:rgba(43,140,255,0.09);
}
/* account.css styles bare `input` as a full-width text field (width:100%, 12px padding). That is
   right for every other input on the site and very wrong for a radio, which it stretched across the
   row and left the text crushed into a column beside it. */
.lab-vis-option input[type="radio"] {
  width:auto; flex:none; margin:3px 0 0; padding:0; border:none; background:none;
}
.lab-vis-option > span { flex:1; min-width:0; }
/* No locked state on the visibility options any more: every game may ASK to be listed, blocks or
   code. The rules that dimmed the public option and printed "comes when you level up" went with
   the level that gated it. */
.lab-vis-option strong { display:block; font-size:13px; font-weight:600; color:var(--fg); }
.lab-vis-option em { display:block; margin-top:2px; font-size:12px; font-style:normal; color:var(--muted); }
.lab-vis-state {
  margin:2px 0 0; padding:9px 11px; border-radius:9px; font-size:12.5px; line-height:1.45;
  background:rgba(234,179,8,0.1); border:1px solid rgba(234,179,8,0.3); color:#f7d774;
}
.lab-vis-state[hidden] { display:none; }
@media (max-width:520px) {
  .lab-cover { flex-direction:column; }
}

/* ---- controls help, in the Run screen ---- */
.lab-keys { position:relative; display:flex; }
.lab-keys-menu {
  position:absolute; bottom:calc(100% + 10px); left:0; z-index:70; width:min(340px, calc(100vw - 40px));
  padding:14px; border-radius:12px; background:#0d1330; border:1px solid rgba(43,140,255,0.3);
  box-shadow:0 20px 44px rgba(0,0,0,0.5);
  /* It opens UPWARD from a button near the bottom of the screen, so its ceiling is the viewport,
     not its content — the full twelve-button table clipped its own title off the top of small
     windows until it learnt to scroll. */
  max-height:calc(100dvh - 130px); overflow-y:auto;
}
.lab-keys-menu[hidden] { display:none; }
.lab-keys-title {
  margin:0 0 8px; font-family:var(--f-display); font-weight:700; font-size:13.5px; color:var(--fg);
}
.lab-keys-why { margin:8px 0 0; font-size:12px; line-height:1.5; color:var(--muted); }
.lab-keys-table { width:100%; margin-top:10px; border-collapse:collapse; font-size:12.5px; }
.lab-keys-table th {
  text-align:left; padding:4px 6px; font-size:10.5px; text-transform:uppercase; letter-spacing:.4px;
  color:var(--muted); border-bottom:1px solid var(--line-soft); font-weight:700;
}
.lab-keys-table td { padding:5px 6px; color:var(--fg); border-bottom:1px solid var(--line-soft); }
.lab-keys-table tr:last-child td { border-bottom:none; }
.lab-keys-table td:first-child { font-weight:600; }
kbd {
  display:inline-block; margin-right:3px; padding:1px 6px; border-radius:5px; font-size:11.5px;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background:rgba(43,140,255,0.14); border:1px solid rgba(43,140,255,0.32); color:#cfe0ff;
}

/* ---- the colour (Look) popover ---- */
.lab-look { position:relative; display:flex; }
.lab-look-menu {
  position:absolute; top:calc(100% + 8px); right:0; z-index:60; width:230px;
  padding:12px; border-radius:12px; background:#0d1330;
  border:1px solid rgba(43,140,255,0.28); box-shadow:0 18px 40px rgba(0,0,0,0.45);
}
.lab-look-menu[hidden] { display:none; }
.lab-look-title {
  margin:0 0 10px; font-family:var(--f-display); font-weight:600; font-size:13.5px; color:var(--fg);
}
.lab-look-row {
  display:flex; align-items:center; gap:10px; margin-bottom:8px; font-size:13px; color:var(--fg);
  cursor:pointer;
}
.lab-look-row input[type="color"] {
  flex:none; width:34px; height:26px; padding:0; cursor:pointer; background:none;
  border:1px solid rgba(43,140,255,0.3); border-radius:7px;
}
.lab-look-note { margin:10px 0 0; font-size:11.5px; line-height:1.45; color:var(--muted); }

/* ============================ code pane ============================ */
/* File tabs. One block makes three things, so the panel shows three files — and a tab carries a dot
   when the selected block put something in THAT file. Without the dot, selecting a "show text"
   block (an element and a rule, no TypeScript) would light up nothing at all with the code tab
   open, and the child would conclude the block does nothing. */
.lab-files { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
/* Each tab says what its file IS, not just what it's called. All three captions are visible at
   once on purpose: the three-way split — behaviour, structure, presentation — is the thing being
   taught, and it only reads as a set if you can see the set. */
.lab-file {
  position:relative; appearance:none; cursor:pointer; font:inherit; text-align:left;
  display:flex; flex-direction:column; gap:1px;
  padding:5px 11px 6px; border-radius:9px;
  border:1px solid transparent; background:transparent; color:var(--muted);
  transition:background .12s ease, color .12s ease, border-color .12s ease;
}
.lab-file-name {
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:12.5px;
}
.lab-file-what { font-size:10.5px; line-height:1.2; color:var(--muted); opacity:.85; }
.lab-file.is-active .lab-file-what { color:#9fc2ff; opacity:1; }

/* At level 2 these stop being an explanation of generated output and become ordinary editor tabs:
   filename only, squared off, with the active one seated against the editor below it. The captions
   are teaching aids for level 1 — here they are noise above someone's own code. */
.lab-editor.is-code .lab-file-what { display:none; }
/* The tab strip IS the pane header — same height as "Files" opposite it, with the tabs running the
   full height so the open one meets the editor with no seam. Previously they floated in a padded
   header and read as detached from the thing they belong to. */
.lab-editor.is-code .lab-pane-code > .lab-pane-head {
  padding:0 10px; min-height:41px; align-items:stretch; gap:0;
}
.lab-editor.is-code .lab-files { gap:0; align-items:stretch; }
/* Inset from the top so the tab is a shape sitting IN the header rather than filling it. */
.lab-editor.is-code .lab-file {
  padding:0 14px; margin-top:5px; border-radius:8px 8px 0 0;
  border:1px solid transparent; border-bottom:none;
}
/* Outlined on three sides and open at the bottom — the classic document tab, where the missing
   edge is what joins it to the editor. A bar underneath said the opposite: it drew a line exactly
   where the tab and its file should meet. */
.lab-editor.is-code .lab-file.is-active {
  background:rgba(5,3,15,0.8); border-color:rgba(43,140,255,0.38); color:var(--fg);
}
.lab-editor.is-code .lab-pane-tools { align-items:center; }
.lab-file:hover { color:var(--fg); background:rgba(43,140,255,0.08); }
.lab-file.is-active {
  color:var(--fg); background:rgba(43,140,255,0.14); border-color:rgba(43,140,255,0.3);
}
/* Nothing in this file yet — still listed, because a tab that appears out of nowhere the first time
   you show something is a surprise, and "you haven't put anything on screen yet" is worth saying. */
.lab-file.is-empty { opacity:.5; }
.lab-file.has-link::after {
  content:''; position:absolute; top:3px; right:4px; width:6px; height:6px; border-radius:50%;
  background:var(--blue); box-shadow:0 0 0 2px rgba(8,10,28,0.9);
}
.lab-file.is-active.has-link::after { background:#46e08a; }

/* The tab a just-made edit landed in glows. Editing one block can rewrite a file you are not
   looking at — recolouring a shown variable changes a CSS rule while the code tab is open — and a
   change nobody sees is a change nobody learns from. */
@keyframes studioTabPulse {
  0%   { box-shadow:0 0 0 0 rgba(70,224,138,0); background:rgba(70,224,138,0.05); }
  35%  { box-shadow:0 0 0 3px rgba(70,224,138,0.28); background:rgba(70,224,138,0.2); }
  100% { box-shadow:0 0 0 0 rgba(70,224,138,0); background:rgba(70,224,138,0); }
}
.lab-file.just-changed { animation:studioTabPulse 1s ease-out; }
@media (prefers-reduced-motion:reduce) {
  .lab-file { transition:none; }
  /* No pulse, but the change still has to be findable — so hold a steady tint instead. */
  .lab-file.just-changed { animation:none; background:rgba(70,224,138,0.16); }
}

/* Bigger than the 12.5px it started at. This line is the one thing on the pane that explains what
   the pane IS — it now answers "why is my game playing, and why won't it move?" before a child can
   decide the game is broken — and at the old size it read as a caption, which is to say as
   something safe to skip. There is room: it is one line above a picture with space to spare. */
.lab-codebanner {
  flex:none; margin:0; padding:12px 16px; font-size:14.5px; line-height:1.5; color:#d5dcef;
  background:rgba(7,104,255,0.06); border-bottom:1px solid rgba(43,140,255,0.12);
}
.lab-monaco {
  flex:1; min-height:200px; width:100%; overflow:hidden; background:rgba(5,3,15,0.8);
}
/* A class that sets `display` outranks the browser's own [hidden] rule, so it has to restate it —
   the same lesson `.lab-pane[hidden]` learned further up this file. Without this the Game view and
   the editor would both be on screen at once, stacked. */
.lab-monaco[hidden] { display:none; }

/* The Game view: the game as it starts, letterboxed at its own 320x240 on a dark ground. */
.lab-gameview {
  display:flex; flex-direction:column; flex:1; min-height:200px; width:100%; overflow:hidden;
  background:rgba(5,3,15,0.8);
}
.lab-gameview[hidden] { display:none; }
/* `container-type: size` is what lets the frame below size itself against THIS box's height as
   well as its width. Without it there is no way in pure CSS to say "the biggest 4:3 rectangle that
   fits", and the frame ends up whatever shape the pane is. */
/* The game is 4:3 and this pane is not, so there is always space left over. It is CENTRED in it
   rather than pushed to the top: a picture with room around it reads as framed and deliberate,
   where the same picture pinned to the top edge reads as a page that has not finished loading.
   The room is the point — this is a television in a dark sitting room, which is exactly what a
   mebobox is, so the space is dressed as the wall behind the telly rather than left as a gap. */
.lab-gameview-stage {
  display:flex; align-items:center; justify-content:center;
  flex:1; min-height:0; padding:16px; container-type:size;
  /* The glow sits BEHIND the game and is what turns leftover height into atmosphere. Two stops
     only, and both nearly transparent: any more and it competes with the game, which is the one
     thing on this half of the screen anybody should be looking at. */
  background:
    radial-gradient(120% 78% at 50% 42%, rgba(43,140,255,0.10), transparent 68%),
    radial-gradient(90% 60% at 50% 100%, rgba(7,104,255,0.07), transparent 70%);
}
/* THE FRAME MUST BE 4:3 ITSELF, not merely contain something that is.
   It was `width:100%; height:100%` with an aspect-ratio, and a definite width and height beat
   aspect-ratio outright — so the iframe took the pane's own shape (838x779, near enough square) and
   the GAME letterboxed itself inside it, painting its own black bars top and bottom out of its page
   background. That is where the wasted space came from: not from this layout, but from a stretched
   iframe that the layout could not see into.
   Sized from whichever runs out first — the stage's width, or its height converted to a width at
   4:3 — so the game is always the largest whole 4:3 picture that fits, with no bars anywhere. */
/* The screen: the sizing, the bezel and the glow all live on this wrapper rather than the iframe,
   because the click-to-play button has to sit exactly over the picture and the two must be one
   box. A thin bezel and a screen-glow, so it reads as a little television rather than a rectangle
   of web page: the hairline is the set, the tint under it is the light coming off the picture, and
   the dark drop is what lifts it off the wall.
   THE FRAME MUST BE 4:3 ITSELF, not merely contain something that is — see the iframe rule below
   for what happens when a definite width and height beat the aspect-ratio. Sized from whichever
   runs out first, the stage's width or its height converted to a width at 4:3. */
.lab-gameview-screen {
  position:relative;
  width:min(100%, calc(100cqh * 4 / 3));
  aspect-ratio:320 / 240;
  border-radius:12px;
  box-shadow:
    0 0 0 1px rgba(43,140,255,0.28),
    0 0 34px -6px rgba(43,140,255,0.30),
    0 18px 40px -12px rgba(0,0,0,0.75);
}
.lab-gameview-screen[hidden] { display:none; }
#lab-game-frame {
  position:absolute; inset:0; display:block; width:100%; height:100%;
  border:0; border-radius:12px; background:#000;
}

/* Click the game, get the game. Invisible at rest — the resting pane is the game and nothing else,
   which is the whole point of it — and it says what it does the moment a pointer is over it or a
   keyboard reaches it. On a touch screen there is no hover to show it with, and none is needed: the
   tap simply works, which is what a child was going to try anyway. */
.lab-gameview-hit {
  position:absolute; inset:0; z-index:1;
  display:flex; align-items:center; justify-content:center;
  padding:0; border:0; border-radius:12px; background:transparent;
  cursor:pointer; opacity:0; transition:opacity 0.16s ease;
}
.lab-gameview-hit:hover, .lab-gameview-hit:focus-visible {
  opacity:1; background:rgba(3,2,10,0.42);
}
.lab-gameview-hit:focus-visible { outline:2px solid var(--blue); outline-offset:3px; }
/* Everything visual comes from the shared .lab-run rule above. Two things differ, both because of
   where this one sits. The height is matched to `.lab-toolbar .lab-run`'s own 38px — that rule is
   scoped to the toolbar, so the shared declaration alone left this one 8px taller than the button
   it is supposed to BE. And it sits on a dimmed overlay over the running game, so it carries an
   extra dark shadow to lift it off whatever is behind it, which the bar button never needs. */
.lab-gameview-hit-pill {
  height:38px; padding-top:0; padding-bottom:0;
  box-shadow:0 10px 24px rgba(3,2,10,0.55), 0 6px 18px rgba(31,185,104,0.35),
             inset 0 1px 0 rgba(255,255,255,0.28);
}
@media (prefers-reduced-motion: reduce) { .lab-gameview-hit { transition:none; } }
/* Nothing to show yet, or the build had something to say. Never an error page — the pane goes
   quiet and Run stays the place findings are worked through. */
#lab-game-frame[hidden] { display:none; }
.lab-gameview-stage.is-empty { padding:20px; }
.lab-gameview-empty {
  margin:0; max-width:34ch; text-align:center; font-size:13px; line-height:1.5; color:var(--muted);
}
.lab-gameview-note {
  flex:none; margin:0; padding:8px 14px; font-size:12px; line-height:1.4; color:var(--muted);
  border-top:1px solid rgba(43,140,255,0.12);
}
.lab-gameview-note[hidden] { display:none; }
.lab-monaco .monaco-editor .cursors-layer > .cursor { display:none; }   /* read-only: no caret */
/* Monaco's own hidden capture textarea is 13px, which makes it an iOS zoom trap like any other
   field — and it is present for EVERY child, not just in code mode, because the read-only game.js
   pane beside the blocks is a Monaco too. Tapping it zooms the page and iOS never zooms back.
   Only under a coarse pointer, so a desktop Monaco is untouched, and only the capture element: the
   text you can see is drawn by Monaco into its own spans and takes its size from the editor's
   options, which this does not go near.
   `!important` because Monaco writes the size as an INLINE style on that element from its own
   editor options, and an inline style beats any rule without it. Overriding a vendored library's
   inline style is the one thing !important is actually for. */
@media (pointer: coarse) {
  .monaco-editor textarea.inputarea { font-size:16px !important; }
}
.lab-graduation {
  display:flex; align-items:center; gap:12px; flex:none; flex-wrap:wrap;
  padding:12px 14px; border-top:1px solid rgba(43,140,255,0.14); background:rgba(7,104,255,0.05);
}
.lab-grad-icon { flex:none; font-size:19px; }
.lab-graduation p { flex:1; min-width:180px; margin:0; font-size:12.5px; line-height:1.45; color:var(--muted); }
.lab-graduation p strong { color:var(--fg); font-weight:600; }
.lab-graduation .ghostbtn:disabled { opacity:.5; cursor:not-allowed; }
/* There is no locked state here any more. Turning on Edit was gated on a middle level that no
   longer exists; the button is live for everybody, so the dimmed variant and its "switch levels up
   in the top bar" note went with it. */

/* ============================ validation bar ============================ */
.lab-valbar {
  display:flex; align-items:center; gap:12px; flex:none; flex-wrap:wrap;
  margin-top:12px; padding:12px 16px; border-radius:16px;
  border:1px solid rgba(43,140,255,0.18); background:var(--grad-panel); font-size:13.5px;
}
.lab-valbar[hidden] { display:none; }
.lab-val-resting, .lab-val-ok { display:flex; align-items:center; gap:10px; }
.lab-val-resting[hidden], .lab-val-ok[hidden], .lab-val-problems[hidden] { display:none; }
.lab-val-resting strong { color:var(--fg); font-weight:700; }
.lab-val-dot {
  flex:none; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center;
  font-size:11px; background:rgba(43,140,255,0.12); border:1px solid rgba(43,140,255,0.35); color:var(--blue);
}
.lab-val-dot.is-ok { background:rgba(70,224,138,0.12); border-color:rgba(70,224,138,0.45); color:var(--green); }
.lab-val-oktitle { font-weight:700; color:var(--green); }
.lab-val-counts { flex:none; margin-left:auto; font-size:12.5px; }

.lab-val-dot.is-bad { background:rgba(255,93,108,0.12); border-color:rgba(255,93,108,0.45); color:var(--red,#ff5d6c); }

/* The headline over the problem list, and the control that folds the list away. Kept visible when
   collapsed, so the panel always still says how many things are wrong. */
.lab-val-summary { display:flex; align-items:center; gap:10px; width:100%; font-weight:700; }
.lab-val-summary[hidden] { display:none; }
.lab-val-summary.is-warn .lab-val-dot {
  background:rgba(251,191,36,0.12); border-color:rgba(251,191,36,0.45); color:#fbbf24;
}
.lab-val-summary #lab-val-toggle { flex:none; font-weight:600; }

.lab-val-problems { display:flex; flex-direction:column; gap:10px; width:100%; }
/* Collapsed gives the canvas back — a long list pushes the editor up, and the editor is the only
   place the problems can actually be fixed. */
.lab-val-problems.is-collapsed { display:none; }
.val-problem {
  border-radius:13px; padding:12px 14px; border:1px solid rgba(255,93,108,0.35);
  background:rgba(255,93,108,0.08);
}
.val-problem.is-warning { border-color:rgba(251,191,36,0.4); background:rgba(251,191,36,0.08); }
.val-problem-title {
  display:flex; align-items:center; gap:8px; margin:0 0 5px;
  font-family:var(--f-display); font-weight:700; font-size:14.5px; color:var(--fg);
}
.val-problem-body { margin:0; font-size:13px; line-height:1.5; color:#e6b9bf; }
.val-problem.is-warning .val-problem-body { color:#e8d4a6; }
.val-problem.is-warning .val-problem-title { color:#fbbf24; }

/* ---- block <-> code linking ----
   Monaco applies these to the line content and to the gutter margin (see monaco.js highlight()).
   BLUE = "this is the code your selected block made". GREEN FLASH = "you just changed this block,
   and this is the code that moved" — a different question, so a different colour, and it fades back
   to blue rather than latching on. Without these the linking exists but is invisible. */
.lab-code-linked { background:rgba(43,140,255,0.16); }
.lab-code-linkedgutter { width:3px !important; margin-left:4px; background:var(--blue); }
.lab-code-changed { background:rgba(70,224,138,0.22); animation:studioCodeFlash 1.2s ease-out; }
@keyframes studioCodeFlash {
  from { background:rgba(70,224,138,0.55); }
  to { background:rgba(70,224,138,0.22); }
}
@media (prefers-reduced-motion: reduce) {
  .lab-code-changed { animation:none; }
}

/* The one-click repair on a problem card. Deliberately the most prominent thing on the card — for
   this audience "just fix it" beats "here is where the problem is". */
.val-problem-fix {
  padding:7px 13px; border-radius:9px; border:none; cursor:pointer;
  font-family:var(--f-ui); font-size:12.5px; font-weight:800;
  background:rgba(248,113,113,0.9); color:#3d0a0a;
}
.val-problem-fix:hover { filter:brightness(1.06); }
.val-problem.is-warning .val-problem-fix { background:rgba(251,191,36,0.9); color:#3d2a04; }

/* Warnings-only: the game launched, so the bar reads as information rather than a blocked run. */
.lab-val-problems.is-warnings-only { opacity:.95; }
.val-problem-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
.val-problem-actions .ghostbtn { padding:7px 12px; font-size:12.5px; }
.val-detail {
  margin-top:10px; padding:9px 11px; border-radius:10px; background:rgba(5,3,15,0.6);
  font-family:var(--f-mono); font-size:11.5px; line-height:1.5; color:var(--muted-2);
  white-space:pre-wrap; overflow-x:auto;
}
.val-detail[hidden] { display:none; }

/* ============================ run overlay ============================
   Scoped to #tab-lab (absolute, not fixed) so the site nav stays visible above it. */
.lab-overlay {
  /* fixed, not absolute: an absolute overlay centres inside the studio panel, and a panel taller
     than the window pushed the dialog's bottom off-screen. */
  position:fixed; inset:0; z-index:70; display:flex; align-items:center; justify-content:center;
  padding:20px; background:rgba(3,2,10,0.78); backdrop-filter:blur(6px);
  animation:wbpop .18s ease-out;
}
.lab-overlay[hidden] { display:none; }
.lab-rundialog {
  display:flex; flex-direction:column; width:min(680px,90%);
  max-height:calc(100dvh - 40px); min-height:0;
  border-radius:20px; overflow:hidden; border:1px solid rgba(43,140,255,0.32);
  background:linear-gradient(180deg,#0e1330,#0a0c20);
  box-shadow:0 30px 90px rgba(0,0,0,0.65),0 0 60px rgba(7,104,255,0.18);
}
/* The dialog holds the keyboard while a game runs (studio.js focusGame), so it is focused the whole
   time you are playing — a ring around the entire window would read as an error, not as a game. It
   has to out-specify the blanket #tab-lab :focus-visible ring further down, which is meant for
   the things you actually tab to. */
#tab-lab .lab-rundialog:focus,
#tab-lab .lab-rundialog:focus-visible { outline:none; }
.lab-run-head {
  display:flex; align-items:center; gap:10px; flex:none; padding:12px 14px;
  border-bottom:1px solid rgba(43,140,255,0.16); font-family:var(--f-display); font-weight:600; font-size:15px;
}

/* "It can't run yet" — same slot and shape as the run dialog, because it stands in for it. Amber,
   not red: the game is unfinished, which is the ordinary state of a game being built. */
.lab-rundialog.is-cantrun { border-color:rgba(251,191,36,0.4); }
.lab-rundialog.is-cantrun .lab-run-head { border-bottom-color:rgba(251,191,36,0.22); }
.lab-cantrun-body {
  flex:1 1 auto; min-height:0; overflow-y:auto; padding:14px; display:flex; flex-direction:column; gap:10px;
}
.cantrun-problem {
  border-radius:13px; padding:12px 14px;
  border:1px solid rgba(251,191,36,0.35); background:rgba(251,191,36,0.08);
}
.cantrun-problem-title { font-weight:700; margin-bottom:5px; }
.cantrun-problem-body { color:var(--muted); line-height:1.5; }
.cantrun-problem-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:10px; }

/* Warnings ride with the game they did not stop, inside the run overlay. */
.lab-run-warnings { flex:none; padding:10px 14px 0; display:flex; flex-direction:column; gap:8px; }
.lab-run-warnings[hidden] { display:none; }
.run-warning {
  display:flex; gap:9px; align-items:flex-start; font-size:12.5px; line-height:1.5;
  border-radius:11px; padding:9px 11px;
  border:1px solid rgba(251,191,36,0.32); background:rgba(251,191,36,0.08); color:var(--muted);
}
.run-warning strong { color:var(--fg); }
.run-warning-mark { flex:none; color:#fbbf24; }
.lab-run-name { color:var(--blue); }
/* Matches the game's logical canvas (src/lab-build.js CANVAS_WIDTH/HEIGHT), so the preview shows
   exactly the shape the child is building against — no letterboxing inside the letterbox. */
/* The game keeps its 4:3 shape, but on a short window it is what shrinks — the title bar, the game
   code and the Restart/Stop/Back controls must stay reachable. */
.lab-run-viewport {
  position:relative; flex:1 1 auto; min-height:0; width:100%; aspect-ratio:4/3; background:#000;
}
.lab-run-viewport iframe { position:absolute; inset:0; width:100%; height:100%; border:0; display:block; background:#000; }
.lab-run-loading {
  position:absolute; inset:0; margin:0; display:flex; align-items:center; justify-content:center;
  color:var(--muted); font-size:14px; pointer-events:none;
}
.lab-run-loading[hidden] { display:none; }
.lab-run-tip {
  flex:none; margin:0; padding:11px 16px; font-size:12.5px; line-height:1.45; color:var(--muted);
  background:rgba(7,104,255,0.06); border-bottom:1px solid rgba(43,140,255,0.12);
}
.lab-run-code {
  font-family:var(--f-mono); font-weight:600; letter-spacing:3px; color:var(--green);
  text-shadow:0 0 10px rgba(70,224,138,0.5);
}
.lab-run-controls { display:flex; align-items:center; gap:10px; flex:none; flex-wrap:wrap; padding:12px 14px; }
.lab-run-hint { font-size:12.5px; }
/* Real browser fullscreen (lab.js's fullscreenchange listener, not the click alone — that is what
   catches Escape and every other way out besides the button). width/height:100% would only fill
   the FULLSCREEN ELEMENT's own box, which the Fullscreen API already does; vw/vh is what actually
   reaches edge to edge on the monitor.
   The viewport keeps its aspect-ratio:4/3 here on purpose — "no letterboxing inside the letterbox"
   above is exactly the property a true fullscreen must keep too, or every game stretches to
   whatever the monitor's own shape is the moment this runs. flex:0 1 auto (not 1 1 auto) lets it
   settle at its own aspect-ratio-derived size instead of being forced to fill the remaining flex
   space; max-width/max-height then pick whichever of the screen's width or height is the tighter
   fit, and margin:auto centres what is left over as real letterbox bars. */
.lab-rundialog.is-full { width:100vw; height:100dvh; max-height:100dvh; border-radius:0; }
/* flex:none — the 4:3 shape here comes from real pixel width/height set in JS (fitFullscreenViewport
   in lab.js), not from aspect-ratio. Both were tried: flex-basis:auto has no in-flow content to size
   from (the iframe is position:absolute, so the box collapses to 0), and giving it an explicit
   height instead just lets max-width clamp the derived width without the height following back down
   to match — the box stops being 4:3 the moment the available space is narrower than it is tall.
   Letterbox math needs to know the ACTUAL pixels .lab-run-head/.lab-run-controls take, which is
   exactly what mebobox-runtime.js's own fit() already does for the game's UI layer inside the
   canvas — same reasoning, one level up. */
.lab-rundialog.is-full .lab-run-viewport { flex:none; margin:0 auto; }

/* ============================ version history drawer ============================ */
.lab-drawer-wrap { position:fixed; inset:0; z-index:60; display:flex; justify-content:flex-end; }
.lab-drawer-wrap[hidden] { display:none; }
.lab-drawer-scrim { position:absolute; inset:0; background:rgba(3,2,10,0.6); backdrop-filter:blur(3px); }
@keyframes lab-drawer-in { from { transform:translateX(100%); } to { transform:translateX(0); } }
.lab-drawer {
  position:relative; display:flex; flex-direction:column; width:390px; max-width:100%; height:100%;
  padding:18px; overflow-y:auto; border-left:1px solid rgba(43,140,255,0.3);
  background:linear-gradient(180deg,#0e1330,#0a0c20);
  box-shadow:-24px 0 70px rgba(0,0,0,0.6); animation:lab-drawer-in .22s cubic-bezier(.2,.8,.2,1);
}
.lab-drawer-head { display:flex; align-items:center; gap:10px; margin-bottom:8px; }
.lab-drawer-head h3 { font-family:var(--f-display); font-weight:700; font-size:17px; margin:0; }
.lab-drawer-desc { font-size:12.5px; color:var(--muted); line-height:1.5; margin:0 0 16px; }
.lab-drawer-list { display:flex; flex-direction:column; gap:10px; }

.version-item {
  border-radius:13px; border:1px solid rgba(43,140,255,0.2); background:rgba(7,104,255,0.05); padding:13px 14px;
}
.version-item.is-current { border-color:rgba(70,224,138,0.45); background:rgba(70,224,138,0.06); }
.version-head { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
/* The glyph is what says who made a row on a list with no badges — the same icon the toolbar
   itself uses (floppy on Save, play on Run, mebo's head on Saved for you). */
.version-glyph { flex:none; font-size:15px; line-height:1; }
.version-name { font-family:var(--f-display); font-weight:600; font-size:14.5px; color:var(--fg); }
.version-badge {
  flex:none; font-size:10px; font-weight:700; letter-spacing:.4px; text-transform:uppercase;
  padding:2px 8px; border-radius:999px;
  color:var(--muted); background:rgba(138,147,173,0.12); border:1px solid rgba(138,147,173,0.35);
}
.version-badge.is-current { color:var(--green); background:rgba(70,224,138,0.12); border-color:rgba(70,224,138,0.45); }
.version-meta { font-size:12px; color:var(--muted); margin:5px 0 0; }
.version-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:11px; }
.version-actions .ghostbtn { padding:7px 12px; font-size:12.5px; }
.version-ren { width:30px; height:30px; font-size:13px; }
/* Renaming in place — swaps in for .version-name, so it inherits the same type. */
.version-rename-input {
  font-family:var(--f-display); font-weight:600; font-size:16px; color:var(--fg);
  background:rgba(5,3,15,0.5); border:1px solid rgba(43,140,255,0.5); border-radius:7px;
  padding:2px 7px; width:100%; max-width:220px;
}

/* ============================ preview (read-only) bar ============================ */
.lab-previewbar {
  display:flex; align-items:center; gap:12px; flex:none; flex-wrap:wrap;
  margin-top:12px; padding:12px 16px; border-radius:16px; font-size:13.5px; color:#e8d4a6;
  background:rgba(251,191,36,0.08); border:1px solid rgba(251,191,36,0.4);
}
.lab-previewbar[hidden] { display:none; }
.lab-previewbar strong { color:var(--fg); font-weight:700; }

/* ============================ modals (rollback / name a version) ============================ */
.lab-modal-wrap {
  position:fixed; inset:0; z-index:80; display:flex; align-items:center; justify-content:center; padding:20px;
}
.lab-modal-wrap[hidden] { display:none; }
.lab-modal-scrim { position:absolute; inset:0; background:rgba(3,2,10,0.72); backdrop-filter:blur(4px); }
.lab-rollback {
  position:relative; width:460px; max-width:100%; padding:26px; border-radius:20px; text-align:center;
  border:1px solid rgba(43,140,255,0.32); background:linear-gradient(180deg,#0e1330,#0a0c20);
  box-shadow:0 30px 90px rgba(0,0,0,0.65),0 0 60px rgba(7,104,255,0.15); animation:wbpop .18s ease-out;
}
/* This dialog now offers three real choices, so it needs the room to lay them out on one line —
   and each label stays whole rather than folding in half. If they ever do not fit (a longer word,
   a narrow window) the row wraps to two lines instead of squashing three buttons into slivers. */
#lab-asset-modal .lab-rollback { width:580px; }
#lab-asset-modal .lab-rollback-actions { flex-wrap:wrap; justify-content:center; }
#lab-asset-modal .lab-rollback-actions button { white-space:nowrap; }

/* Close in the corner, so a dialog's button row is only things you might want to DO. */
.lab-modal-x {
  position:absolute; top:12px; right:12px; z-index:1;
  min-width:30px; min-height:30px; font-size:13px;
}
.lab-rollback-icon {
  width:56px; height:56px; border-radius:50%; margin:0 auto 14px; display:flex; align-items:center; justify-content:center;
  font-size:25px; background:rgba(43,140,255,0.12); border:1px solid rgba(43,140,255,0.4);
  box-shadow:0 0 26px rgba(7,104,255,0.35);
}
.lab-rollback h3 { font-family:var(--f-display); font-weight:700; font-size:20px; margin:0 0 10px; }
.lab-rollback p { font-size:13.5px; color:var(--muted); line-height:1.55; margin:0 0 12px; }
.lab-rollback p strong { color:var(--fg); font-weight:600; }
.lab-rollback-note {
  padding:10px 12px; border-radius:11px; font-size:12.5px;
  background:rgba(7,104,255,0.07); border:1px solid rgba(43,140,255,0.2);
}
.lab-rollback input { text-align:left; margin-bottom:6px; }
.lab-rollback-actions { display:flex; justify-content:center; gap:10px; margin-top:18px; }

/* ============================ mebo: the help panel ============================
   A DRAWER, not a dialog, and the difference is the whole design. mebo's answer is "look at that
   block and drag it under start" — so the block has to be visible and draggable while the answer is
   on screen. A centred card put the answer on top of the thing it was pointing at.
   It docks right, over the CODE pane, so the blocks pane on the left stays clear. No scrim, nothing
   modal, and pointer-events are off on the wrapper so every click that is not on the panel itself
   goes straight through to the workspace underneath. Reuses .lab-drawer's shape and animation. */
.lab-askmebo { display:inline-flex; align-items:center; gap:7px; }
.lab-askmebo img { display:block; }

/* "Submit my game" — the only gold thing in the app, because it is the only button in this row that
   is not about making the game. Everything else here changes it; this one enters it. */
.lab-enter { color:#ffc74a; border-color:rgba(255,199,74,0.4); background:rgba(255,199,74,0.08); }
.lab-enter:hover { color:#ffe49a; border-color:rgba(255,199,74,0.75); background:rgba(255,199,74,0.16); }

.lab-help-wrap { pointer-events:none; }
.lab-help-wrap[hidden] { display:none; }
/* "Show me" on a control the drawer is covering: the panel goes see-through for the pulse and
   comes back. Only opacity — nothing moves, nothing closes, the transcript is exactly where it was. */
.lab-help-wrap.is-peek .lab-help { opacity:0.12; pointer-events:none; transition:opacity 0.2s; }
.lab-help-wrap .lab-help { transition:opacity 0.2s; }
.lab-help {
  pointer-events:auto;                 /* the panel itself is live; the space beside it is not */
  width:400px; text-align:left;
}
.lab-help-head { display:flex; align-items:flex-start; gap:12px; margin-bottom:16px; position:relative; }

/* The ⋯ menu, holding what used to be two permanent grey paragraphs under the composer: what mebo
   does, the help page, and the tips switch. Anchored to the header so it opens over the transcript
   rather than pushing anything down. */
.lab-help-menu {
  position:absolute; top:100%; right:0; z-index:20; width:260px; padding:14px 15px;
  border-radius:12px; border:1px solid var(--line); background:var(--panel, #131a2b);
  box-shadow:0 14px 36px rgba(0,0,0,0.5);
}
.lab-help-menu[hidden] { display:none; }
.lab-help-menu-note { margin:0 0 10px; font-size:12.5px; line-height:1.55; color:var(--muted); }
.lab-help-menu a { font-size:12.5px; }
.lab-help-menu .lab-tips-toggle { margin:12px 0 0; padding-top:12px; border-top:1px solid var(--line); font-size:12.5px; color:var(--muted); }
.lab-help-menu .lab-tips-toggle[hidden] { display:none; }
.lab-help-menu .lab-tips-toggle .linkbtn { display:block; margin-top:4px; font-size:12.5px; }
.lab-help-head > div { flex:1; min-width:0; }
.lab-help-head h3 { font-family:var(--f-display); font-weight:700; font-size:19px; margin:0; color:var(--fg); }
.lab-help-mebo { flex:none; margin-top:-2px; }   /* bigger, so nudge it back into line */
.lab-help-sub { font-size:13px; color:var(--muted); margin:3px 0 0; }
.lab-help-sub:empty { display:none; }
/* Any class that sets `display` beats the UA's [hidden] rule — see the note in account.css. */
.lab-help-step[hidden], .lab-help-cards[hidden], .lab-help-actions[hidden] { display:none; }
.lab-help-lead { font-size:14px; font-weight:600; color:var(--fg); margin:0 0 12px; display:block; }

/* the free checks, shown before mebo is ever offered */
.lab-help-finding {
  display:block; padding:11px 13px; border-radius:12px; margin-bottom:9px;
  border:1px solid rgba(43,140,255,0.2); background:rgba(7,104,255,0.07);
}
.lab-help-finding.is-error { border-color:rgba(255,93,108,0.4); background:rgba(255,93,108,0.08); }
.lab-help-finding strong { display:block; font-size:13.5px; color:var(--fg); margin-bottom:3px; }
.lab-help-finding span { display:block; font-size:12.5px; color:var(--muted); line-height:1.5; }

/* The three ways in. Stacked, not three-across: the panel is 400px, and a row of three would give
   each card 120px and turn every label into three lines. */
.lab-help-cards { display:grid; grid-template-columns:1fr; gap:9px; }
.lab-help-card {
  display:grid; grid-template-columns:auto 1fr; grid-template-rows:auto auto;
  column-gap:11px; row-gap:2px; text-align:left; cursor:pointer;
  padding:12px 13px; border-radius:13px; color:var(--fg);
  border:1px solid rgba(43,140,255,0.25); background:rgba(7,104,255,0.06);
  transition:border-color .12s, background .12s;
}
.lab-help-card:hover { border-color:rgba(43,140,255,0.55); background:rgba(43,140,255,0.12); }
.lab-help-card-icon { grid-row:1 / span 2; align-self:center; font-size:20px; }
.lab-help-card-title { font-size:13.5px; font-weight:600; }
.lab-help-card-desc { font-size:12px; color:var(--muted); line-height:1.4; }

/* the questions answered for free */
.lab-help-common-list { display:flex; flex-direction:column; gap:7px; }
.lab-help-common {
  text-align:left; cursor:pointer; padding:11px 13px; border-radius:11px; font-size:13.5px; color:var(--fg);
  border:1px solid rgba(43,140,255,0.22); background:rgba(7,104,255,0.05);
}
.lab-help-common:hover { border-color:rgba(43,140,255,0.5); background:rgba(43,140,255,0.11); }

.lab-help textarea {
  width:100%; padding:11px 14px; border-radius:11px; font:inherit; font-size:16px; resize:vertical;
  color:var(--fg); background:rgba(5,3,15,0.7); border:1px solid rgba(43,140,255,0.25);
}
.lab-help textarea:focus {
  outline:none; border-color:rgba(43,140,255,0.7); box-shadow:0 0 0 3px rgba(43,140,255,0.15);
}
.lab-help-count { font-size:11.5px; color:var(--muted); margin:6px 0 0; }

/* thinking */
.lab-help-thinking { display:flex; align-items:center; gap:10px; font-size:13.5px; color:var(--muted); margin:22px 0; }
.lab-help-dots { display:inline-flex; gap:4px; }
.lab-help-dots i { width:6px; height:6px; border-radius:50%; background:var(--blue); animation:mb-help-dot 1.1s infinite ease-in-out; }
.lab-help-dots i:nth-child(2) { animation-delay:.16s; }
.lab-help-dots i:nth-child(3) { animation-delay:.32s; }
@keyframes mb-help-dot { 0%,80%,100% { opacity:.25; transform:translateY(0); } 40% { opacity:1; transform:translateY(-3px); } }
@media (prefers-reduced-motion:reduce) { .lab-help-dots i { animation:none; opacity:.6; } }

/* the answer */
.lab-help-bubble {
  padding:14px 16px; border-radius:14px;
  border:1px solid rgba(43,140,255,0.28); background:rgba(7,104,255,0.08);
}
.lab-help-bubble p { font-size:14px; color:var(--fg); line-height:1.6; margin:0; }
.lab-help-bubble ol { margin:11px 0 0; padding-left:20px; }
.lab-help-bubble ol[hidden] { display:none; }
.lab-help-bubble li { font-size:13px; color:var(--muted-2); line-height:1.6; margin-bottom:4px; }
.lab-help-bubble b { color:var(--fg); font-weight:600; }
.lab-help-note {
  font-size:12.5px; color:var(--muted); margin:11px 0 0; padding-left:20px; position:relative;
}
.lab-help-note::before { content:'↖'; position:absolute; left:2px; color:var(--blue); }
.lab-help-note[hidden], .lab-help-more[hidden] { display:none; }
/* "Ask mebo properly" — a real control now, where the sentence used to just name the Ask button and
   rely on the child's words still sitting in the composer. Styled as the link it reads as rather
   than a second primary button: the free answer above it is usually right, and this is the way out
   when it is not, so it must be findable without competing with the answer. */
.lab-help-again {
  background:none; border:0; padding:0; font:inherit; color:var(--blue);
  text-decoration:underline; cursor:pointer;
}
.lab-help-again:hover { color:var(--fg); }
.lab-help-again:disabled { color:var(--muted); text-decoration:none; cursor:default; }
.lab-help-more { font-size:12.5px; margin:9px 0 0; }
.lab-help-more a { color:var(--blue); }

/* The snippet card (code mode). Deliberately built like a little file rather than like more of
   mebo's speech: a header naming the file, then the code itself on the editor's own background, in
   the editor's own font. What is being shown IS a piece of a file, and it should look like one.
   Long lines scroll inside the card — the drawer is 380px and must never push the page sideways. */
.lab-help-code {
  margin-top:12px; border-radius:12px; overflow:hidden;
  border:1px solid rgba(43,140,255,0.24); background:#080a1c;
}
.lab-help-code[hidden] { display:none; }
.lab-help-code-head {
  display:flex; align-items:center; gap:8px; padding:9px 11px;
  border-bottom:1px solid rgba(43,140,255,0.18); background:rgba(7,104,255,0.07);
}
.lab-help-code-verb { font-size:11.5px; color:var(--muted); white-space:nowrap; }
.lab-help-code-file {
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size:12px; color:var(--blue); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.lab-help-code-file[hidden] { display:none; }
.lab-help-code-head .ghostbtn { min-height:26px; padding:0 10px; font-size:11.5px; }
.lab-help-code-where {
  margin:0; padding:9px 12px 0; font-size:12.5px; color:var(--muted-2); line-height:1.5;
}
.lab-help-code-where[hidden] { display:none; }
.lab-help-code-body {
  margin:0; padding:11px 12px 12px; overflow-x:auto;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size:12.5px; line-height:1.65; white-space:pre; tab-size:2;
}
/* The COLOUR has to land on the <code>, not the <pre> around it. The site styles `code` blue for
   inline mentions of a filename or a block name, which is right there and wrong here: a whole
   snippet in #2b8cff on the editor's near-black reads as a smudge. This is the one place `code`
   means "a listing", so it takes the editor's own foreground. */
.lab-help-code-body code { color:#e8ecf6; font:inherit; }
.lab-help-code-body[hidden] { display:none; }

/* Which lines are the change, and which are only there so you can see where it lands. The bar down
   the side does the work — the tint alone is too quiet against this background, and colour alone
   would be the only signal for somebody who cannot see it.
   The rows bleed into the <pre>'s padding so the bar sits at the card's edge, and `min-width:100%`
   with `width:max-content` keeps the tint under the whole of a line that scrolls. */
.lab-help-code-line { display:block; margin:0 -12px; padding:0 12px 0 10px; border-left:2px solid transparent; }
/* The <pre> is the scroll container, so the CODE has to be as wide as its widest line — otherwise a
   highlighted row that scrolls off ends its tint at the fold. */
.lab-help-code-body code { display:block; min-width:100%; width:max-content; }
.lab-help-code-line.is-context { opacity:0.45; }
.lab-help-code-line.is-change { background:rgba(43,140,255,0.14); border-left-color:var(--blue); }
.lab-help-code-show {
  display:block; width:calc(100% - 24px); margin:0 12px 12px; min-height:32px; font-size:12px;
}
.lab-help-code-show[hidden] { display:none; }

/* .primary pads 12/20 and .ghostbtn pads 9/15, so side by side one stands taller than the other.
   Inside the panel they are one row of equals — same height, same vertical padding. */
/* The "learn more" fold. Closed by default and quiet when closed: it is an offer, not a lesson a
   child has to get past to reach the answer. */
.lab-help-learn {
  margin-top:12px; padding:10px 13px; border-radius:12px;
  border:1px solid rgba(43,140,255,0.2); background:rgba(7,104,255,0.05);
}
.lab-help-learn[hidden] { display:none; }
.lab-help-learn summary {
  cursor:pointer; font-size:12.5px; font-weight:600; color:var(--blue); list-style:none;
}
.lab-help-learn summary::-webkit-details-marker { display:none; }
.lab-help-learn summary::before { content:'💡 '; }
.lab-help-learn summary::after { content:' ▾'; color:var(--muted); }
.lab-help-learn[open] summary::after { content:' ▴'; }
.lab-help-learn p { font-size:12.5px; color:var(--muted-2); line-height:1.6; margin:9px 0 0; }

.lab-help-actions { display:flex; align-items:stretch; gap:10px; margin-top:16px; }
.lab-help-actions .primary,
.lab-help-actions .ghostbtn {
  display:inline-flex; align-items:center; justify-content:center;
  min-height:38px; padding-top:0; padding-bottom:0; font-size:13px;
}
.lab-help-actions .iconbtn { align-self:center; }
.lab-help-rate { align-items:center; }
.lab-help-rate .lab-help-rate-q { font-size:12.5px; color:var(--muted); }
.lab-help-rate.is-hidden-rate .lab-help-rate-q,
.lab-help-rate.is-hidden-rate [data-help-rate] { display:none; }

.lab-help-foot {
  font-size:11.5px; color:var(--muted); line-height:1.5; margin:18px 0 0;
  padding-top:13px; border-top:1px solid rgba(43,140,255,0.14);
}
.lab-help-foot a { color:var(--blue); }
/* How many asks are left today. Shown before they spend one, not sprung on them at zero — a child
   who can see "3 left" saves them for what they are actually stuck on. */
.lab-help-left {
  display:inline-block; margin-right:6px; padding:2px 8px; border-radius:999px;
  font-size:11px; font-weight:600; color:var(--muted-2);
  background:rgba(43,140,255,0.12); border:1px solid rgba(43,140,255,0.25);
}
.lab-help-left[hidden] { display:none; }
.lab-help-left.is-low { color:var(--red); background:rgba(255,93,108,0.1); border-color:rgba(255,93,108,0.35); }
.lab-mebo-left-wrap { margin:0 0 14px; padding-top:0; border-top:0; }
.lab-mebo-left-wrap[hidden] { display:none; }

/* ------------------------------- mebo: the chat -------------------------------
   The drawer's contents are a chat now: the LOG is the flexing, scrolling middle; the chips and
   the composer sit under it; the head and the foot stay put. The panel itself stops scrolling —
   scrolling belongs to the conversation, the way it does in every chat a child has seen. */
.lab-chat { overflow-y:hidden; }
.lab-chat-log {
  flex:1; min-height:0; overflow-y:auto; display:flex; flex-direction:column; gap:10px;
  /* 24px top: the face pokes ~21.5px above its bubble (see .lab-chat-face), and less padding than
     that clips the very first message's face against the log's own scroll boundary. */
  padding:24px 2px 10px;
}
.lab-chat-msg { display:flex; }
.lab-chat-msg.is-child { justify-content:flex-end; }
/* mebo's bubble sits flush against the log's own left edge (2px of padding), which is far less
   than the face's own protrusion — so its LEFT side was being clipped by the log's scroll
   boundary, not by anything about the icon itself. A child's bubble never has this problem: it is
   pushed hard right, with plenty of room behind it. Shifting the row over, not the face, is the
   fix — the overlap amount stays exactly what the child's face already uses. */
.lab-chat-msg.is-mebo { margin-left:22px; }

/* The bubble plus its face, as one unit: relatively positioned so the face (help.js faceEl) can
   pin itself to the bubble's OWN top-left corner and overlap it slightly, rather than sitting
   beside it in the row — the max-width that used to live directly on .lab-chat-body moves here
   so the bubble still shrinks to its text and the face still tracks its corner. */
.lab-chat-bubblewrap { position:relative; max-width:88%; }

/* Both faces are solid, not translucent — a washed-out rgba circle read as a smudge rather than
   a marker. The ring is each bubble's OWN border colour (below), so a face reads as part of its
   message rather than a separate decoration. */
.lab-chat-face {
  position:absolute; top:-13px; left:-13px; z-index:1;
  width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  font-size:10px; font-weight:700; letter-spacing:.3px; color:#fff;
  background:var(--blue, #2b8cff); border:1.5px solid rgba(43,140,255,0.28);
  background-size:cover; background-position:center; user-select:none;
}
.lab-chat-msg.is-child .lab-chat-face { border-color:rgba(43,140,255,0.4); }
.lab-chat-face.is-mebo-face { background:rgb(9,12,26); }
.lab-chat-face-img {
  /* Sized well BELOW the 26px circle it sits in and centred by the circle's own flex centring —
     mebo-blue.png's art fills its canvas edge to edge, so relying on the circle to CLIP a
     full-size image read as cropped. A smaller image can never touch the ring, whatever margin
     the source art does or doesn't have. 18px keeps a ~1.8px gap to the ring at every corner —
     measured, not guessed: half the rendered box's diagonal is ~11.2px against a 13px radius. */
  display:block; width:18px; height:auto;
  /* The art's own visual weight (checked by canvas pixel-averaging: the alpha-weighted centroid
     sits ~8% of the height below the geometric middle — the thin antennae read as empty, the
     head as the real mass) sits BELOW the geometric centre of its frame, so centring the frame
     reads as the face sitting low. Nudging it up compensates without touching the source art. */
  transform:translateY(-1px);
}
.lab-chat-body {
  max-width:100%; padding:11px 14px; border-radius:14px;
  border:1px solid rgba(43,140,255,0.28); background:rgba(7,104,255,0.08);
  font-size:13.5px; color:var(--fg); line-height:1.6;
}
.lab-chat-msg.is-child .lab-chat-body {
  background:rgba(43,140,255,0.18); border-color:rgba(43,140,255,0.4); border-bottom-right-radius:5px;
}
.lab-chat-msg.is-mebo .lab-chat-body { border-bottom-left-radius:5px; }
.lab-chat-body p { margin:0; font-size:13.5px; line-height:1.6; }
.lab-chat-body p + * { margin-top:9px; }
.lab-chat-body ol { margin:9px 0 0; padding-left:20px; }
.lab-chat-body li { font-size:12.5px; color:var(--muted-2); line-height:1.6; margin-bottom:3px; }
.lab-chat-body b { color:var(--fg); font-weight:600; }
.lab-chat-body.is-err {
  border-color:rgba(255,93,108,0.4); background:rgba(255,93,108,0.08); color:var(--muted-2);
  font-size:12.5px;
}
.lab-chat-body .lab-help-finding { margin-top:9px; margin-bottom:0; }
.lab-chat-body .lab-help-finding + .lab-help-finding { margin-top:7px; }
.lab-chat-wait { display:flex; align-items:center; gap:9px; color:var(--muted); font-size:13px; }

/* The quiet line between sittings — "Yesterday", "Just now" (help.js stampWhen). A label, not a
   control: it only appears where a real pause in the chat crossed one of the when-labels. */
.lab-chat-when {
  display:flex; align-items:center; gap:10px; margin:3px 0 -2px;
  color:var(--muted); font-size:10.5px; letter-spacing:.4px; text-transform:uppercase;
  user-select:none;
}
.lab-chat-when::before, .lab-chat-when::after {
  content:''; flex:1; height:1px; background:rgba(43,140,255,0.18);
}

/* mebo asking back: tappable readings plus one line for "something else" — Claude-Code-shaped. */
.lab-chat-options { display:flex; flex-direction:column; gap:7px; margin-top:10px; }
.lab-chat-option {
  text-align:left; cursor:pointer; padding:9px 12px; border-radius:10px; font-size:13px; color:var(--fg);
  border:1px solid rgba(43,140,255,0.3); background:rgba(7,104,255,0.07);
}
.lab-chat-option:hover:not(:disabled) { border-color:rgba(43,140,255,0.6); background:rgba(43,140,255,0.13); }
.lab-chat-option:disabled { opacity:0.5; cursor:default; }
.lab-chat-other input {
  width:100%; padding:8px 12px; border-radius:10px; font:inherit; font-size:16px;
  color:var(--fg); background:rgba(5,3,15,0.7); border:1px solid rgba(43,140,255,0.22);
}
.lab-chat-other input:focus { outline:none; border-color:rgba(43,140,255,0.6); }
.lab-chat-other input:disabled { opacity:0.5; }

/* the per-answer thumbs */
.lab-chat-rate { display:flex; align-items:center; gap:7px; margin-top:10px; }
.lab-chat-rate .iconbtn { min-width:30px; min-height:30px; font-size:14px; }

/* "«draw sprite» — in the Sprites drawer [Show me]": the block mebo says they need, and the way
   to it. One row per block, at most two. */
.lab-chat-newblocks { display:flex; flex-direction:column; gap:6px; margin-top:10px; }
.lab-chat-newblock {
  display:flex; align-items:center; gap:9px; padding:8px 11px; border-radius:10px;
  border:1px solid rgba(43,140,255,0.24); background:rgba(7,104,255,0.06); font-size:12.5px;
}
.lab-chat-newblock span { flex:1; color:var(--muted-2); }
.lab-chat-newblock b { color:var(--fg); font-weight:600; }
.lab-chat-newblock .ghostbtn { min-height:28px; padding:0 11px; font-size:11.5px; white-space:nowrap; }

/* The dim-and-blur behind a showing tip. z-index sits BELOW both the callout and the drawer
   (60 each) so neither is itself blurred — only the editor behind them is — and it covers the
   whole viewport regardless of which of the two is actually showing, so the toolbar and canvas
   read as one dimmed backdrop either way. Same recipe as .lab-drawer-scrim, a shade darker: a tip
   competing with a canvas full of blocks needed more contrast than a drawer opening over a mostly
   empty scrim already had. */
.lab-mebo-overlay {
  position:fixed; inset:0; z-index:55; pointer-events:none;
  background:rgba(3,2,10,0.55); backdrop-filter:blur(5px); -webkit-backdrop-filter:blur(5px);
  animation:lab-mebo-overlay-in 0.22s ease-out;
}
.lab-mebo-overlay[hidden] { display:none; }
@keyframes lab-mebo-overlay-in { from { opacity:0; } to { opacity:1; } }
@media (prefers-reduced-motion: reduce) { .lab-mebo-overlay { animation:none; } }

/* THE ONE THING THE DIM DOES NOT DIM, WITHOUT THIS.
   Blockly gives its toolbox — the Game / Sprites / Drawing category list — `z-index: 70` of its
   own, and the toolbox and this overlay end up in the SAME stacking context (both are inside
   `main.workspace`, which sets z-index:1 and so contains them both). 70 beats the overlay's 55
   outright, so the category list punched a bright, sharp column through a backdrop that was
   blurring everything else: it read as though the menu, of all things, was what the tip was about.
   It beat the callout's 60 as well — invisible only because the callout is anchored top-right and
   the toolbox is far left, so they never happened to overlap.
   Demoted only WHILE a tip is showing, rather than by renumbering our own layers: the overlay has
   to stay below the callout and the drawer (60 each) so neither is itself blurred, so lifting it
   over 70 would have meant moving those too, and the drawer's z-index is load-bearing elsewhere
   (see the .workspace:has(...) note near the end of this file). 25 keeps the toolbox above its own
   flyout (20), which is the relationship Blockly's 70 was there to hold in the first place. */
#lab-editor:has(#lab-mebo-overlay:not([hidden])) .blocklyToolboxDiv { z-index:25; }

/* mebo's tip callout. Fixed, and placed from the Ask-mebo button's box by tips.js — see the note
   on the markup for why it cannot live inside the toolbar. */
/* Sized for a ten-year-old reading it, not for a toolbar badge. It was 12.5px type in 12px of
   padding with 11px buttons — legible to an adult who already knew what it said, and cramped and
   forbidding to the child it is actually for. Everything here is bigger and further apart. */
.lab-mebo-note {
  position:fixed; z-index:60; display:flex; gap:14px; padding:18px 20px;
  border-radius:16px; border:1px solid rgba(43,140,255,0.45);
  background:var(--panel, #131a2b); box-shadow:0 16px 44px rgba(0,0,0,0.5);
  animation:mb-note-in 0.22s ease-out;
}
/* An author `display` beats the browser's own [hidden] rule, so without this the empty callout
   sits over the toolbar from the moment the page loads. The same trap account.css:127 already
   guards for .linkbtn — any element given a display here needs it. */
.lab-mebo-note[hidden] { display:none; }
@keyframes mb-note-in { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:none; } }
@media (prefers-reduced-motion: reduce) { .lab-mebo-note { animation:none; } }
.lab-mebo-note-face { flex:none; margin-top:1px; }
.lab-mebo-note-face svg, .lab-mebo-note-face img { width:38px; height:38px; }
.lab-mebo-note-main { flex:1; min-width:0; }
/* A tip is a sentence or two a child reads once. 15px with generous leading is the difference
   between reading it and skipping it. */
/* pre-line so a tip written in paragraphs reads as paragraphs. The welcome is three short ones —
   what mebo is, what to say to him, and what else he knows — which is the difference between an
   introduction and a wall. */
.lab-mebo-note-text { margin:0 0 14px; font-size:15px; line-height:1.65; color:var(--fg); white-space:pre-line; }
.lab-mebo-note-title {
  margin:0 0 6px; font-size:16px; font-weight:800; letter-spacing:-0.01em; color:var(--fg);
}
.lab-mebo-note-actions { display:flex; flex-wrap:wrap; gap:9px; align-items:center; }
/* Tap targets a child can actually hit, and text they can read on the button. */
.lab-mebo-note-actions .sm { min-height:38px; padding:0 16px; font-size:13.5px; border-radius:10px; }
/* Quiet, but a real button with a real hit area — the off switch is not decoration. */
.lab-mebo-note-off {
  margin-top:12px; padding:4px 0; background:none; border:0; cursor:pointer;
  font-size:12.5px; color:var(--muted-2); text-decoration:underline;
}
.lab-mebo-note-off:hover { color:var(--fg); }

.lab-chat-tiptext { white-space:pre-line; }

/* the free chips, above the composer. They vanish one by one as they are used. */
.lab-chat-chips { display:flex; flex-wrap:wrap; gap:6px; padding:8px 0 6px; }
.lab-chat-chips:empty { display:none; }
/* `display:flex` above beats the UA's [hidden] rule, so hiding the row from JS once a conversation
   starts would silently do nothing — the same trap .inst-classes hit in account.css. */
.lab-chat-chips[hidden] { display:none; }
.lab-chat-chip {
  cursor:pointer; padding:5px 11px; border-radius:999px; font-size:11.5px; color:var(--muted-2);
  border:1px solid rgba(43,140,255,0.25); background:rgba(7,104,255,0.05); white-space:nowrap;
}
.lab-chat-chip:hover { border-color:rgba(43,140,255,0.55); background:rgba(43,140,255,0.12); color:var(--fg); }
/* The two studio questions ("how do I save?") sit with the six about blocks but are not the same
   sort of question, so they are tinted rather than separated — a second row with a heading would
   make a shelf into a menu. */
.lab-chat-chip.is-guide { border-style:dashed; }

/* A tip from mebo, in the transcript. Marked, because "here is something you did not ask about"
   should not look identical to an answer to a question you did. */
.lab-chat-body.is-tip { border-left:2px solid rgba(43,140,255,0.45); padding-left:10px; }
/* "Don't show me any more tips", under the tip itself. Quiet, but a real button with a real hit
   area — findable when you want it, ignorable when you don't. */
.lab-chat-tipoff {
  display:block; margin-top:9px; padding:2px 0; background:none; border:0; cursor:pointer;
  font-size:11px; color:var(--muted-2); text-decoration:underline; text-align:left;
}
.lab-chat-tipoff:hover { color:var(--fg); }
.lab-chat-tipoff:disabled { cursor:default; text-decoration:none; opacity:0.8; }

/* "Show me": the pointing finger for a real control, the button-shaped sibling of the warning badge
   Blockly puts on a block. Four seconds and it takes itself off — it points, it does not decorate.
   The ring is drawn OUTSIDE the element (box-shadow, not border) so nothing in the toolbar moves. */
@keyframes mb-point {
  0%, 100% { box-shadow:0 0 0 2px rgba(43,140,255,0.65), 0 0 12px 2px rgba(43,140,255,0.35); }
  50%      { box-shadow:0 0 0 4px rgba(43,140,255,0.95), 0 0 20px 5px rgba(43,140,255,0.55); }
}
.is-pointed {
  animation:mb-point 0.9s ease-in-out 3; border-radius:8px; position:relative; z-index:2;
}
@media (prefers-reduced-motion: reduce) {
  .is-pointed { animation:none; box-shadow:0 0 0 3px rgba(43,140,255,0.9); }
}

/* the composer: the box and the button share a row; the button carries the countdown when clocked */
.lab-chat-composer { display:flex; gap:8px; align-items:flex-end; }
.lab-chat-composer textarea { flex:1; }
.lab-chat-send {
  flex:none; min-height:42px; min-width:58px; padding:0 16px; font-size:13px;
  display:inline-flex; align-items:center; justify-content:center;
}
.lab-chat-send:disabled { opacity:0.55; cursor:default; font-variant-numeric:tabular-nums; }

/* Reviewing a pupil's game: a DIFFERENT disabled look from the ordinary countdown or daily-limit
   dimming — a teacher must recognise this as deliberate without reading the note first, and must
   not mistake it for "wait a few seconds" or "no more today", both of which clear on their own. */
.lab-help-review-note {
  margin:0 0 8px; padding:8px 10px; border-radius:10px; font-size:12.5px; line-height:1.4;
  background:rgba(234,179,8,0.10); border:1px solid rgba(234,179,8,0.32); color:#f0dca0;
}
.lab-help-review-note[hidden] { display:none; }
#lab-help-composer.is-review-locked textarea {
  cursor:not-allowed; opacity:.5;
  background-image:repeating-linear-gradient(135deg, rgba(255,255,255,.05) 0 10px, transparent 10px 20px);
}
#lab-help-composer.is-review-locked .lab-chat-send {
  opacity:.5; cursor:not-allowed; filter:grayscale(1);
}
.lab-chat-meta { display:flex; align-items:center; gap:8px; }
.lab-chat .lab-help-foot { margin-top:10px; }

/* ------------------------------ the sprite makers ------------------------------
   Two modals, one family: the character builder (parts + colours, animation free) and the pixel
   editor (the finishing tool). Both borrow .lab-mebo's card so they read as the same product. */
/* Take one, build one, draw one — three equal ways to get a picture, side by side. */
.lab-get-art { display:grid; grid-template-columns:repeat(3, 1fr); gap:8px; margin-bottom:22px; }
/* Pictures and Sounds have one way in, not three: a full-width button would read as a banner,
   so it keeps a third of the row and sits where the gallery button sits on the Sprites tab. */
.lab-get-art.is-one { grid-template-columns:1fr 1fr 1fr; }
.lab-get-art.is-one .lab-gallery-open { grid-column:1; }
.lab-get-art .lab-gallery-open { width:100%; margin:0; }
@media (max-width: 900px) { .lab-get-art { grid-template-columns:1fr; } }

/* The two buttons on a look card: draw on it, and throw it away.
 *
 * The pencil was `opacity:0` until `:hover` and nothing else — no :focus-within, no non-hover twin —
 * so on a touch device it was invisible and still hit-testable: a child could only ever press it by
 * accident. That matters more than a hidden button usually would, because "a child replaces mebo's
 * art by drawing over it" is a locked decision and this is the only door to it from the card.
 *
 * The ✕ beside it had NO CSS AT ALL — one grep hit in the whole of public/account/, the line that
 * renders it — so it drew as a raw user-agent button at 23x18px, square corners, 11px type. The
 * destructive one, at half the minimum tap size, next to an invisible pencil.
 *
 * So both are visible always, both are 32px on a mouse and 44 on a finger, and the ✕ is red because
 * it is a different KIND of act from the pencil next to it and should read that way before it is
 * read as a symbol. `(hover: none)` is the first use of that idiom in this codebase; it is the right
 * question to ask, rather than a width, because what is missing on an iPad is the hover and not the
 * pixels. */
.sprite-state-draw, .sprite-state-del {
  position:absolute; top:4px; display:flex; align-items:center; justify-content:center;
  min-width:32px; min-height:32px; padding:0; border:0; border-radius:8px; cursor:pointer;
  font-size:13px; line-height:1; background:rgba(5,3,15,0.8); color:var(--fg);
  transition:background .12s, color .12s;
}
.sprite-state-draw { left:4px; }
.sprite-state-del { right:4px; color:var(--red); font-size:14px; font-weight:700; }
.sprite-state-draw:hover { background:rgba(43,140,255,0.35); }
.sprite-state-del:hover { background:rgba(255,93,108,0.28); color:#fff; }
/* On a finger they are 44px, and they come OFF the picture to get there: two 44px squares overlaid
   on a 96px card cover most of the thumbnail they are about, which trades one unusable control for
   another. Bottom corners instead, in the card's own footer — 4 + 44 + 44 + 4 is exactly 96. */
@media (hover: none) {
  .sprite-state { padding-bottom:57px; }
  .sprite-state-draw, .sprite-state-del { top:auto; bottom:5px; min-width:44px; min-height:44px; }
}

.lab-builder { width:660px; }
/* Which way up the world is, first and biggest: it swaps the whole part list under everything. */
.lab-builder-views { display:grid; grid-template-columns:1fr 1fr; gap:9px; margin:4px 0 14px; }
.lab-builder-view {
  display:flex; flex-direction:column; gap:2px; text-align:left; cursor:pointer;
  padding:10px 13px; border-radius:12px; color:var(--fg);
  border:1px solid rgba(43,140,255,0.25); background:rgba(7,104,255,0.06);
}
.lab-builder-view:hover { border-color:rgba(43,140,255,0.55); background:rgba(43,140,255,0.11); }
.lab-builder-view.is-active { border-color:rgba(43,140,255,0.85); background:rgba(43,140,255,0.18); }
.lab-builder-view strong { font-size:13.5px; font-weight:600; }
.lab-builder-view span { font-size:11.5px; color:var(--muted); line-height:1.4; }

.lab-builder-grid { display:grid; grid-template-columns:auto 1fr; gap:18px; margin-top:6px; }
.lab-builder-facings { display:flex; flex-direction:column; gap:5px; }
.lab-builder-facing { display:flex; align-items:center; gap:7px; font-size:12px; color:var(--muted-2); cursor:pointer; }
.lab-builder-facing input { accent-color:var(--blue); }
.lab-builder-facing input:disabled { opacity:0.5; }
/* The colour of one part, shown as itself. A swatch needs no label and no unit. */
.lab-builder-dot {
  width:22px; height:22px; flex:none; border-radius:50%; cursor:pointer; padding:0;
  border:2px solid rgba(255,255,255,0.2);
}
.lab-builder-dot:hover { border-color:#fff; }
.lab-builder-tray {
  display:flex; flex-wrap:wrap; gap:5px; padding:8px; margin:0 0 6px; border-radius:10px;
  background:rgba(5,3,15,0.5); border:1px solid rgba(43,140,255,0.2);
}
.lab-builder-tray[hidden] { display:none; }
.lab-builder-canvas {
  width:192px; height:192px; image-rendering:pixelated; border-radius:14px;
  background:
    linear-gradient(45deg, rgba(255,255,255,0.05) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.05) 75%),
    linear-gradient(45deg, rgba(255,255,255,0.05) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.05) 75%),
    #10132b;
  background-size:16px 16px; background-position:0 0, 8px 8px;
  border:1px solid rgba(43,140,255,0.3);
}
.lab-builder-poses { display:flex; flex-wrap:wrap; gap:8px; margin:6px 0 12px; max-height:190px; overflow-y:auto; }
.lab-builder-pose { display:flex; flex-direction:column; align-items:center; gap:2px; }
.lab-builder-pose canvas {
  width:40px; height:40px; image-rendering:pixelated; border-radius:8px;
  background:#10132b; border:1px solid rgba(43,140,255,0.2);
}
.lab-builder-pose span { font-size:10px; color:var(--muted); }
#lab-builder-dice { width:100%; min-height:36px; }
.lab-builder-slot { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
.lab-builder-slot-label { width:64px; font-size:12.5px; color:var(--muted); }
.lab-builder-slot-value { flex:1; text-align:center; font-size:13px; color:var(--fg); }
.lab-builder-slot .iconbtn { min-width:30px; min-height:30px; }
.lab-builder-swatch-label { margin:12px 0 6px; font-size:12px; color:var(--muted); }
.lab-builder-swatches { display:flex; flex-wrap:wrap; gap:6px; }

.lab-paint { width:480px; }
.lab-paint-stage {
  position:relative; width:320px; height:320px; margin:10px auto; border-radius:12px; overflow:hidden;
  background:
    linear-gradient(45deg, rgba(255,255,255,0.07) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.07) 75%),
    linear-gradient(45deg, rgba(255,255,255,0.07) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.07) 75%),
    #0b0e22;
  background-size:20px 20px; background-position:0 0, 10px 10px;
  border:1px solid rgba(43,140,255,0.3);
}
/* Fill the stage rather than assume it is 320 square: paint.js sizes the stage to the picture's own
   proportions, and a layer pinned to 320 would stretch a wide sprite back out of shape. */
.lab-paint-layer { position:absolute; inset:0; width:100%; height:100%; image-rendering:pixelated; }
.lab-paint-layer.is-top { touch-action:none; cursor:crosshair; }
.lab-paint-tools { display:flex; gap:6px; align-items:center; margin:4px 0 8px; }
.lab-paint-tools .ghostbtn { min-height:32px; padding:0 10px; font-size:13px; }
.lab-paint-tools .is-active, #lab-paint-mirror.is-active {
  border-color:rgba(43,140,255,0.8); background:rgba(43,140,255,0.18); color:var(--fg);
}
.lab-paint-palette { display:flex; flex-wrap:wrap; gap:5px; }
.lab-paint-swatch {
  width:24px; height:24px; border-radius:7px; cursor:pointer; padding:0;
  border:2px solid rgba(255,255,255,0.12);
}
.lab-paint-swatch.is-active { border-color:#fff; box-shadow:0 0 0 2px rgba(43,140,255,0.6); }
.lab-paint-swatch.is-transparent {
  background:
    linear-gradient(45deg, #666 25%, transparent 25%, transparent 75%, #666 75%),
    linear-gradient(45deg, #666 25%, transparent 25%, transparent 75%, #666 75%), #333;
  background-size:8px 8px; background-position:0 0, 4px 4px;
}
.lab-paint-name-row { margin-bottom:4px; }
.lab-paint-sizelabel { font-size:11.5px; color:var(--muted); }
.lab-paint-sizes { display:flex; gap:4px; }
/* A brush button is a dot of the brush's actual size — the control shows what it does. */
.lab-paint-size {
  width:26px; height:26px; display:inline-flex; align-items:center; justify-content:center;
  padding:0; border-radius:8px; cursor:pointer;
  border:1px solid rgba(43,140,255,0.25); background:rgba(7,104,255,0.06);
}
.lab-paint-size span { display:block; border-radius:2px; background:var(--muted-2); }
.lab-paint-size.is-active { border-color:rgba(43,140,255,0.8); background:rgba(43,140,255,0.18); }
.lab-paint-size.is-active span { background:#fff; }
.lab-paint-fill { display:flex; align-items:center; gap:5px; font-size:11.5px; color:var(--muted-2); cursor:pointer; }
.lab-paint-fill input { accent-color:var(--blue); }

/* ---------------------------- mebo: the new-game wizard ----------------------------
   A centred modal, unlike the help panel: there is no workspace behind it yet, so there is nothing
   for it to cover. Borrows the help panel's cards, lead text and dots wholesale. */
.lab-mebo {
  position:relative; width:560px; max-width:100%; max-height:calc(100dvh - 40px); overflow-y:auto;
  padding:22px 24px; border-radius:20px; text-align:left;
  border:1px solid rgba(43,140,255,0.32); background:linear-gradient(180deg,#0e1330,#0a0c20);
  box-shadow:0 30px 90px rgba(0,0,0,0.65),0 0 60px rgba(7,104,255,0.15); animation:wbpop .18s ease-out;
}
.lab-mebo-step[hidden] { display:none; }
.lab-mebo-spaced { margin-top:18px; }
.lab-mebo-optional { font-weight:400; font-size:12px; color:var(--muted); }

.lab-mebo-rail { display:flex; gap:6px; list-style:none; padding:0; margin:0 0 18px; }
/* `display:flex` above beats the hidden attribute, so say it again — the rail is hidden when mebo
   is resting, where there are no steps left to be part-way through. */
.lab-mebo-rail[hidden] { display:none; }
.lab-mebo-rail li { flex:1; height:4px; border-radius:2px; background:rgba(43,140,255,0.16); }
.lab-mebo-rail li.is-active, .lab-mebo-rail li.is-done { background:linear-gradient(90deg,#0768ff,#2b8cff); }

.lab-mebo textarea {
  width:100%; padding:11px 14px; border-radius:11px; font:inherit; font-size:16px; resize:vertical;
  color:var(--fg); background:rgba(5,3,15,0.7); border:1px solid rgba(43,140,255,0.25);
}
.lab-mebo textarea:focus {
  outline:none; border-color:rgba(43,140,255,0.7); box-shadow:0 0 0 3px rgba(43,140,255,0.15);
}
/* The chosen card in the two-part "how do you win / how do you lose" step, which does not advance
   until both halves are answered — so the first answer has to stay visible. */
.lab-help-card.is-chosen { border-color:var(--blue); background:rgba(43,140,255,0.18); }

.lab-mebo-card {
  padding:16px 18px; border-radius:14px;
  border:1px solid rgba(43,140,255,0.28); background:rgba(7,104,255,0.08);
}
.lab-mebo-card h4 { font-family:var(--f-display); font-size:18px; margin:0; color:var(--fg); }
.lab-mebo-blurb { font-size:13px; color:var(--muted-2); margin:5px 0 12px; line-height:1.5; }
.lab-mebo-card ul { margin:0; padding-left:19px; }
.lab-mebo-card li { font-size:13px; color:var(--fg); line-height:1.65; }
.lab-made-name {
  margin:10px 0 4px; font-family:var(--f-display); font-weight:700; font-size:19px; color:var(--fg);
}
.lab-made-desc { margin:0; font-size:13.5px; line-height:1.55; color:var(--muted); }
.lab-made-legend-lead {
  margin:18px 0 8px; font-size:11px; text-transform:uppercase; letter-spacing:.5px;
  font-weight:700; color:var(--muted);
}
.lab-made-legend { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:7px; }
.lab-made-row {
  display:flex; align-items:center; gap:11px; padding:8px 11px; border-radius:10px;
  background:rgba(5,3,15,0.45); border:1px solid rgba(43,140,255,0.16);
}
/* A fixed box the picture is drawn INSIDE at its real proportions, so a tall thing reads as tall
   and a small thing as small — which is the whole point of showing it rather than describing it. */
/* The same light checker the gallery uses. A dark tile hid a black tree completely, and the whole
   point of this row is that you can see which thing it is — so the backing has to work for a black
   sprite and a white one at the same time. */
.lab-made-pic {
  flex:none; width:34px; height:34px; border-radius:7px; display:flex;
  align-items:center; justify-content:center; overflow:hidden;
  background-color:#c8c8c8;
  background-image:linear-gradient(45deg,#a9a9a9 25%,transparent 25%,transparent 75%,#a9a9a9 75%),
                   linear-gradient(45deg,#a9a9a9 25%,transparent 25%,transparent 75%,#a9a9a9 75%);
  background-size:10px 10px; background-position:0 0,5px 5px;
}
.lab-made-pic img { image-rendering:pixelated; object-fit:contain; }
.lab-made-text { display:flex; flex-direction:column; gap:1px; min-width:0; flex:1; }
.lab-made-text strong { font-size:13px; color:var(--fg); }
.lab-made-job { font-size:11.5px; color:var(--muted); }
.lab-made-next {
  margin:16px 0 0; padding:12px 14px; border-radius:11px;
  background:rgba(70,224,138,0.07); border:1px solid rgba(70,224,138,0.22);
}
.lab-made-next-lead { margin:0 0 7px; font-weight:700; font-size:13px; color:#8ff0b6; }
.lab-made-next ul { margin:0; padding-left:18px; }
.lab-made-next li { font-size:12.5px; line-height:1.6; color:var(--fg); }
.lab-mebo-name {
  width:100%; padding:9px 11px; border-radius:9px; font:inherit; color:var(--fg);
  background:rgba(5,3,15,0.55); border:1px solid rgba(43,140,255,0.28);
}
.lab-mebo-name:focus {
  outline:none; border-color:rgba(43,140,255,0.7); box-shadow:0 0 0 3px rgba(43,140,255,0.15);
}
/* The three things a good idea says. Stated, not folded away: a child cannot yet write a prompt,
   and an empty box with "tell me your idea" over it teaches them nothing. */
.lab-mebo-recipe {
  margin:0 0 9px; padding:9px 12px 9px 26px; border-radius:9px;
  background:rgba(43,140,255,0.06); border:1px solid rgba(43,140,255,0.16);
}
.lab-mebo-recipe li { font-size:12.5px; line-height:1.65; color:var(--muted); }
.lab-mebo-recipe strong { color:var(--fg); }
.lab-mebo-eg-box { margin-top:14px; }
.lab-mebo-eg-lead { margin:0 0 6px; font-size:12px; color:var(--muted); }
.lab-mebo-eg-why { display:flex; flex-direction:column; gap:2px; margin:7px 0 0; }
.lab-mebo-eg-why span { font-size:11.5px; color:var(--muted); }
.lab-mebo-eg-why em { color:#8ff0b6; font-style:normal; font-weight:600; }
.lab-mebo-eg-tap { margin:7px 0 0; font-size:11.5px; color:var(--muted-2); }
/* The same lesson by demonstration — tap one, then change it into your own. */
.lab-mebo-eg {
  display:block; width:100%; text-align:left; font:inherit; font-size:12.5px; line-height:1.45;
  color:var(--fg); padding:9px 12px; border-radius:9px; cursor:pointer;
  background:rgba(5,3,15,0.4); border:1px dashed rgba(43,140,255,0.3);
}
.lab-mebo-eg:hover { border-color:rgba(43,140,255,0.65); background:rgba(43,140,255,0.08); }
.lab-mebo-else-label { display:block; margin:14px 0 6px; font-size:12px; color:var(--muted); }
.lab-mebo-else { display:flex; gap:8px; }
.lab-mebo-else input {
  flex:1; padding:8px 11px; border-radius:9px; font:inherit; font-size:16px; color:var(--fg);
  background:rgba(5,3,15,0.55); border:1px solid rgba(43,140,255,0.28);
}
.lab-mebo-else input:focus {
  outline:none; border-color:rgba(43,140,255,0.7); box-shadow:0 0 0 3px rgba(43,140,255,0.15);
}
.lab-mebo-resting-note { font-size:13px; line-height:1.55; color:var(--muted); margin:8px 0 16px; }
.lab-mebo-art { font-size:12px; color:var(--muted); margin:12px 0 0; }
/* What mebo could not build. Amber, not red: this is honesty about the starting point, not an
   error — a child should read it and still press Build it. */
.lab-mebo-cannot {
  margin:12px 0 0; padding:10px 12px; border-radius:10px;
  background:rgba(255,176,32,0.08); border:1px solid rgba(255,176,32,0.28);
}
.lab-mebo-cannot-lead { margin:0 0 6px; font-size:12.5px; font-weight:700; color:#ffce70; }
.lab-mebo-cannot ul { margin:0; padding-left:18px; }
.lab-mebo-cannot li { font-size:12.5px; line-height:1.5; color:var(--fg); }
.lab-mebo-cannot-foot { margin:7px 0 0; font-size:12px; color:var(--muted); }
/* When the ask was a whole genre away, this is the headline rather than the footnote. */
.lab-mebo-cannot.is-big { background:rgba(255,176,32,0.13); border-color:rgba(255,176,32,0.45); }
.lab-mebo-after { font-size:12.5px; color:var(--muted); line-height:1.5; margin:13px 0 0; }

/* The first-run card that starts all this. mebo's face instead of an emoji, because he is a
   character the child is about to work with rather than a category of thing. */
.starter-card .starter-icon img { display:block; width:34px; height:auto; }

@media (max-width:560px) {
  .lab-help-cards { grid-template-columns:1fr; }
  .lab-help-actions { flex-wrap:wrap; }
}

/* ============================ responsive ============================
   The EDITOR does not reflow — it has a fixed 1040px floor and scrolls sideways below it (see the
   small-screens section above). So nothing here touches the toolbar, the panes or .lab-body:
   the old rules that collapsed them into one column at ≤1023px are exactly what made the file tree
   fill the screen (block layout, and the view-tab toggle only ever hid the blocks/code panes,
   never the files pane). What remains is for things that genuinely ARE viewport-responsive: the
   first-run / template screens (ordinary pages), and the fixed-position overlays and dialogs, none
   of which are inside the editor's scroll region. */
@media (max-width:640px) {
  .workspace:has(#tab-lab:not([hidden]) #lab-editor[hidden]) { padding:16px 14px 0; }
  .starter-grid { grid-template-columns:1fr; }
  .lab-drawer { width:100%; }
  .lab-run-controls .ghostbtn, .lab-run-controls .primary { flex:1; }
}
@media (max-width:520px) {
  .lab-assets { padding:14px; }
  .sprite-new { flex-direction:column; align-items:flex-start; gap:12px; }
  .lab-rollback { padding:20px; }
  .lab-rollback-actions { flex-direction:column-reverse; }
  .lab-rollback-actions > * { width:100%; }
}

/* ============================ focus + motion ============================
   Every interactive element gets a visible ring (the mockup had none). */
#tab-lab :focus-visible { outline:2px solid var(--blue); outline-offset:2px; }
#tab-lab .lab-name:focus-visible, #tab-lab .sprite-name:focus-visible { outline-offset:0; }
.lab-divider:focus-visible span { background:var(--blue); height:76px; }

@media (prefers-reduced-motion: reduce) {
  .lab-invalid-block { animation:none; filter:drop-shadow(0 0 7px rgba(255,93,108,0.9)); }
  .lab-drawer, .lab-overlay, .lab-rollback, .starter-card { animation:none; }
  .pill.is-running::before, .lab-savestate.is-saving .dot { animation:none; }
  .lab-divider span, .lab-divider:hover span { transition:none; }
}

/* Deleting a saved version is the one destructive thing in the history drawer, so it reads red —
   restoring is safe (it makes a recovery point), this is not. */
.lab-rollback-icon.is-danger {
  background:rgba(255,93,108,0.12); border-color:rgba(255,93,108,0.35); color:var(--red);
}
.version-del { width:30px; height:30px; font-size:13px; }
.iconbtn.danger { border-color:rgba(255,93,108,0.3); color:var(--red); }
.iconbtn.danger:hover:not(:disabled) {
  color:var(--red); border-color:rgba(255,93,108,0.6); background:rgba(255,93,108,0.1);
}

/* Asset thumbnails render an <img>, not a background — without this a 16x16 sprite draws at its
   natural size in the corner of the tile instead of filling it. Pixelated so small art scales up
   crisp rather than blurred. */
.sprite-state-thumb, .asset-row-thumb { display:flex; align-items:center; justify-content:center; overflow:hidden; }
.sprite-state-thumb img, .asset-row-thumb img {
  width:100%; height:100%; object-fit:contain; image-rendering:pixelated; padding:12%;
}

/* .workspace sets z-index:1, which creates a stacking context — so the now-fixed overlays are
   trapped inside it and paint UNDER the app bar (z-index:40), hiding the run dialog's title bar.
   Their own z-index can't escape an ancestor's context, so lift the workspace itself, and only
   while one is actually open. */
.workspace:has(.lab-overlay:not([hidden])),
.workspace:has(.lab-modal-wrap:not([hidden])),
.workspace:has(.lab-drawer-wrap:not([hidden])) { z-index:50; }

/* ---- request a sprite ---- */
/* Under the gallery grid, not above it: the gallery is instant and this is a person drawing
   something, so it is offered only once you have looked and not found it. */
.lab-gallery-foot {
  display:flex; align-items:center; justify-content:center; gap:10px; flex:none;
  padding:14px; border-top:1px solid rgba(43,140,255,0.14); font-size:13px;
}
/* A class selector setting `display` beats the browser's own [hidden] rule, so the JS that hides
   this for the SOUND gallery — where "request a sprite" answers a question nobody asked — set the
   attribute and nothing happened. Any element hidden from script needs this pair. */
.lab-gallery-foot[hidden] { display:none; }
.sprite-req-hint { margin:0 0 8px; font-size:12.5px; color:var(--muted); line-height:1.5; }
.sprite-req-hint strong { color:var(--fg); font-weight:600; }
/* The eight headings laid out AS a compass — four columns with the middle of the second row left
   empty, so N is above NE and W is left of E. A flat list of eight checkboxes makes you read the
   labels; this shape you can point at. */
.sprite-req-compass {
  display:grid; grid-template-columns:repeat(3, 1fr); gap:8px; max-width:320px;
}
.sprite-req-compass .check { margin:0; font-size:13px; }
.sprite-req-compass .check:nth-child(4) { grid-column:1; }
.sprite-req-compass .check:nth-child(5) { grid-column:3; }

/* The ask's row id, under what mebo said. Quiet enough that a child never asks what it is, there
   enough that a grown-up can quote it into Admin · mebo and find the exact input and output. */
.lab-chat-id {
  display:block; margin-top:6px; font-size:10px; line-height:1;
  font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
  color:var(--muted); opacity:0.35; user-select:all; cursor:text;
}
.lab-chat-id:hover { opacity:0.75; }
