:root {
  color-scheme: dark light;
  /* XploreBot brand. --orange is sampled from the supplied logo files
     (#F15F24). --orange-ink is a darkened brand orange used ONLY for small
     text: #F15F24 on white is 3.30:1, which fails WCAG AA for body copy,
     while #C24A17 reaches 4.94:1. Fills keep the true brand orange, where
     3:1 is the applicable threshold.
     Buttons pair BLACK on orange (6.36:1) rather than white on orange
     (3.30:1) — which is also how the wordmark itself pairs them. */
  /* V2's IDE palette. The wordmark is black-on-transparent and cannot sit
     on a dark bar, so the header uses the orange badge logo, which is drawn
     for exactly this. On dark, #f15f24 reaches 5.6:1 — it can carry small
     text here, unlike on white where it needed darkening. */
  --ink: #e8edf7;
  --muted: #94a3b8;
  --surface: #0b1220;
  --panel: #131c2e;
  --line: #24304a;
  --orange: #f15f24;
  --orange-ink: #ff8654;
  --orange-soft: #2a1a12;
  --canvas: #0f1728;
  --toolbox: #131c2e;
  --flyout: #131c2e;
  --grid: #334155;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

/* Light theme. The two differ in more than lightness: on white, #f15f24 is
   only 3.30:1 and cannot carry small text, so --orange-ink darkens; on the
   dark shell the brand orange itself is legible and --orange-ink lightens
   instead. Same token, opposite direction, for the same reason. */
:root[data-theme="light"] {
  color-scheme: light;
  --ink: #100d0b;
  --muted: #6b6560;
  --surface: #ffffff;
  --panel: #fbf8f6;
  --line: #e6e0da;
  --orange-ink: #c24a17;
  --orange-soft: #fdece4;
  --canvas: #ffffff;
  --toolbox: #fbf8f6;
  --flyout: #ffffff;
  --grid: #d8d0c9;
}

:root[data-theme="light"] .topbar { background: rgba(255, 255, 255, .92); }
:root[data-theme="light"] .status[data-kind="error"],
:root[data-theme="light"] .status[data-kind="warning"] { color: #a8320c; }
:root[data-theme="light"] .checks-panel[data-state="ok"] { border-color: #1e7a4d; }
:root[data-theme="light"] .checks-panel[data-state="error"] { border-color: #a8320c; }
:root[data-theme="light"] .checks-list .safety { color: #8a2c0a; }
:root[data-theme="light"] .checks-ok { color: #1e7a4d; }
:root[data-theme="light"] .server-notice { color: #6b1f08; }
:root[data-theme="light"] .danger-button { border-color: #9b1c0e; background: #9b1c0e; }

/* The wordmark is black-on-transparent; the badge is white-on-orange. Each
   is shown on the theme it can actually be read on — `data-shown-on` names
   that theme, so the markup no longer reads backwards. */
.brand-logo[data-shown-on="light"] { display: none; }
.brand-logo[data-shown-on="dark"] { display: block; }
:root[data-theme="light"] .brand-logo[data-shown-on="light"] { display: block; }
:root[data-theme="light"] .brand-logo[data-shown-on="dark"] { display: none; }

.theme-toggle {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: 11px;
  background: var(--panel);
  color: var(--ink);
  font-size: 1.05rem;
  line-height: 1;
}

.theme-toggle:hover { border-color: var(--orange); }
.theme-toggle:focus-visible { outline: 3px solid var(--orange-ink); outline-offset: 2px; }

.topbar-actions { display: flex; align-items: center; gap: .9rem; }

* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; color: var(--ink); background: var(--surface); }
button, input, select { font: inherit; }
button { cursor: pointer; }
.skip-link { position: fixed; left: 1rem; top: -4rem; z-index: 20; padding: .8rem 1rem; color: var(--ink); background: var(--orange); border-radius: .5rem; font-weight: 750; }
.skip-link:focus { top: 1rem; }
/* `min-height`, not `height`, with real vertical padding.
   A fixed height and centred content left the logo sitting hard against
   the window's top edge — in the desktop shell there is no title bar above
   it, so "centred in 76px" is 21px of air below the frame and nothing that
   reads as a margin. The padding gives the same air on both sides and lets
   the row grow rather than crushing its contents if anything wraps. */
.topbar { min-height: 84px; display: flex; align-items: center; justify-content: space-between; padding: .85rem clamp(1.25rem, 5vw, 5rem); border-bottom: 1px solid var(--line); background: rgba(11, 18, 32, .92); backdrop-filter: blur(8px); }
.brand { display: flex; align-items: center; gap: .8rem; color: inherit; text-decoration: none; }
.brand-logo { display: block; height: 34px; width: auto; border-radius: 7px; }
.brand strong, .brand small { display: block; }
.brand strong { font-size: 1.05rem; letter-spacing: .01em; }
.brand small { margin-top: .1rem; color: var(--muted); font-size: .72rem; }
.brand-divider { width: 1px; height: 26px; background: var(--line); }
.local-badge { display: flex; align-items: center; gap: .5rem; color: var(--muted); font-size: .8rem; }
.local-badge span { width: 8px; height: 8px; border-radius: 99px; background: var(--orange); }
main { min-height: calc(100vh - 76px); background: radial-gradient(circle at 82% 0%, #f15f241f 0, transparent 32rem); }
.hero { display: flex; align-items: flex-end; justify-content: space-between; gap: 2rem; padding: clamp(3.5rem, 8vw, 7rem) clamp(1.25rem, 8vw, 8rem) clamp(3rem, 6vw, 5rem); max-width: 1500px; margin: auto; }
.eyebrow { margin: 0 0 1rem; color: var(--orange-ink); font-weight: 800; font-size: .72rem; letter-spacing: .18em; }
h1 { margin: 0; max-width: 750px; font-size: clamp(3rem, 7vw, 6.5rem); line-height: .94; letter-spacing: -.065em; }
h1 em { color: var(--orange); font-style: normal; }
.hero-copy { max-width: 600px; margin: 1.5rem 0 0; color: var(--muted); font-size: clamp(1rem, 1.5vw, 1.18rem); line-height: 1.65; }
.primary-button, .secondary-button { min-height: 48px; padding: .75rem 1.15rem; border-radius: 12px; font-weight: 750; }
.primary-button { color: #12100e; border: 1px solid var(--orange); background: var(--orange); box-shadow: 0 10px 26px #f15f2433; }
.primary-button:hover { filter: brightness(1.12); transform: translateY(-1px); }
.primary-button:focus-visible, .secondary-button:focus-visible, input:focus-visible, select:focus-visible, .project-card:focus-visible, .dialog-close:focus-visible { outline: 3px solid var(--orange-ink); outline-offset: 3px; }
.primary-button:disabled { cursor: wait; opacity: .55; }
.workspace { max-width: 1500px; min-height: 360px; margin: auto; padding: 0 clamp(1.25rem, 8vw, 8rem) 6rem; }
.section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; padding-bottom: 1.25rem; border-bottom: 1px solid var(--line); }
.section-heading .eyebrow { margin-bottom: .45rem; }
h2 { margin: 0; font-size: clamp(1.8rem, 3vw, 2.5rem); letter-spacing: -.04em; }
.section-heading > p { color: var(--muted); font-size: .85rem; }
.status { min-height: 2.5rem; padding: .75rem 0; color: var(--muted); font-size: .9rem; }
.status[data-kind="success"] { color: var(--orange-ink); }
.status[data-kind="error"], .status[data-kind="warning"] { color: #ff9f7a; }
.project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 275px), 1fr)); gap: 1.25rem; }
.project-card { overflow: hidden; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); transition: border-color .2s, transform .2s, box-shadow .2s; }
.project-card:hover, .project-card:focus-visible { border-color: var(--orange); transform: translateY(-3px); box-shadow: 0 12px 30px #100d0b14; }
.project-visual { position: relative; display: grid; place-items: center; height: 150px; overflow: hidden; color: var(--orange); font-size: 3.8rem; background: var(--orange-soft); }
.project-visual::before, .project-visual::after, .project-visual i, .project-visual b { content: ""; position: absolute; border: 1px solid #f15f2440; border-radius: 50%; }
.project-visual::before { width: 170px; height: 170px; }
.project-visual::after { width: 230px; height: 230px; }
.project-visual i { width: 9px; height: 9px; top: 24%; right: 22%; background: var(--orange); }
.project-visual b { width: 7px; height: 7px; bottom: 22%; left: 25%; background: var(--ink); }
.project-body { padding: 1.15rem; }
.project-meta, .card-footer { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.project-meta time, .target, .saved-state { color: var(--muted); font-size: .75rem; }
.mode-badge { padding: .3rem .45rem; color: var(--orange-ink); border-radius: 5px; background: var(--orange-soft); font-size: .64rem; font-weight: 850; letter-spacing: .08em; }
.project-card h3 { margin: 1rem 0 .35rem; font-size: 1.12rem; }
.target { margin: 0; }
.card-footer { margin-top: 1.2rem; padding-top: .9rem; border-top: 1px solid var(--line); }
.saved-state { color: var(--orange-ink); }
.open-arrow { color: var(--orange); font-size: 1.2rem; }
.empty-state { padding: 4rem 1rem; text-align: center; }
.empty-icon { margin: auto; color: var(--orange); font-size: 3rem; }
.empty-state h3 { margin: .8rem 0; font-size: 1.35rem; }
.empty-state p { max-width: 480px; margin: 0 auto 1.5rem; color: var(--muted); line-height: 1.6; }
.secondary-button { color: var(--ink); border: 1px solid var(--line); background: var(--surface); }
dialog { width: min(92vw, 510px); padding: 0; color: var(--ink); border: 1px solid var(--line); border-radius: 20px; background: var(--surface); box-shadow: 0 30px 90px #100d0b26; }
dialog::backdrop { background: #100d0b66; backdrop-filter: blur(6px); }
dialog form { position: relative; padding: 2rem; }
.dialog-close { position: absolute; right: 1rem; top: 1rem; width: 36px; height: 36px; color: var(--muted); border: 0; border-radius: 8px; background: var(--panel); font-size: 1.4rem; }
dialog h2 { margin-bottom: 1.8rem; }
dialog label { display: block; margin: 1rem 0 .45rem; color: var(--ink); font-size: .8rem; font-weight: 700; }
dialog input, dialog select { width: 100%; min-height: 48px; padding: .7rem .8rem; color: var(--ink); border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.form-note { color: var(--muted); font-size: .78rem; line-height: 1.5; }
.dialog-actions { display: flex; justify-content: flex-end; gap: .75rem; margin-top: 1.6rem; }
@media (max-width: 700px) { .hero { display: block; } .hero .primary-button { width: 100%; margin-top: 2rem; } .local-badge { font-size: 0; } .section-heading { align-items: flex-start; flex-direction: column; } }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } }

/* ---------------------------------------------------------- workspace view */

.hero-actions {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  align-items: center;
}

.workspace-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
  margin: 1.5rem 0 2rem;
  padding: 1.25rem;
  border: 1px solid var(--line);
  border-radius: 14px;
}

.workspace-facts dt {
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.6;
  margin-bottom: 0.35rem;
}

.workspace-facts dd {
  margin: 0;
  font-weight: 600;
}

.workspace-subhead {
  margin: 2rem 0 0.5rem;
  font-size: 1rem;
}

.content-preview {
  margin: 0.5rem 0 0;
  padding: 1rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  /* Long generated Python and block XML must scroll inside the box
     rather than stretching the page. */
  overflow-x: auto;
  max-height: 20rem;
  white-space: pre;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85rem;
  line-height: 1.5;
}

.revision-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
}

.revision-list li {
  display: flex;
  gap: 1rem;
  align-items: baseline;
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--line);
}

.revision-list strong {
  min-width: 9rem;
  text-transform: capitalize;
}

.revision-list time {
  opacity: 0.65;
  font-size: 0.85rem;
}

.project-card { cursor: pointer; }

.project-card:focus-visible {
  outline: 2px solid var(--orange-ink);
  outline-offset: 3px;
}

/* ------------------------------------------------------------- archiving */

.archived-section {
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px solid var(--line);
}

.archived-section h3 { margin: 0 0 .35rem; font-size: 1.05rem; }

.archived-list { list-style: none; margin: 1rem 0 0; padding: 0; }

.archived-list li {
  display: flex;
  gap: 1rem;
  align-items: center;
  flex-wrap: wrap;
  padding: .85rem 0;
  border-bottom: 1px solid var(--line);
}

.archived-list .archived-name { font-weight: 700; }
.archived-list .archived-meta { color: var(--muted); font-size: .85rem; }
.archived-list .spacer { flex: 1; }

.archived-list button {
  min-height: 40px;
  padding: .45rem .85rem;
  border-radius: 9px;
  font-weight: 700;
  font-size: .85rem;
}

/* Destructive actions read as destructive. Deep red on white is 6.1:1,
   so the label stays legible rather than relying on colour alone. */
.danger-button {
  min-height: 48px;
  padding: .75rem 1.15rem;
  border-radius: 12px;
  font-weight: 750;
  color: #ffffff;
  border: 1px solid #b3261e;
  background: #b3261e;
}

.danger-button:hover { filter: brightness(1.1); }

.danger-button:focus-visible,
.archived-list button:focus-visible {
  outline: 3px solid var(--orange-ink);
  outline-offset: 3px;
}

/* -------------------------------------------------------- language choice */

.language-choice { margin: 1.25rem 0 0; padding: 0; border: 0; }

.language-choice legend {
  padding: 0;
  margin-bottom: .55rem;
  font-size: .8rem;
  font-weight: 700;
}

#language-options { display: grid; gap: .5rem; }

.language-option {
  display: flex;
  gap: .7rem;
  align-items: flex-start;
  padding: .7rem .8rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  cursor: pointer;
}

.language-option:hover { border-color: var(--orange); }

/* Selection is shown by border AND a filled radio, never colour alone. */
.language-option:has(input:checked) {
  border-color: var(--orange);
  background: var(--orange-soft);
}

.language-option:has(input:focus-visible) {
  outline: 3px solid var(--orange-ink);
  outline-offset: 2px;
}

.language-option input { margin: .2rem 0 0; accent-color: var(--orange); }
.language-option strong { display: block; font-size: .92rem; }
.language-option span { color: var(--muted); font-size: .8rem; line-height: 1.4; }

/* ------------------------------------------------------------------ editor */

.editor-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.save-state { margin: 0; font-size: .82rem; color: var(--muted); }
.save-state[data-state="saving"] { color: var(--orange-ink); }
.save-state[data-state="saved"] { color: var(--orange-ink); }
.save-state[data-state="failed"] { color: #a8320c; font-weight: 700; }

.editor-shell {
  display: flex;
  margin-top: .5rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  overflow: hidden;
}

.editor-shell:focus-within {
  border-color: var(--orange);
  box-shadow: 0 0 0 3px #f15f2426;
}

.editor-gutter {
  padding: 1rem .6rem 1rem .9rem;
  border-right: 1px solid var(--line);
  color: var(--muted);
  text-align: right;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85rem;
  line-height: 1.6;
  white-space: pre;
  user-select: none;
}

.editor-area {
  flex: 1;
  min-height: 22rem;
  padding: 1rem;
  border: 0;
  resize: vertical;
  color: var(--ink);
  background: transparent;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85rem;
  line-height: 1.6;
  tab-size: 4;
  /* Long lines scroll rather than wrapping, so indentation stays readable. */
  white-space: pre;
  overflow-x: auto;
}

.editor-area:focus { outline: none; }

.server-notice {
  margin: 0;
  padding: .85rem clamp(1.25rem, 8vw, 8rem);
  color: #ffd0ba;
  background: var(--orange-soft);
  border-bottom: 1px solid var(--orange);
  font-size: .9rem;
  font-weight: 600;
}

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

.blockly-canvas {
  /* The canvas is the page. Sized against the viewport so it grows with the
     window instead of sitting in a fixed box with metadata around it. */
  height: clamp(24rem, 62vh, 48rem);
  margin-top: .5rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
}

.checks-panel {
  margin-top: 1.25rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
}

.checks-panel[data-state="ok"] { border-color: #2dd4a7; }
.checks-panel[data-state="error"] { border-color: #ff6b4a; }

.checks-panel h4 { margin: 0 0 .5rem; font-size: .95rem; }

.checks-list { list-style: none; margin: 0; padding: 0; }

.checks-list li {
  padding: .8rem 0;
  border-top: 1px solid var(--line);
}

.checks-list li:first-child { border-top: 0; }

.checks-list .what { display: block; font-weight: 700; }

/* The four parts FR-ENG-003 requires are shown as four labelled lines, so
   none can be quietly dropped by a future redesign. */
.checks-list dl { display: grid; grid-template-columns: auto 1fr; gap: .2rem .6rem; margin: .45rem 0 0; }
.checks-list dt { color: var(--muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; }
.checks-list dd { margin: 0; font-size: .88rem; line-height: 1.5; }
.checks-list .safety { color: #ffb08c; font-weight: 600; }
.checks-ok { margin: 0; color: #2dd4a7; font-weight: 600; }


/* ------------------------------------------------- V2-style block toolbox */

/* Class names verified against Blockly 10 in the browser. An earlier pass
   guessed .blocklyToolboxDiv and .blocklyTreeRow, neither of which exists
   in this version, so none of it applied. */

.blockly-canvas .blocklyToolbox {
  padding: .75rem .6rem;
  background: var(--toolbox);
  border-right: 1px solid var(--line);
}

/* V2 fills the whole pill with the category colour; Blockly's default draws
   a thin swatch beside the label. */
.blockly-canvas .blocklyToolboxCategory {
  display: flex;
  align-items: center;
  margin-bottom: .5rem;
  padding: .55rem .75rem !important;
  border-radius: 12px;
  background: var(--category-colour, #334155);
  cursor: pointer;
  /* Blockly fixes the row at 22px, which is less than the padding plus an
     18px label — the text was clipped to its top half. Let the content
     size the pill instead. */
  height: auto !important;
  min-height: 0;
  line-height: 1.25;
  display: flex;
  align-items: center;
}

.blockly-canvas .blocklyToolboxCategory:hover { filter: brightness(1.12); }

.blockly-canvas .blocklyToolboxCategorySelected {
  outline: 2px solid #ffffff88;
  outline-offset: -2px;
}

/* V2's category glyph, drawn from the icon the API supplies. */
.blockly-canvas .blocklyToolboxCategory::before {
  content: "";
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-right: .5rem;
  background-image: var(--category-icon);
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

/* A category with no icon should not leave a hole where the glyph goes. */
.blockly-canvas .blocklyToolboxCategory:not([style*="--category-icon"])::before {
  display: none;
}

/* Blockly's swatch element is redundant once the pill is filled. */
.blockly-canvas .blocklyTreeRowContentContainer > :not(.blocklyToolboxCategoryLabel) {
  display: none;
}

.blockly-canvas .blocklyToolboxCategoryLabel {
  color: #fff !important;
  font-weight: 750;
  font-size: .92rem;
}

.blockly-canvas .blocklyFlyoutBackground { fill: var(--flyout); fill-opacity: .97; }
.blockly-canvas .blocklyMainBackground { stroke: none; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}


/* --------------------------------------------------------- project details */

/* Target, mode, storage and revision history are reference, not the work.
   They sit behind a disclosure so the canvas and its checks own the page. */
.project-details {
  margin-top: 1.5rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
}

.project-details > summary {
  padding: .85rem 1.15rem;
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}

.project-details > summary::-webkit-details-marker { display: none; }

.project-details > summary::before {
  content: "▸";
  display: inline-block;
  width: 1.1em;
  color: var(--muted);
}

.project-details[open] > summary::before { content: "▾"; }

.project-details > summary:focus-visible {
  outline: 3px solid var(--orange-ink);
  outline-offset: -3px;
}

.project-details .workspace-facts { margin: 0 1.15rem 1.15rem; }
.project-details .workspace-subhead { margin: 0 1.15rem .25rem; }
.project-details .revision-list { margin: 0 1.15rem 1rem; }

/* The checks panel reads as the workspace's own status strip, like V2's
   terminal output docked under the canvas. */
.checks-panel { margin-top: 1rem; }

/* ------------------------------------------------- full-bleed workspace */

/* The Project Hub stays a centred reading column, but the workspace is an
   IDE: it should fill the window rather than float in a 1500px box with the
   canvas boxed inside it again. */
#workspace-view {
  max-width: none;
  margin: 0;
  padding: 1rem clamp(1rem, 2vw, 1.75rem) 0;
  /* Fill what is left under the 76px top bar, and lay the parts out so the
     canvas absorbs the slack instead of every section having a fixed size. */
  height: calc(100vh - 76px);
  display: flex;
  flex-direction: column;
  gap: .75rem;
}

#workspace-view .section-heading,
#workspace-view .editor-heading,
#workspace-view #content-path { flex: 0 0 auto; }

#workspace-view .section-heading { padding-bottom: .85rem; }
#workspace-view #content-path { margin: 0; }

#workspace-view .blockly-canvas {
  /* Overrides the clamp: inside the flex column the canvas takes whatever
     is left, so it grows with the window in both directions. */
  height: auto;
  flex: 1 1 auto;
  min-height: 12rem;
  margin-top: 0;
}

#workspace-view .editor-shell { flex: 1 1 auto; min-height: 12rem; }
#workspace-view .editor-area { min-height: 0; height: 100%; }

/* Docked under the canvas like V2's terminal: visible, bounded, scrolls
   rather than pushing the canvas out of view. */
#workspace-view .dock { flex: 0 0 auto; }

#workspace-view .project-details {
  flex: 0 0 auto;
  margin: 0 0 1rem;
  max-height: 40vh;
  overflow-y: auto;
}

/* ----------------------------------------------------- project in the bar */

/* The project's identity and its actions belong on the same line as the
   logo, as they do in V2 — a second full-width header underneath was a
   band of chrome above the canvas that said little. */
.project-bar {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex: 1 1 auto;
  min-width: 0;                 /* lets the title truncate, not overflow */
  margin-left: 1rem;
}

.project-bar-title {
  margin: 0;
  font-size: 1.05rem;
  letter-spacing: -.01em;
  /* A long project name shortens rather than pushing the actions off. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Actions sit hard right, against the global controls. */
.project-bar .hero-actions { margin-left: auto; flex-wrap: nowrap; gap: .45rem; }

.compact {
  min-height: 36px;
  padding: .4rem .7rem;
  border-radius: 9px;
  font-size: .85rem;
}

.topbar { gap: .75rem; }

/* Narrow windows: the actions keep working, the wordmark gives way first. */
@media (max-width: 1100px) {
  .brand small, .brand-divider { display: none; }
}

@media (max-width: 860px) {
  .project-bar .hero-actions { flex-wrap: wrap; justify-content: flex-end; }
  .local-badge { display: none; }
}

/* The save state sits with the project name — it is about the project, and
   a standalone "Current program / Saved locally" header over the canvas was
   a row of chrome restating what the bar already says. */
.project-bar .save-state { margin: 0 0 0 .3rem; font-size: .8rem; white-space: nowrap; }

/* Program checks reads as a status strip under the canvas, not a panel. */
#workspace-view .checks-list li { padding: .45rem 0; }

/* Blocks / Code toggle — V2 puts this in the top bar beside the project. */
.view-toggle { display: inline-flex; margin-left: auto; padding: 2px;
  border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); }
.view-tab { padding: .3rem .85rem; border: 0; border-radius: 6px; cursor: pointer;
  background: none; color: var(--muted); font: inherit; font-size: .82rem;
  font-weight: 600; }
.view-tab:hover { color: var(--text); }
.view-tab.is-active { background: var(--surface); color: var(--text);
  box-shadow: 0 1px 2px rgb(0 0 0 / .12); }
.view-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The bar's action group keeps its own spacing now that the toggle claimed
   the auto margin. */
.project-bar .hero-actions { margin-left: .75rem; }

.code-view { display: grid; place-items: center; min-height: 0; padding: 2rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 10px; }
.code-view-inner { max-width: 46ch; text-align: center; }
.code-view-inner h3 { margin-bottom: .6rem; font-size: 1rem; }
.code-view-inner p { color: var(--muted); font-size: .88rem; line-height: 1.55; }
.code-view-inner p + p { margin-top: .75rem; }

/* `hidden` is only a `display: none` UA rule, so any component that sets its
   own display wins over it. `.editor-shell` (flex) and `.code-view` (grid)
   both did: they stayed laid out while hidden and ate the flex column, which
   is why the canvas never grew past its 12rem minimum. */
[hidden] { display: none !important; }

/* In the workspace column the code pane fills the same space the canvas does. */
#workspace-view .code-view { flex: 1 1 auto; min-height: 12rem; }

/* Everything in the bar is a fixed-size control except the title, so the
   title is what should absorb the slack — not be crushed to one letter by
   buttons that wrapped their own labels instead of staying on one line. */
.project-bar > button,
.project-bar .hero-actions,
.project-bar .view-toggle { flex: 0 0 auto; }
.project-bar button { white-space: nowrap; }
.project-bar-title { flex: 1 1 auto; min-width: 5rem; }

/* Seven controls, a title and a logo share one row, so the row's own
   controls have to be tight. Measured at 1280px the untightened bar
   overflowed by ~60px and the export button slid under the theme toggle. */
.project-bar .secondary-button.compact,
.project-bar .primary-button.compact { padding: .34rem .7rem; font-size: .8rem; }
.project-bar .hero-actions { gap: .35rem; }
/* The title is the only elastic thing here: it shortens with an ellipsis so
   that no button is ever pushed off the edge. */
.project-bar-title { min-width: 0; }

/* ------------------------------------------------------ project settings menu */

.menu { position: relative; flex: 0 0 auto; }

.menu-panel {
  position: absolute;
  top: calc(100% + .4rem);
  right: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  min-width: 12rem;
  padding: .3rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  box-shadow: 0 12px 30px rgb(0 0 0 / .18);
}

.menu-item {
  padding: .5rem .65rem;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: .86rem;
  text-align: left;
  cursor: pointer;
}

.menu-item:hover { background: var(--orange-soft); color: var(--orange-ink); }
.menu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Blockly injects positioned layers into the canvas, which paint over a
   dropdown that hangs out of the header. The header owns the menu, so the
   header has to sit above the workspace. */
header { position: relative; z-index: 50; }

/* ------------------------------------------------------------- home screen */

.home-nav {
  display: flex;
  gap: .35rem;
  justify-content: center;
  padding: .75rem 1rem;
  border-bottom: 1px solid var(--line);
}

.home-tab {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .5rem .9rem;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: .82rem;
  font-weight: 650;
  letter-spacing: .04em;
  text-transform: uppercase;
  cursor: pointer;
}

.home-tab svg {
  width: 20px; height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.home-tab:hover { color: var(--text); }
.home-tab.is-active { color: var(--text); }
.home-tab.is-active svg { stroke: var(--orange); }
.home-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.home-panel { max-width: 1100px; margin: 0 auto; padding: 2rem clamp(1rem, 4vw, 3rem) 5rem; }

/* The hero: what to do first, and nothing else competing with it. */
.start-here {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 360px);
  align-items: center;
  gap: 2rem;
  padding: clamp(1.5rem, 4vw, 3rem);
  border-radius: 20px;
  background: var(--orange-soft);
}

.start-here h1 { font-size: clamp(2.4rem, 6vw, 4rem); letter-spacing: -.045em; line-height: 1; }
.start-here-copy p { max-width: 34ch; margin: 1.25rem 0 2rem; color: var(--muted); line-height: 1.55; }
.start-here-art { justify-self: end; width: 100%; max-width: 320px; height: auto; }

.start-button {
  padding: .8rem 2.75rem;
  border: 2px solid var(--orange);
  border-radius: 999px;
  background: transparent;
  color: var(--orange-ink);
  font: inherit;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .18s, color .18s;
}

.start-button:hover { background: var(--orange); color: #fff; }
.start-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.strip-title { margin: 2.5rem 0 1rem; font-size: 1.35rem; letter-spacing: -.02em; }

/* A row, not a grid: recent work is a short list you scan, not a library. */
.recent-strip {
  display: flex;
  gap: 1rem;
  padding-bottom: .5rem;
  overflow-x: auto;
}

/* The strip holds the same cards as the hub grid, laid on their side. */
/* 230px so the new tile plus four projects clear the panel's width at a
   1280px window — a card sliced by the container edge reads as broken
   rather than as "scroll for more". */
.recent-strip > .project-card,
.recent-new { flex: 0 0 230px; }

.recent-strip .project-visual { height: 120px; font-size: 2.6rem; }

/* The "new" tile matches the cards' silhouette so the row reads as one set. */
.recent-new {
  display: grid;
  place-content: center;
  gap: .5rem;
  justify-items: center;
  min-height: 240px;
  padding: 1rem;
  border: 1px dashed var(--line);
  border-radius: 18px;
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  transition: border-color .2s, color .2s, transform .2s;
}

.recent-new:hover {
  border-color: var(--orange);
  color: var(--orange-ink);
  transform: translateY(-3px);
}

.recent-new:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.recent-plus { font-size: 2.4rem; font-weight: 300; line-height: 1; }
.recent-new-label { font-size: .9rem; font-weight: 650; }

.home-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  gap: 1.25rem;
  margin-top: 3rem;
}

.home-card {
  padding: 1.75rem;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color .2s, transform .2s;
}

.home-card:hover { border-color: var(--orange); transform: translateY(-2px); }
.home-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.home-card h3 { margin-bottom: .5rem; font-size: 1.35rem; letter-spacing: -.02em; }
.home-card p { color: var(--muted); font-size: .9rem; line-height: 1.5; }

.robot-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 1.25rem;
  margin-top: 1.5rem;
}

.robot-card { padding: 1.5rem; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); }
.robot-card h3 { margin-bottom: .4rem; font-size: 1.15rem; }
.robot-card p { color: var(--muted); font-size: .88rem; line-height: 1.5; }
.robot-status { margin-top: .9rem; font-size: .78rem; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; }
.robot-status[data-shipping="true"] { color: var(--orange-ink); }

@media (max-width: 720px) {
  .start-here { grid-template-columns: 1fr; }
  .start-here-art { display: none; }
  .home-nav { overflow-x: auto; justify-content: flex-start; }
}

/* Hero art: block silhouettes, drawn rather than photographed. */
.start-here-art { width: 100%; max-width: 300px; height: auto; }
.start-here-art .art-block { filter: drop-shadow(0 4px 10px rgb(0 0 0 / .12)); }


/* ------------------------------------------------------------------- dock */

.dock {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  min-height: 0;
  max-height: 38vh;
  margin-top: 0;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  overflow: hidden;
}

.dock-tabs {
  display: flex;
  flex: 0 0 auto;
  gap: .25rem;
  padding: .4rem .4rem 0;
  border-bottom: 1px solid var(--line);
}

.dock-tab {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .45rem .85rem;
  border: 0;
  border-radius: 8px 8px 0 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: .82rem;
  font-weight: 650;
  cursor: pointer;
}

.dock-tab:hover { color: var(--text); }
.dock-tab.is-active { background: var(--surface); color: var(--text); }
.dock-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.dock-badge {
  min-width: 1.25rem;
  padding: .05rem .35rem;
  border-radius: 999px;
  background: var(--muted);
  color: var(--panel);
  font-size: .7rem;
  text-align: center;
}

.dock-badge[data-state="error"] { background: #ff6b4a; color: #2a0d06; }
.dock-badge[data-state="warning"] { background: #f5a623; color: #2a1a00; }

.dock-pane { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: .75rem .9rem; }
.dock-pane.checks-panel { border: 0; border-radius: 0; background: none; }

/* ---- serial monitor */

#pane-serial { display: flex; flex-direction: column; gap: .5rem; overflow: hidden; }

/* Superseded by the compact one-line bar further down; kept only so the
   selector order stays readable. */
.serial-bar { display: flex; align-items: center; gap: .6rem; }
.serial-baud-label, .serial-check { color: var(--muted); font-size: .8rem; }
.serial-check { display: inline-flex; align-items: center; gap: .3rem; }
#serial-baud { padding: .3rem .4rem; border: 1px solid var(--line); border-radius: 7px;
  background: var(--surface); color: var(--text); font: inherit; font-size: .8rem; }

.serial-status { margin-left: auto; color: var(--muted); font-size: .8rem; }
.serial-status[data-kind="ok"] { color: var(--orange-ink); }
.serial-status[data-kind="error"] { color: #ff6b4a; }

/* The log is the point of the pane, so it takes the slack. */
.serial-log {
  flex: 1 1 auto;
  min-height: 6rem;
  padding: .6rem .7rem;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--canvas);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82rem;
  line-height: 1.5;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.serial-log:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Sent lines are marked, so a session reads as a conversation rather than
   one undifferentiated stream. */
.serial-out { color: var(--orange-ink); }
.serial-out::before { content: "› "; opacity: .7; }

.serial-send { display: flex; flex: 0 0 auto; gap: .5rem; }
#serial-input {
  flex: 1 1 auto;
  padding: .4rem .6rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82rem;
}
#serial-input:disabled { opacity: .55; }
#serial-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ------------------------------------------- rename and new, in the bar */

.project-bar-title { cursor: text; border-radius: 6px; padding: .1rem .3rem; margin-left: -.3rem; }
.project-bar-title:hover { background: var(--orange-soft); }
.project-bar-title:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Sized like the heading it replaces, so the bar does not jump on edit. */
.project-bar-title-input {
  flex: 0 1 16rem;
  min-width: 6rem;
  padding: .1rem .3rem;
  border: 1px solid var(--orange);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -.01em;
}

.project-bar-title-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.icon-button {
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: none;
  color: var(--muted);
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color .18s, color .18s;
}

.icon-button:hover { border-color: var(--orange); color: var(--orange-ink); }
.icon-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* --------------------------------------------------- dock size and place */

/* Half the width, centred under the canvas. The monitor is a companion to
   the work, not the work itself, and a full-width strip made it look like
   the main event. */
#workspace-view .dock {
  width: min(50%, 900px);
  margin: 0 auto;
}

/* Below that there is no room for two columns of anything, so the dock
   takes the width it needs rather than being squeezed to half of little. */
@media (max-width: 900px) {
  #workspace-view .dock { width: 100%; }
}

/* ------------------------------------------------------------ action bar */

/* The workspace is the positioning context for both the bar and the dock. */
#workspace-view { position: relative; }

.action-bar {
  position: absolute;
  right: clamp(1rem, 2vw, 2rem);
  bottom: 1.25rem;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: .6rem;
}

.action-group {
  display: flex;
  align-items: center;
  gap: .3rem;
  padding: .35rem;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 8px 24px rgb(0 0 0 / .14);
}

.action-button {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid transparent;
  border-radius: 11px;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: background .18s, color .18s, border-color .18s;
}

.action-button svg {
  width: 20px; height: 20px;
  fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}

/* The play triangle and the stop square are filled shapes, not outlines. */
.action-button.run svg, .action-button.stop svg { fill: currentColor; stroke: none; }

.action-button:hover:not(:disabled) { background: var(--orange-soft); color: var(--orange-ink); }
.action-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Disabled here means "not built yet", and the title says which. Showing it
   greyed rather than hiding it keeps the shape of the finished bar visible
   without pretending the button works. */
.action-button:disabled { opacity: .38; cursor: not-allowed; }

.action-button[aria-pressed="true"] {
  background: var(--orange-soft);
  color: var(--orange-ink);
  border-color: var(--orange);
}

.connect-wrap { position: relative; }

.action-button.connect {
  width: 46px;
  height: 46px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 8px 24px rgb(0 0 0 / .14);
}

.action-button.connect[data-connected="true"] {
  border-color: #2dd4a7;
  color: #17a97f;
}

/* A dot on the bolt, so a live connection is visible without opening
   anything and without the monitor being on screen. */
.connect-dot {
  position: absolute;
  top: 4px; right: 4px;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: transparent;
}

.action-button.connect[data-connected="true"] .connect-dot { background: #2dd4a7; }

.connect-popover {
  position: absolute;
  right: 0;
  bottom: calc(100% + .5rem);
  z-index: 40;
  width: 17rem;
  padding: .35rem;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 14px 34px rgb(0 0 0 / .2);
}

.connect-heading {
  padding: .5rem .6rem .3rem;
  color: var(--muted);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
}

.connect-row {
  display: flex;
  align-items: center;
  gap: .6rem;
  width: 100%;
  padding: .55rem .6rem;
  border: 0;
  border-radius: 9px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: .88rem;
  text-align: left;
  cursor: pointer;
}

.connect-row:hover:not(:disabled) { background: var(--orange-soft); }
.connect-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.connect-row:disabled { opacity: .45; cursor: not-allowed; }
.connect-row.danger { color: #d9483b; }
.connect-icon { width: 1rem; text-align: center; color: var(--orange); }
.connect-name { flex: 1 1 auto; }
.connect-live { width: 9px; height: 9px; border-radius: 50%; background: #2dd4a7; }
.connect-note { padding: .35rem .6rem .6rem; color: var(--muted); font-size: .82rem; }

/* ------------------------------------------------- the dock now floats */

/* Overlaying the canvas rather than sitting in the flex column: showing the
   monitor must not resize the editor underneath it. */
#workspace-view .dock {
  position: absolute;
  left: 50%;
  bottom: 5.5rem;
  z-index: 25;
  width: min(50%, 900px);
  max-height: 38vh;
  margin: 0;
  transform: translateX(-50%);
  box-shadow: 0 18px 44px rgb(0 0 0 / .22);
}

.dock-close {
  order: 1;
  margin-left: auto;
  padding: 0 .5rem;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.dock-close:hover { color: var(--text); }
.dock-close:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

@media (max-width: 900px) {
  #workspace-view .dock { width: calc(100% - 2rem); }
}

/* ------------------------------------------------ compact monitor controls */

/* Everything on the tab row: tabs left, controls right, one line. */
.dock-tabs { align-items: center; padding: .35rem .45rem; border-bottom: 1px solid var(--line); }
.dock-tab { border-radius: 8px; }

.serial-bar {
  display: flex;
  flex: 1 1 auto;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: flex-end;
  gap: .4rem;
  min-width: 0;
  margin-left: .5rem;
}

.serial-connect {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .3rem .7rem;
  border: 1px solid var(--orange);
  border-radius: 999px;
  background: var(--orange);
  color: #fff;
  font: inherit;
  font-size: .78rem;
  font-weight: 700;
  cursor: pointer;
}

.serial-connect svg { width: 13px; height: 13px; fill: currentColor; }
.serial-connect:hover { filter: brightness(1.06); }
.serial-connect:disabled { opacity: .5; cursor: not-allowed; }
.serial-connect:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Connected: the button becomes the way out, so it stops shouting. */
.dock[data-connected="true"] .serial-connect {
  background: none;
  border-color: var(--line);
  color: var(--muted);
}

#serial-baud {
  padding: .25rem .3rem;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface);
  color: var(--muted);
  font: inherit;
  font-size: .75rem;
}

/* The status is the elastic part: it truncates rather than pushing the
   controls off the row. */
.serial-status {
  flex: 0 1 auto;
  min-width: 0;
  margin: 0 .2rem 0 auto;
  color: var(--muted);
  font-size: .75rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.serial-toggle, .serial-icon {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border: 1px solid transparent;
  border-radius: 7px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.serial-toggle svg, .serial-icon svg {
  width: 15px; height: 15px;
  fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}

.serial-toggle input { position: absolute; opacity: 0; pointer-events: none; }
.serial-toggle:hover, .serial-icon:hover { background: var(--orange-soft); color: var(--orange-ink); }
.serial-toggle:has(input:checked) { background: var(--orange-soft); color: var(--orange-ink); }
.serial-toggle:has(input:focus-visible), .serial-icon:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}

.dock-close { order: 0; margin-left: .2rem; }

/* ------------------------------------------------------ dock at the bottom */

/* Flush with the bottom of the workspace. Floating it clear of the edge made
   it look detached, like a dialog someone forgot to close. */
#workspace-view .dock {
  bottom: 0;
  border-bottom: 0;
  border-radius: 14px 14px 0 0;
}

/* ----------------------------------------------------------- project tabs */

.project-tabs {
  display: flex;
  align-items: flex-end;
  gap: .25rem;
  padding: 0 clamp(1rem, 2vw, 1.75rem);
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  overflow-x: auto;
}

.project-tab {
  display: inline-flex;
  align-items: center;
  gap: .1rem;
  padding: 0 .2rem 0 .1rem;
  border: 1px solid transparent;
  border-bottom: 0;
  border-radius: 9px 9px 0 0;
  color: var(--muted);
}

/* The active tab joins the workspace below it, so the strip reads as a stack
   of documents rather than a row of buttons. */
.project-tab.is-active {
  border-color: var(--line);
  background: var(--canvas);
  color: var(--text);
  margin-bottom: -1px;
  padding-bottom: 1px;
}

.project-tab-open {
  max-width: 14rem;
  padding: .5rem .5rem .5rem .7rem;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: .82rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}

.project-tab-close {
  padding: .1rem .35rem;
  border: 0;
  border-radius: 5px;
  background: none;
  color: inherit;
  font-size: .95rem;
  line-height: 1;
  opacity: .5;
  cursor: pointer;
}

.project-tab-close:hover { background: var(--orange-soft); color: var(--orange-ink); opacity: 1; }
.project-tab-open:focus-visible, .project-tab-close:focus-visible,
.project-tab-add:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.project-tab-add {
  align-self: center;
  padding: .15rem .5rem;
  margin-left: .25rem;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--muted);
  font-size: 1.15rem;
  line-height: 1.4;
  cursor: pointer;
}

.project-tab-add:hover { background: var(--orange-soft); color: var(--orange-ink); }

/* The tab strip eats a slice of the window, so the workspace keeps the rest. */
#workspace-view { height: calc(100vh - 76px - 41px); }

/* At half the width the tab row has to hold two tabs plus the controls. The
   tabs must keep their own line — "Serial monitor" wrapping to two lines made
   the dock taller than the log inside it. */
.dock-tab { flex: 0 0 auto; white-space: nowrap; }
.serial-bar > * { flex: 0 0 auto; }

/* The status is the only elastic part, and it disappears rather than forcing
   a wrap: the same words are in the log and on the Connect button. */
.serial-status { flex: 0 1 auto; min-width: 0; }
@media (max-width: 1250px) { .serial-status { display: none; } }

/* The dock sized itself to its contents, which meant the log sat at its
   minimum and the chrome took more than half the panel. Giving the dock a
   height lets the log have whatever the tab row and the send box do not. */
#workspace-view .dock { height: min(34vh, 300px); }
.serial-log { min-height: 0; }

/* A blocked action stays clickable so it can say why. `disabled` would make
   it unfocusable and silent, which is how a grey Run button with a robot
   plugged in becomes a mystery instead of a message. */
.action-button[aria-disabled="true"] { opacity: .38; }
.action-button[aria-disabled="true"]:hover { background: none; color: var(--muted); }
.action-button[aria-disabled="true"]:active { transform: scale(.96); }

/* Generated code, and what could not be generated. */
.code-view { align-items: stretch; justify-content: flex-start; padding: 0; }
.generated-code {
  flex: 1 1 auto;
  min-height: 0;
  margin: 0;
  padding: 1rem 1.1rem;
  overflow: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85rem;
  line-height: 1.6;
  white-space: pre;
}

.code-gaps {
  flex: 0 0 auto;
  max-height: 30%;
  padding: .75rem 1.1rem;
  border-top: 1px solid var(--line);
  overflow-y: auto;
}

.code-gaps h4 { margin-bottom: .4rem; font-size: .78rem; letter-spacing: .06em;
  text-transform: uppercase; color: #d9483b; }
.code-gaps li { margin-left: 1.1rem; color: var(--muted); font-size: .85rem;
  line-height: 1.5; }

/* Handing authority from the blocks to the code (FR-ENG-006). */
.code-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: .9rem;
  padding: .7rem 1.1rem;
  border-top: 1px solid var(--line);
}

.code-actions[hidden] { display: none; }
.code-actions-note { margin: 0; color: var(--muted); font-size: .8rem;
  line-height: 1.45; }

/* Firmware, in the Connect menu because it needs a connected robot. */
.connect-note {
  margin: .1rem .55rem .45rem;
  color: var(--muted);
  font-size: .78rem;
  line-height: 1.45;
}

/* The simulator pane: controls above, canvas filling what is left. */
.sim-panel { display: flex; flex-direction: column; min-height: 0; }
.sim-panel[hidden] { display: none; }

.sim-controls {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .55rem .8rem;
  border-bottom: 1px solid var(--line);
}

.sim-readout {
  margin: 0 0 0 .3rem;
  color: var(--muted);
  font-size: .8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sim-canvas { flex: 1 1 auto; width: 100%; min-height: 0; display: block; }

/* Firmware install: a window, because it rewrites the robot's own software. */
/* Every other dialog wraps its content in a form, and `dialog form` is
   where the padding lives. This one has no form — nothing here is being
   submitted — so its content sat flush against the border and the first
   letter of every line was clipped by the rounded corner. The padding
   belongs on whatever holds the content, not on the form specifically. */
#firmware-dialog { width: min(92vw, 30rem); padding: 2rem; }
#firmware-dialog h2 {
  /* The shared `dialog h2` margin is sized for a form with fields under
     it. Here the next line is a sentence about the same thing, so it wants
     to sit closer. */
  margin-bottom: .6rem;
  line-height: 1.15;
}
#firmware-step { margin: 0 0 1.25rem; color: var(--muted); font-size: .95rem; }
/* The bar is the only thing on screen that moves during a job that rewrites
   the robot's software, so it is worth more than a hairline. */
#firmware-dialog .progress-track { height: .65rem; }
#firmware-warning { margin-top: 1.1rem; }
#firmware-dialog .dialog-actions { margin-top: 1.5rem; }

/* Done and failed are the two states somebody reads from across a room. */
#firmware-dialog[data-outcome="done"] h2 { color: var(--ok, #16794f); }
#firmware-dialog[data-outcome="failed"] h2 { color: var(--danger, #b4402a); }

.progress-track {
  height: .5rem;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: var(--accent);
  transition: width .2s ease;
}

/* Indeterminate: the robot is working and we cannot see how far it is. */
.progress-track[data-state="waiting"] .progress-fill {
  width: 35%;
  animation: progress-sweep 1.1s ease-in-out infinite;
}

@keyframes progress-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(300%); }
}

.progress-track[data-state="failed"] .progress-fill { background: #d9483b; }

/* The simulator, as a view of the project rather than a drawer. */
.simulate-view {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 17rem;
  min-height: 0;
  height: 100%;
}

.simulate-view[hidden] { display: none; }

.sim-stage {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  /* The rail is positioned against this. */
  position: relative;
}

/*
 * View controls, over the canvas rather than under it.
 *
 * Zoom, framing and flat-or-3D are about looking at the simulation, not
 * about running it, and as toolbar entries they crowded out the controls
 * that are — the row had wrapped onto two lines. A rail costs no width at
 * all: it sits on the picture, where the thing it acts on is.
 */
.sim-rail {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.rail-button {
  width: 38px;
  height: 38px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(32, 30, 28, 0.92);
  color: #f2ece4;
  cursor: pointer;
  /* Over a 3D scene, so it needs to lift off it rather than blend in. */
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  transition: background 0.15s ease, transform 0.15s ease;
}

.rail-button svg {
  width: 19px;
  height: 19px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.rail-button:hover { background: rgba(46, 43, 40, 0.96); transform: translateY(-1px); }
.rail-button:active { transform: translateY(0); }

/* A toggle that is on reads as pressed, not merely hovered. */
.rail-button.is-on {
  background: var(--accent, #e2571f);
  border-color: var(--accent, #e2571f);
  color: #fff;
}

/*
 * The app carries its own theme on the root, and the OS preference is not
 * it — the two disagree the moment somebody uses the app's toggle. Keying
 * off `prefers-color-scheme` gave a dark rail on a light page, which is how
 * this was found.
 */
:root[data-theme="light"] .rail-button {
  background: rgba(255, 255, 255, 0.94);
  border-color: rgba(0, 0, 0, 0.1);
  color: #231f20;
}
:root[data-theme="light"] .rail-button:hover { background: #fff; }

:root[data-theme="light"] .rail-button.is-on {
  background: var(--accent, #e2571f);
  border-color: var(--accent, #e2571f);
  color: #fff;
}

.sim-canvas { flex: 1 1 auto; width: 100%; min-height: 0; display: block; }

.sim-controls {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .7rem .9rem;
  border-top: 1px solid var(--line);
}

.sim-readout {
  margin: 0 0 0 .3rem;
  color: var(--muted);
  font-size: .8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* What the robot is doing, beside what it is doing it in. */
.sim-devices {
  border-left: 1px solid var(--line);
  padding: 1rem .95rem;
  overflow-y: auto;
  background: var(--surface);
}

.sim-devices h3 {
  margin: 0 0 .5rem;
  font-size: .72rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}

.sim-devices h3 + .device-list { margin-bottom: 1.2rem; }

.device-list { margin: 0; }
.device {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .6rem;
  padding: .28rem 0;
}
.device dt { color: var(--muted); font-size: .82rem; }
.device dd {
  margin: 0;
  font-size: .82rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
  display: flex;
  align-items: center;
  gap: .4rem;
}

.device-swatch {
  width: .8rem;
  height: .8rem;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: #2a2a2a;
  display: inline-block;
}

/* Five little lamps: dark when the sensor is off the line, lit when on it. */
.line-array { display: inline-flex; gap: .2rem; }
.line-cell {
  width: .55rem;
  height: .9rem;
  border-radius: 2px;
  background: var(--line);
}
.line-cell[data-on="true"] { background: var(--accent); }

.sim-log {
  margin: 0;
  font-size: .78rem;
  line-height: 1.5;
  color: var(--muted);
  list-style: none;
}
.sim-log li { padding: .12rem 0; border-bottom: 1px solid var(--line); }
.sim-log li:last-child { border-bottom: 0; }

@media (max-width: 900px) {
  .simulate-view { grid-template-columns: minmax(0, 1fr); }
  .sim-devices { border-left: 0; border-top: 1px solid var(--line); max-height: 40%; }
}

/* Two ways of watching the same run. */
.sim-dimension { margin-left: auto; }
.sim-dimension .view-tab { font-size: .74rem; padding: .3rem .6rem; }
.sim-canvas[hidden] { display: none; }

/* Simulator controls: wheels, pen, camera. */
.sim-field select {
  font: inherit;
  font-size: .78rem;
  padding: .28rem .45rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
}

.sim-check {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  font-size: .78rem;
  color: var(--muted);
  cursor: pointer;
}

.sim-controls { flex-wrap: wrap; }
.sim-controls .sim-readout { flex: 1 1 6rem; }

/* The challenge: what to do, and whether it was done. */
.challenge { margin-bottom: 1.2rem; }
.challenge-task { margin: 0 0 .4rem; font-size: .82rem; line-height: 1.5; }
.challenge-verdict {
  margin: 0;
  font-size: .82rem;
  line-height: 1.5;
  padding: .45rem .6rem;
  border-radius: 8px;
  background: var(--line);
  color: var(--muted);
}
.challenge-verdict[data-passed="true"] { background: #dff2e3; color: #1d6b34; }
.challenge-verdict[data-passed="false"] { background: #fbe3e0; color: #a2352a; }
.challenge-verdict:empty { display: none; }

/* The robot's own buttons, pressable while a program runs. */
.device-hint { margin: -.2rem 0 .5rem; color: var(--muted); font-size: .74rem; }
.button-row { display: flex; gap: .35rem; margin-bottom: 1.2rem; }

.robot-button {
  flex: 1 1 0;
  padding: .4rem .2rem;
  font: inherit;
  font-size: .78rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  user-select: none;
}

.robot-button[data-down="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}


/*
 * The measuring readout.
 *
 * Over the canvas beside the rail, because it belongs to the thing being
 * measured rather than to the controls underneath — and because a number
 * that appears where you are looking is one you do not have to hunt for.
 */
.sim-measure {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 8px 7px 12px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(32, 30, 28, 0.92);
  color: #f2ece4;
  font: 600 13px/1 system-ui, sans-serif;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}

.measure-unit {
  padding: 4px 9px;
  border-radius: 999px;
  border: 0;
  background: var(--accent, #e2571f);
  color: #fff;
  font: 600 12px/1 system-ui, sans-serif;
  cursor: pointer;
}

:root[data-theme="light"] .sim-measure {
  background: rgba(255, 255, 255, 0.94);
  border-color: rgba(0, 0, 0, 0.1);
  color: #231f20;
}

/* ------------------------------------------------------------- AI Lab
   A step bar across the top, then one step's worth of screen. Collect is
   the lens on the left and what it is filling on the right, so the count
   rises where the learner is already looking. */
.ai-view { display: block; height: 100%; overflow: auto; }
.ai-lab-shell { display: flex; flex-direction: column; min-height: 100%; }

.ai-topbar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 10px 16px;
  border-bottom: 1px solid var(--line); background: var(--panel);
}
.ai-brand { display: flex; align-items: center; gap: 8px; }
.ai-brand-back {
  width: 26px; height: 26px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  font: inherit; line-height: 1;
}
.ai-brand-back:hover { border-color: var(--orange); }
.ai-brand-name { font-weight: 700; font-size: 14px; }
.ai-brand-badge {
  font-size: 10px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
  background: var(--orange-soft); color: var(--orange-ink);
  border: 1px solid var(--orange);
}

.ai-steps {
  display: flex; gap: 4px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 999px; padding: 4px;
}
.ai-step-pill {
  display: flex; align-items: center; gap: 6px;
  border: 0; background: transparent; color: var(--muted);
  border-radius: 999px; padding: 6px 14px; cursor: pointer;
  font: inherit; font-size: 12.5px; font-weight: 600; white-space: nowrap;
}
.ai-step-pill:hover:not(:disabled) { color: var(--ink); }
.ai-step-pill.is-active { background: var(--orange); color: #000; }
.ai-step-pill:disabled { opacity: .4; cursor: not-allowed; }
.ai-step-number { opacity: .7; }

.ai-topbar-right { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.ai-modes {
  display: flex; gap: 2px; padding: 3px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px;
}
.ai-mode {
  border: 0; background: transparent; color: var(--muted); cursor: pointer;
  border-radius: 999px; padding: 5px 12px; font: inherit; font-size: 12px; font-weight: 600;
}
.ai-mode.is-active { background: var(--orange); color: #000; }
.ai-robot-status {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--muted);
  padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
}
.ai-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
.ai-robot-status.is-on { color: #2f9e6f; border-color: #2f9e6f; }
.ai-robot-status.is-on .ai-dot { background: #2f9e6f; }
.ai-send {
  border: 0; background: var(--orange); color: #000; cursor: pointer;
  border-radius: 999px; padding: 7px 16px; font: inherit; font-size: 12.5px; font-weight: 700;
}
.ai-send:disabled { opacity: .4; cursor: not-allowed; }

.ai-main { padding: 22px 20px 30px; display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.ai-banner { text-align: center; display: flex; flex-direction: column; gap: 4px; align-items: center; }
.ai-banner-step {
  margin: 0; font-size: 11px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--orange-ink);
  background: var(--orange-soft); border: 1px solid var(--orange);
  padding: 4px 12px; border-radius: 999px;
}
.ai-banner-title { margin: 4px 0 0; font-size: 25px; }
.ai-banner-hint { margin: 0; font-size: 13px; color: var(--muted); }
.ai-stage-area { display: flex; flex-direction: column; gap: 16px; align-items: center; }

/* Collect: lens left, categories right. */
.ai-collect {
  display: grid; gap: 16px; width: 100%; max-width: 62rem;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: start;
}
@media (max-width: 880px) { .ai-collect { grid-template-columns: 1fr; } }
.ai-camera-side { display: flex; flex-direction: column; gap: 10px; }
/* Landscape, always. A camera gives a wide picture; showing it in a tall
   box either crops the sides off or pillarboxes it, and the sides are
   where a held-up hand or card usually is. */
.ai-camera-stage {
  aspect-ratio: 16 / 10; border-radius: 14px; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line);
  display: grid; place-items: center; width: 100%;
}
.ai-camera-stage { position: relative; }
.ai-camera-stage video, .ai-camera-stage img { width: 100%; height: 100%; object-fit: cover; }
/* Over the picture rather than beside it, so the learner reads which
   category they are filling without looking away from the lens. */
.ai-camera-tag {
  position: absolute; left: 10px; bottom: 10px; margin: 0;
  padding: 6px 12px; border-radius: 999px;
  background: rgba(0, 0, 0, .62); color: #fff;
  font-size: 12px; font-weight: 600;
}
.ai-camera-idle { text-align: center; padding: 20px; display: grid; gap: 6px; justify-items: center; }
.ai-camera-mark {
  font-size: 30px; color: var(--orange);
  border: 2px dashed var(--orange); border-radius: 12px;
  width: 84px; height: 66px; display: grid; place-items: center;
}
.ai-camera-target { margin: 8px 0 0; font-weight: 700; font-size: 14px; }
.ai-snap {
  border: 0; background: var(--orange); color: #000; cursor: pointer;
  border-radius: 12px; padding: 15px; font: inherit; font-size: 15px; font-weight: 700;
}
.ai-snap.is-live { background: #c0392b; color: #fff; }
.ai-upload-wide {
  text-align: center; font-size: 12px; color: var(--muted); cursor: pointer;
  padding: 8px; border: 1px dashed var(--line); border-radius: 10px;
}
.ai-upload-wide:hover { border-color: var(--orange); color: var(--ink); }
.ai-upload-wide input { display: none; }

.ai-categories-side { display: flex; flex-direction: column; gap: 12px; }
.ai-card-head { display: flex; align-items: center; justify-content: space-between; }
.ai-card-head h4 { margin: 0; font-size: 13px; }
.ai-pill {
  font-size: 11px; padding: 3px 10px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--line); color: var(--muted);
}
.ai-adder { display: flex; gap: 6px; }
.ai-adder input {
  flex: 1; min-width: 0; background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 12px;
}
.ai-adder input:focus { border-color: var(--orange); outline: none; }

.ai-cat {
  display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--line); border-radius: 10px;
  padding: 8px 10px; background: var(--surface);
}
.ai-cat.is-active { border-color: var(--orange); }
.ai-cat-pick {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 9px;
  background: transparent; border: 0; cursor: pointer; color: var(--ink);
  font: inherit; text-align: left; padding: 0;
}
.ai-cat-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  background: var(--class-colour, var(--orange));
}
.ai-cat-text { display: flex; flex-direction: column; min-width: 0; }
/* A field, not a label: renaming is the first thing anyone wants to do,
   and "Class 1" tells nobody what it is a picture of. It only looks like
   an input once you reach for it, so the row still reads as a list. */
.ai-cat-name {
  font-size: 13px; font-weight: 600; font-family: inherit;
  color: var(--ink); background: transparent;
  border: 1px solid transparent; border-radius: 6px;
  padding: 2px 5px; margin: -2px -5px; width: 100%; min-width: 0;
}
.ai-cat-name:hover { border-color: var(--line); background: var(--panel); }
.ai-cat-name:focus { border-color: var(--orange); background: var(--panel); outline: none; }
.ai-cat-count { font-size: 11px; color: var(--muted); }
.ai-cat-state {
  font-size: 10px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
  background: var(--panel); border: 1px solid var(--line); color: var(--muted);
}
.ai-cat-state.is-ready { color: #2f9e6f; border-color: #2f9e6f; }

.ai-panel { display: flex; flex-direction: column; gap: 12px; width: 100%; max-width: 42rem; }
.ai-step-footer { display: flex; gap: 10px; width: 100%; max-width: 62rem; }
.ai-step-footer .primary-button { margin-left: auto; }
.ai-step-footer button:disabled { opacity: .45; cursor: not-allowed; }

.ai-rules { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.ai-rule { font-size: 13px; }
.ai-code {
  margin: 0; padding: 10px 12px; border-radius: 8px;
  background: var(--surface); border: 1px solid var(--line);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; line-height: 1.5; overflow-x: auto; white-space: pre;
}
.ai-caveat { border-left: 3px solid var(--orange); }

.ai-column { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.ai-column-head { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; }
.ai-step {
  grid-row: span 2;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--orange); color: #000;
  font-weight: 700; font-size: 13px;
}
.ai-column-head h3 { margin: 0; font-size: 15px; align-self: center; }
.ai-hint { margin: 0; color: var(--muted); font-size: 12px; grid-column: 2; }

.ai-card, .ai-class {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 12px;
}
.ai-card { display: flex; flex-direction: column; gap: 10px; }

.ai-class-list { display: flex; flex-direction: column; gap: 10px; }
/* The colour bar is how a class is followed across all three columns —
   the same hue on its card, its prediction bar and its row in the grid. */
.ai-class { border-left: 4px solid var(--class-colour, var(--orange)); }
.ai-class-head { display: flex; align-items: center; gap: 8px; }
.ai-class-name {
  flex: 1; min-width: 0;
  background: transparent; border: 1px solid transparent;
  border-radius: 6px; padding: 4px 6px;
  color: var(--ink); font: inherit; font-weight: 600;
}
.ai-class-name:hover { border-color: var(--line); }
.ai-class-name:focus { border-color: var(--orange); outline: none; }
.ai-icon-button {
  background: transparent; border: 0; cursor: pointer;
  color: var(--muted); font-size: 14px; padding: 4px 6px; border-radius: 6px;
}
.ai-icon-button:hover { color: var(--ink); background: var(--surface); }

.ai-count { margin: 6px 0 0; font-size: 12px; color: var(--muted); }
.ai-count.is-short { color: var(--orange-ink); }
.ai-thumbs { display: flex; flex-wrap: wrap; gap: 4px; margin: 8px 0; }
.ai-thumb {
  width: 34px; height: 34px; border-radius: 6px;
  object-fit: cover; cursor: pointer;
  border: 1px solid var(--line);
}
.ai-thumb:hover { border-color: var(--orange); opacity: .7; }

.ai-class-tools { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.ai-capture, .ai-upload {
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  border-radius: 8px; padding: 6px 12px; font-size: 13px; cursor: pointer;
}
.ai-capture:hover, .ai-upload:hover { border-color: var(--orange); }
.ai-capture.is-live { background: var(--orange); color: #000; border-color: var(--orange); }
.ai-upload input { display: none; }
.ai-add {
  border: 1px dashed var(--line); background: transparent; color: var(--muted);
  border-radius: 10px; padding: 10px; cursor: pointer; font-size: 13px;
}
.ai-add:hover { border-color: var(--orange); color: var(--ink); }

/* Left over from the first three-column layout, and the reason the
   camera stayed portrait after the stage was made landscape: a 220px cap
   and its own 4:3 box, both winning over the stage around it. The stage
   sets the shape now; the video simply fills it. */
.ai-video, .ai-stage video {
  width: 100%; height: 100%; border-radius: 8px;
  background: #000; object-fit: cover;
}
.ai-stage { display: flex; justify-content: center; }

/* Findings sit above the Train button, because the point is to fix the
   examples rather than to explain a bad model afterwards. */
.ai-findings { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.ai-finding {
  font-size: 12px; line-height: 1.45;
  border-left: 3px solid var(--line);
  padding: 6px 8px; border-radius: 0 6px 6px 0;
  background: var(--surface);
}
.ai-finding.is-blocking { border-left-color: #e0533a; }
.ai-finding.is-warning { border-left-color: var(--orange); }
.ai-finding strong { display: block; font-weight: 600; }
.ai-finding span { color: var(--muted); }

.ai-train-button:disabled { opacity: .45; cursor: not-allowed; }
.ai-score { text-align: center; padding: 6px 0; }
.ai-score-value { margin: 0; font-size: 30px; font-weight: 700; color: var(--orange-ink); }
.ai-score-detail { margin: 2px 0 0; font-size: 11px; color: var(--muted); line-height: 1.4; }
.ai-note { margin: 0; font-size: 12px; color: var(--muted); line-height: 1.5; }

.ai-confusion h4 { margin: 0 0 6px; font-size: 12px; }
.ai-confusion table { border-collapse: collapse; width: 100%; font-size: 11px; }
.ai-confusion th, .ai-confusion td {
  border: 1px solid var(--line); padding: 4px; text-align: center;
}
.ai-confusion th { color: var(--muted); font-weight: 500; }
.ai-confusion td.is-right { background: rgba(46, 160, 108, .22); font-weight: 600; }
.ai-confusion td.is-wrong { background: rgba(224, 83, 58, .22); font-weight: 600; }

.ai-bars { display: grid; gap: 6px; }
.ai-bar-row { display: grid; grid-template-columns: 1fr 2fr auto; gap: 8px; align-items: center; }
.ai-bar-name { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-bar-track { height: 10px; background: var(--surface); border-radius: 5px; overflow: hidden; }
.ai-bar-fill {
  height: 100%; background: var(--class-colour, var(--orange));
  border-radius: 5px; transition: width .12s linear;
}
.ai-bar-value { font-size: 11px; color: var(--muted); min-width: 32px; text-align: right; }
.ai-agreement { margin: 2px 0 0; font-size: 11px; color: var(--muted); }

.ai-robot { border-top: 1px solid var(--line); padding-top: 10px; display: grid; gap: 8px; }
.ai-robot h4 { margin: 0; font-size: 12px; }
.ai-robot-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: center; }
.ai-robot-class { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-robot-row select {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 6px; padding: 5px; font-size: 12px;
}
.danger-button {
  background: #c0392b; color: #fff; border: 0;
  border-radius: 8px; padding: 8px 14px; font-size: 13px; cursor: pointer;
}

/* The two decisions made once, above the three columns: what kind of model,
   and which camera. The camera choice being buried is how a learner teaches
   a model from the wrong lens without noticing. */
.ai-lab-shell { display: flex; flex-direction: column; gap: 14px; }
.ai-chrome {
  display: flex; flex-wrap: wrap; gap: 18px 28px; align-items: center;
  padding: 10px 12px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
}
.ai-kinds, .ai-cameras { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.ai-chrome-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin-right: 4px;
}
.ai-kind {
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  border-radius: 999px; padding: 5px 12px; font-size: 12px; cursor: pointer;
}
.ai-kind:hover:not(:disabled) { border-color: var(--orange); }
.ai-kind.is-active { background: var(--orange); border-color: var(--orange); color: #000; font-weight: 600; }
.ai-kind:disabled { opacity: .5; cursor: not-allowed; }
.ai-cameras select, .ai-stream-url {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 8px; padding: 5px 8px; font-size: 12px;
}
.ai-stream-url { min-width: 230px; }
.ai-cameras .ai-note { flex-basis: 100%; margin: 2px 0 0; }
.ai-soon { max-width: 460px; }

/* The wall of cards: the AI Lab's front door. The card is the choice —
   a dropdown of four words does not tell a child what "numbers" would
   even mean, and this is where they decide what their project is. */
.ai-chooser { padding: 28px 24px 40px; max-width: 66rem; margin: 0 auto; }
.ai-chooser-head { text-align: center; max-width: 40rem; margin: 0 auto 26px; }
.ai-eyebrow {
  margin: 0 0 6px; font-size: 11px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--orange-ink);
}
.ai-chooser-title { margin: 0 0 8px; font-size: 27px; line-height: 1.2; }
.ai-chooser-hint { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.6; }

.ai-wall {
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(15.5rem, 1fr));
}
.ai-recipe {
  display: flex; flex-direction: column; gap: 6px;
  text-align: left; cursor: pointer; font: inherit; color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 16px;
  transition: border-color .12s, transform .12s;
}
.ai-recipe:hover:not(:disabled) { border-color: var(--orange); transform: translateY(-2px); }
.ai-recipe:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.ai-recipe.is-soon { opacity: .62; cursor: not-allowed; }

.ai-recipe-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.ai-recipe-icon {
  width: 42px; height: 42px; border-radius: 12px;
  display: grid; place-items: center; font-size: 20px;
  background: var(--orange); color: #000;
}
.ai-recipe.is-soon .ai-recipe-icon { background: var(--line); color: var(--muted); }
.ai-recipe-badge {
  font-size: 10px; font-weight: 700; letter-spacing: .04em;
  padding: 3px 9px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--line); color: var(--muted);
}
.ai-recipe-title { margin: 0; font-size: 16px; }
.ai-recipe-subtitle { margin: 0; font-size: 12px; font-weight: 600; color: var(--orange-ink); }
.ai-recipe-body {
  margin: 4px 0 0; font-size: 12px; color: var(--muted); line-height: 1.55;
  /* Two lines on the card. The rest is a title attribute — a wall of grey
     text on six cards reads as work to do before you have started. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.ai-recipe.is-soon .ai-recipe-body { -webkit-line-clamp: 3; }
.ai-recipe-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
.ai-tag {
  font-size: 10px; padding: 3px 8px; border-radius: 7px;
  background: var(--surface); border: 1px solid var(--line); color: var(--muted);
}
.ai-recipe-why {
  margin: 10px 0 0; padding-top: 9px; border-top: 1px solid var(--line);
  font-size: 11px; color: var(--muted); line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Even height, so a long reason cannot make one card tower over its row. */
.ai-recipe { min-height: 15.5rem; }

.ai-hub-back {
  background: transparent; border: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 12px;
  padding: 4px 8px; margin-bottom: 6px; border-radius: 6px; text-align: left;
}
.ai-hub-back:hover { color: var(--ink); background: var(--surface); }

/* Inline SVG, sized by the box it sits in. */
.ai-recipe-icon svg { width: 22px; height: 22px; display: block; }

/* The whistle. Over the field rather than in the panel, because the match
   ending is about the match, not about a readout beside it. */
.sim-whistle {
  position: absolute; left: 50%; top: 42%;
  transform: translate(-50%, -50%);
  background: rgba(0, 0, 0, .8); color: #fff;
  padding: 18px 30px; border-radius: 14px;
  text-align: center; pointer-events: none; z-index: 5;
}
.sim-whistle-title {
  margin: 0; font-size: 22px; font-weight: 700; letter-spacing: .01em;
}
.sim-whistle-detail { margin: 6px 0 0; font-size: 13px; opacity: .85; }
.sim-whistle[hidden] { display: none; }

/* The Library Manager. Everything written to a robot, in one window.
   Padded explicitly: the shared rule hangs padding off `dialog form`, and
   this one is not a form — nothing here submits. */
/* Denser than it was. The first version gave every row three lines and
   every section a paragraph of explanation, so six drivers and six models
   filled two screens and the window read as a form to fill in rather than
   a list to pick from. Two columns on a wide screen, one on a phone. */
#library-dialog {
  width: min(96vw, 52rem);
  max-height: 90vh;
  padding: 1.4rem 1.5rem;
  overflow-y: auto;
}
@media (min-width: 46rem) {
  /* Firmware and the robot line are short; the two long lists sit side by
     side so neither has to be scrolled past to reach the other. */
  .library-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; }
}
.dialog-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; margin-bottom: 1.2rem;
}
/* The shared rule puts 1.8rem under every dialog h2, which here would
   push the subtitle away from the title it belongs to. */
.dialog-head h2 { margin-bottom: .3rem; }
.icon-button {
  width: 36px; height: 36px; flex: none;
  color: var(--muted); border: 0; border-radius: 8px;
  background: var(--panel); font-size: 1.4rem; line-height: 1; cursor: pointer;
}
.icon-button:hover { color: var(--ink); }

/* One status line for the window, three tones.
   A left bar, not a tinted border: at 40% alpha on a 1px edge the success
   and failure states were indistinguishable from each other and from
   neutral, which is three tones on paper and one on screen. Colour still
   never carries the meaning — every message reads correctly in mono. */
.library-status {
  margin-bottom: 1rem; padding: .6rem .8rem;
  border: 1px solid var(--line); border-left: 4px solid var(--line);
  border-radius: 10px; background: var(--panel); font-size: .82rem;
}
.library-status[data-tone="good"] { border-left-color: #2f9e52; }
.library-status[data-tone="bad"] { border-left-color: #d1442f; }
.library-status[hidden] { display: none; }

.library-section { padding-top: .9rem; border-top: 1px solid var(--line); }
.library-section:first-of-type { padding-top: 0; border-top: 0; }
.library-columns .library-section { padding-top: 0; border-top: 0; }
.library-section h3 {
  margin-bottom: .35rem; font-size: .7rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
}
.library-section .primary-button { margin-top: .8rem; }
.library-robot { font-size: .9rem; font-weight: 600; }

.library-search {
  width: 100%; min-height: 34px; margin: .5rem 0;
  padding: .35rem .6rem; font: inherit; font-size: .85rem;
  color: var(--ink); border: 1px solid var(--line);
  border-radius: 10px; background: var(--panel);
}
.library-list { display: grid; gap: .35rem; }
.library-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .8rem;
  padding: .5rem .65rem;
  border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
}
/* Flex, not grid. A grid with the button spanning two rows put it in the
   *first* column and pushed the text right — the button ended up on the
   left of every row, which is not where anyone looks for it. */
.library-row > div { min-width: 0; }
.library-row.is-installed { opacity: .66; }
.library-name { font-size: .84rem; font-weight: 600; }
.library-blocked { margin-top: .6rem; }
.library-detail { color: var(--muted); font-size: .75rem; line-height: 1.35; }
/* Beside the summary, not under it: "18 MB · fine on a slow laptop" is a
   caption for the line above rather than a third paragraph. */
/* An inline caption on the end of the summary, not a paragraph. */
.library-meta { font-weight: 600; opacity: .85; }
/* A model that would not load says why, on the row it failed on. */
.library-meta.is-bad { color: #d1442f; font-weight: 500; }
.library-section .secondary-button { margin-top: .8rem; }
.library-section .library-row .secondary-button { margin-top: 0; }
/* The button must not shrink to fit a long summary — "Install" wrapping to
   two letters per line is the failure mode of a flex row like this. */
.library-row .secondary-button { flex: none; }

/* A rule inside the menu, so the robot action reads as a different kind of
   thing from the four project actions above it. */
.menu-rule { height: 1px; margin: .35rem .5rem; border: 0; background: var(--line); }

/* Choosing a Bluetooth robot. Electron has no picker, so this is it. */
#bluetooth-dialog { max-width: 26rem; }
.bluetooth-list { display: grid; gap: 6px; margin: 12px 0; max-height: 16rem; overflow-y: auto; }

/* An action that reads as a sentence rather than a control: revealing the
   hidden devices is a footnote to the list, not a peer of Cancel. */
.link-button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: .82rem;
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.link-button:hover { color: var(--ink); }
.link-button[hidden] { display: none; }
.bluetooth-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; text-align: left; cursor: pointer; font: inherit;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
}
.bluetooth-row:hover { border-color: var(--orange); }
.bluetooth-row.is-known { border-color: var(--orange); }
.bluetooth-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
.bluetooth-tag {
  font-size: 10px; font-weight: 700; white-space: nowrap;
  padding: 3px 9px; border-radius: 999px;
  background: var(--orange-soft); color: var(--orange-ink); border: 1px solid var(--orange);
}

/* A Bluetooth failure is a few sentences and a bracketed error name; it
   needs its line breaks kept. */
#bluetooth-note { white-space: pre-line; line-height: 1.55; }

/* The camera preview, under the simulator's controls.
   Landscape, because a webcam is landscape and a portrait box would letterbox
   it into a stripe — which was a complaint about the AI Lab's first camera. */
/* A preview, not a stage.
   Left unconstrained it took the full width of the simulator column and,
   at 4:3, all of its height — the field then had 957x0 to draw in and said
   so. Capped, it sits under the controls and the table keeps its room. */
.camera-panel {
  margin-top: .6rem;
  width: min(20rem, 40vw);
  flex: none;
}
.camera-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: #0d0d0f;
}
.camera-stage video,
.camera-stage canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
}
/* `cover`, and the overlay does the same arithmetic. A video that letterboxes
   while the boxes assume it fills would drift them by the black bars. */
.camera-stage video { object-fit: cover; }
/* The overlay never takes a click — the controls beneath it must stay
   reachable, and there is nothing here to click anyway. */
.camera-stage canvas { pointer-events: none; }
.camera-readout {
  margin-top: .4rem; color: var(--muted); font-size: .76rem; line-height: 1.4;
}
.camera-panel[hidden] { display: none; }

/* Who this computer is signed in as (ADR-0022).
   Quiet by default: an expiring session earns attention, a healthy one
   does not, and not being signed in earns none at all — it is the free
   tier, which is a working product rather than a state to be fixed. */
.session-line { margin: 0; color: var(--muted); font-size: .8rem; }
.session-line[data-level="soon"] { color: var(--ink); font-weight: 600; }
.session-line[data-level="urgent"],
.session-line[data-level="expired"] { color: var(--danger, #b4402a); font-weight: 600; }
@media (max-width: 700px) { .session-line { display: none; } }

/* Signing in by pairing (TMS contract v1.0.0, POST /pair).
   The code is the interface: a learner reads it off their own school
   account and types it here, often from a phone in the other hand. So the
   field is set large, spaced, and in a face where the characters cannot be
   confused — the code alphabet already excludes 0/O, 1/I/L, 5/S and 8/B,
   and this is the other half of the same care. */
#signin-dialog { width: min(92vw, 28rem); padding: 2rem; }
#signin-dialog h2 { margin-bottom: .6rem; line-height: 1.15; }
#signin-step { margin: 0 0 1.25rem; color: var(--muted); font-size: .95rem; }
/* A label above its control. `dialog label` already sets the type; this
   only stacks the two, so the caption cannot end up beside a full-width
   input where it reads as part of the value. */
.field { display: block; }
.field > span { display: block; margin-bottom: .4rem; }

.signin-code-input {
  width: 100%;
  padding: .8rem 1rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  color: var(--ink);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-align: center;
  text-transform: uppercase;
}
.signin-code-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Whose computer this is. Cards rather than a dropdown, for the same
   reason the board choice uses them: it decides how long the sign-in lasts
   and both options need what distinguishes them written on them. */
.device-choice {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin: 1rem 0;
  padding: 0;
  border: 0;
}
.device-choice legend { margin-bottom: .5rem; font-size: .82rem; color: var(--muted); }
.device-option {
  flex: 1 1 11rem;
  display: flex;
  flex-direction: column;
  gap: .15rem;
  padding: .6rem .9rem;
  border: 1px solid var(--line);
  border-radius: .6rem;
  cursor: pointer;
}
.device-option:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.device-option input { position: absolute; opacity: 0; pointer-events: none; }
.device-option .device-name { font-weight: 600; font-size: .88rem; }
.device-option .device-note { font-size: .75rem; color: var(--muted); line-height: 1.4; }

/* Why a sign-in or a hand-in did not work. Every message behind this comes
   from the tested classifiers, so it says what to do and not only what
   failed. */
.signin-problem {
  margin: .75rem 0 0;
  padding: .6rem .8rem;
  border-radius: 8px;
  border-left: 3px solid var(--danger, #b4402a);
  background: color-mix(in srgb, var(--danger, #b4402a) 8%, transparent);
  color: var(--ink);
  font-size: .84rem;
  line-height: 1.5;
}
.signin-problem[hidden] { display: none; }

.signin-button { padding: .35rem .8rem; border-radius: 8px; font-size: .8rem; }

/* ---------------------------------------------------------------------------
   The School panel: what the teacher set, and what is waiting to be sent.

   No teacher view anywhere in it, by design (REPLY-2026-08-03b §D):
   learners work here and submit, teachers set and mark in the TMS. A
   class-scoped list would put every classmate's work on the least trusted
   machine in the building. */

/* What the school system just did. Outside every panel, so it is visible
   from the School tab and from the workspace alike — the hub's own status
   line is inside the My projects panel and is hidden from both. */
.tms-status {
  margin: 0 0 1rem;
  padding: .65rem .9rem;
  border-radius: 10px;
  border-left: 3px solid var(--line);
  background: var(--panel);
  color: var(--ink);
  font-size: .85rem;
  line-height: 1.5;
}
.tms-status[data-tone="success"] { border-left-color: var(--ok, #16794f); }
.tms-status[data-tone="error"] { border-left-color: var(--danger, #b4402a); }
.tms-status[hidden] { display: none; }

.school-signin {
  max-width: 34rem;
  padding: 1.5rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
}
.school-signin h3 { margin: 0 0 .5rem; }
.school-signin .form-note { margin-bottom: 1rem; }

/* Now a stack of groups, not a grid of cards.
   The grid moved down to `.assignments-cards`, which is the only thing that
   still holds cards. Left as a grid here, the two group sections would have
   been laid out side by side in columns — "Coding tasks" beside "Quizzes"
   rather than above them, which is not the separation that was asked for. */
.assignments-list {
  display: flex;
  flex-direction: column;
  margin: 1rem 0;
}

.assignment-row {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding: 1rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
}
.assignment-row .assignment-title { margin: 0; font-size: .95rem; font-weight: 600; }
.assignment-row .assignment-detail {
  margin: 0;
  font-size: .8rem;
  color: var(--muted);
  line-height: 1.5;
}
.assignment-row > button { margin-top: auto; align-self: flex-start; }

.assignments-problem {
  margin: .5rem 0;
  padding: .6rem .8rem;
  border-radius: 8px;
  border-left: 3px solid var(--danger, #b4402a);
  background: color-mix(in srgb, var(--danger, #b4402a) 8%, transparent);
  font-size: .84rem;
}
.assignments-problem[hidden] { display: none; }

/* The queue. Its own section because "where has my work got to" is a
   different question from "what am I meant to do", and a learner asks the
   first one when the second is already answered. */
.school-queue {
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.school-queue h3 { margin: 0 0 .4rem; font-size: .95rem; }
.school-queue .form-note { margin-bottom: .75rem; }

.handin-select {
  width: 100%;
  padding: .55rem .7rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  color: var(--ink);
  font: inherit;
}
#handin-dialog { width: min(92vw, 30rem); padding: 2rem; }
#handin-dialog h2 { margin-bottom: .6rem; line-height: 1.15; }

/* ---------------------------------------------------------------------------
   Syntax colouring.

   A coloured <pre> sits behind a transparent <textarea>. The browser keeps
   doing the editing — caret, selection, undo, IME, accessibility — and
   nothing here reimplements any of it.

   The two layers MUST agree on every metric that affects where a glyph
   lands. Font, size, line height, letter spacing, padding, tab size, and
   white-space handling are therefore set once, on both, from the same
   values. A disagreement in any one of them shows up as text drifting out of
   its own colours further down the file.
   --------------------------------------------------------------------------- */

.editor-stack {
  position: relative;
  flex: 1;
  min-height: 22rem;
  display: flex;
  overflow: hidden;
}

.editor-stack > .editor-area,
.editor-stack > .editor-highlight {
  margin: 0;
  padding: 1rem;
  border: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85rem;
  line-height: 1.6;
  letter-spacing: normal;
  tab-size: 4;
  white-space: pre;
  overflow-wrap: normal;
  word-break: normal;
}

.editor-stack > .editor-highlight {
  position: absolute;
  inset: 0;
  overflow: hidden;      /* scrolled by script, to follow the textarea */
  pointer-events: none;
  color: var(--ink);
  background: transparent;
}

.editor-stack > .editor-area {
  position: relative;
  flex: 1;
  resize: none;
  overflow: auto;
  background: transparent;
  /* The text itself is invisible; the caret is not. Without the caret colour
     being set explicitly, a transparent-text textarea has an invisible caret
     and the editor appears not to accept typing at all. */
  color: transparent;
  caret-color: var(--ink);
}

.editor-stack > .editor-area::selection { background: rgba(90, 150, 255, .35); }

/* Colours. Chosen to stay legible on both themes and to keep comments
   quieter than code — a comment is context, not the program. */
.tok-comment   { color: #6b7a86; font-style: italic; }
.tok-string    { color: #b5651d; }
.tok-number    { color: #1a7f5a; }
.tok-keyword   { color: #7b3fb5; font-weight: 600; }
.tok-constant  { color: #7b3fb5; }
.tok-builtin   { color: #1f6feb; }
.tok-type      { color: #0b7285; }
.tok-preproc   { color: #8a6d1f; }
.tok-decorator { color: #8a6d1f; }
.tok-operator  { color: var(--muted); }

@media (prefers-color-scheme: dark) {
  .tok-comment   { color: #7d8b96; }
  .tok-string    { color: #e5a06a; }
  .tok-number    { color: #6fd6a8; }
  .tok-keyword   { color: #c9a0ff; }
  .tok-constant  { color: #c9a0ff; }
  .tok-builtin   { color: #79b8ff; }
  .tok-type      { color: #5fd0e0; }
  .tok-preproc   { color: #e0c46c; }
  .tok-decorator { color: #e0c46c; }
}

/* The board choice for a Pico in BOOTSEL. Cards rather than a dropdown: it
   is a decision that cannot be detected and cannot be undone by guessing, so
   both options are visible with what distinguishes them written on them. */
.board-choice {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin: .75rem 0;
  padding: 0;
  border: 0;
}

.board-choice legend { margin-bottom: .5rem; }

.board-option {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .6rem .9rem;
  border: 1px solid var(--line);
  border-radius: .6rem;
  cursor: pointer;
}

.board-option:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.board-option input { position: absolute; opacity: 0; pointer-events: none; }
.board-option .board-name { font-weight: 600; }
.board-option .board-note { font-size: .78rem; color: var(--muted); }

/* ---------------------------------------------------------------------------
   Library Manager as cards.

   The list version fitted a name, a summary and a button on one line and read
   as a settings page. Cards give each thing room to say what it is and what
   installing it costs, which is the difference between choosing and guessing
   — and it matches how V2 arranges the same information, so somebody moving
   between the two IDEs recognises it.

   Two columns where there is room, one where there is not. `auto-fill` rather
   than a fixed count: the dialog is resizable and a two-column grid squeezed
   into a narrow window is worse than a single column.
   --------------------------------------------------------------------------- */

.library-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: .6rem;
  align-items: stretch;
}

.library-row {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  padding: .8rem .9rem;
  border: 1px solid var(--line);
  border-radius: .7rem;
  background: color-mix(in srgb, var(--ink) 3%, transparent);
}

/* The button sits at the bottom of every card whatever the summary length,
   so a row of cards has one line of buttons rather than a ragged edge. */
.library-row > button { margin-top: auto; width: 100%; }

.library-row .library-name { margin: 0; font-weight: 600; }
.library-row .library-detail { margin: 0; font-size: .82rem; color: var(--muted); }

/* Installed cards recede. They are still readable and no longer competing
   for attention with the ones a teacher still has to decide about. */
.library-row.is-installed {
  border-style: dashed;
  background: transparent;
}

/* The size, where something has one. Sizes matter here in a way they do not
   in most software: this runs in schools with metered connections and slow
   machines, and "18 MB" is the difference between yes and later. */
.library-badge {
  align-self: flex-start;
  padding: .1rem .4rem;
  border-radius: .35rem;
  font-size: .7rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--muted);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}

/* Coding tasks — `kind: "task"`.
   The only assignment kind that says what "done" means, so it gets the room
   to say it. Quizzes and courses keep the compact card they had. */
.assignment-row.is-task { border-left: 3px solid var(--accent); }

.assignment-brief {
  margin: .1rem 0 0;
  font-size: .85rem;
  line-height: 1.55;
  color: var(--ink);
}

/* The marking criteria, shown before handing in rather than after marking.
   The identical list the teacher marks against — a child who reads it knows
   what finished looks like instead of guessing. */
.assignment-rubric {
  margin: .5rem 0 0;
  padding: .7rem .85rem;
  border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}
.assignment-rubric-title {
  margin: 0 0 .45rem;
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}
.assignment-rubric ul { margin: 0; padding: 0; list-style: none; }
.assignment-rubric li {
  display: flex;
  justify-content: space-between;
  gap: .75rem;
  padding: .22rem 0;
  font-size: .82rem;
  line-height: 1.45;
}
.assignment-rubric li + li { border-top: 1px solid color-mix(in srgb, var(--ink) 8%, transparent); }
.rubric-points { flex: none; font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; }
.assignment-rubric-total {
  margin: .5rem 0 0;
  font-size: .76rem;
  font-weight: 600;
  color: var(--muted);
}
.assignment-criteria {
  margin: .5rem 0 0;
  font-size: .78rem;
  font-style: italic;
  line-height: 1.5;
  color: var(--muted);
}

/* Linked, never copied: the requirements live on the lesson page and a copy
   held here would drift from what the learner is reading. */
.assignment-lesson {
  margin-top: .55rem;
  font-size: .8rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}
.assignment-lesson:hover { text-decoration: underline; }

/* The same criteria again inside the Hand in window, so the last thing a
   learner reads before submitting is what it will be marked against. */
.handin-rubric { margin-top: .9rem; }
.handin-rubric .assignment-rubric { margin-top: .35rem; }

/* Start this task, beside Hand in. Only tasks carrying a starting point get
   the first one — nothing else in the list has anything to open. */
.assignment-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-top: auto;
  padding-top: .6rem;
}
.assignment-actions .primary-button.compact,
.assignment-actions .secondary-button.compact {
  padding: .35rem .8rem;
  font-size: .8rem;
  border-radius: 8px;
}

/* Reviewing a submission.
   A read-and-run view, not a smaller editor: no hub, no saving, no project
   management. The teacher's job here is to understand what a learner built. */
.review-main { max-width: 68rem; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.review-main h1 { margin: .2rem 0 .3rem; }

.review-source {
  max-width: 42rem;
  margin-top: 1.5rem;
  padding: 1.5rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
}
.review-source h2 { margin: 0 0 .4rem; font-size: 1.05rem; }
.review-source textarea {
  width: 100%;
  padding: .7rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg, transparent);
  color: var(--ink);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: .8rem;
  line-height: 1.5;
  resize: vertical;
}

.review-tabs { display: flex; gap: .35rem; margin: 1.25rem 0 1rem; flex-wrap: wrap; }

/* Tall enough to read a whole program without scrolling the page, which is
   what a teacher does first. */
.review-canvas { height: min(62vh, 34rem); border: 1px solid var(--line); border-radius: 12px; }

/* The run pane, laid out as the editor's simulator is: the robot gets the
   room and the readout sits beside it. Stacked on a narrow screen, because
   a 17rem panel next to a phone-width canvas leaves neither usable. */
.review-run {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 17rem;
  gap: 0;
  height: min(62vh, 34rem);
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
}
.review-stage { min-width: 0; height: 100%; }
.review-run .sim-devices { overflow-y: auto; }
.review-run-bar {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  margin-top: .75rem;
}
@media (max-width: 900px) {
  .review-run { grid-template-columns: minmax(0, 1fr); height: auto; }
  .review-stage { height: min(50vh, 26rem); }
  .review-run .sim-devices { max-height: 22rem; border-left: 0; border-top: 1px solid var(--line); }
}


.review-code {
  margin: 0;
  padding: 1rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  overflow-x: auto;
  font-size: .82rem;
  line-height: 1.6;
}

/* Marking, in the review page.
   Rows are the teacher's own rubric from the TMS; the IDE never invents
   one, and shows no total until every row has a score — a partial sum
   beside a denominator reads as a mark and is not one yet. */
.mark-rubric { margin: 1rem 0; display: grid; gap: .5rem; max-width: 42rem; }
.mark-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: .6rem;
  padding: .6rem .8rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
}
.mark-label { margin: 0; font-size: .88rem; font-weight: 600; }
.mark-score {
  width: 4.5rem;
  padding: .35rem .5rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--bg, transparent);
  color: var(--ink);
  font: inherit;
  text-align: right;
}
.mark-outof { font-size: .82rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.mark-total { margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; }

/* What the teacher allowed on this task.
   Sits above the action bar so it is read before a button is pressed. */
.task-limits {
  /* Positioned like the action bar rather than relative to it: the two are
     siblings, so anchoring to 100% would have measured the whole canvas. */
  position: absolute;
  right: clamp(1rem, 2vw, 2rem);
  bottom: 4.9rem;
  z-index: 30;
  margin: 0;
  padding: 0.35rem 0.7rem;
  border-radius: 0.6rem;
  font-size: 0.85rem;
  line-height: 1.35;
  max-width: 22rem;
  background: var(--surface-raised, #fff);
  border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

/* Locked is stated in words as well as colour — the sentence is the
   message and the colour only agrees with it. */
.task-limits[data-state="locked"] {
  border-color: var(--warning-border, #d9822b);
  background: var(--warning-surface, #fff6ec);
  color: var(--warning-text, #7a4a12);
}

/* ---------------------------------------------- grouped assignments
   Two unrelated kinds of work shared one grid: coding tasks a teacher
   wrote, and lesson quizzes that arrive eight at a time. The grouping is
   the message, so the headings carry weight and the rows do not. */

.assignment-group + .assignment-group {
  margin-top: 2.25rem;
}

.assignment-group-title {
  margin: 0 0 0.25rem;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}

.assignment-group-note {
  margin: 0 0 0.9rem;
  font-size: 0.88rem;
  opacity: 0.72;
}

/* Tasks keep the card grid they already had. */
.assignments-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 1rem;
  align-items: start;
}

/* Everything else is a list. A quiz has nothing to show but its name, and
   a card the size of a task's claims it is comparable work. */
.assignments-rows {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.assignment-compact {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.65rem 0.9rem;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.1));
  border-radius: 0.7rem;
  background: var(--surface, transparent);
}

.assignment-compact-title {
  margin: 0;
  font-weight: 600;
  /* A long generated name must wrap rather than push the link off the row. */
  overflow-wrap: anywhere;
}

.assignment-compact-detail {
  margin: 0.1rem 0 0;
  font-size: 0.85rem;
  opacity: 0.7;
}

/* The tasks group leads, so it is the one that gets the accent. */
.assignment-group.is-tasks .assignment-group-title {
  color: var(--accent, #e2571e);
}

@media (max-width: 34rem) {
  .assignment-compact {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* A task whose goes are spent. Read, never change.
   Stated in words by #task-limits; this only agrees with it. */
.blockly-canvas.is-locked,
#workspace.is-locked #editor {
  /* Not opacity: a learner still has to read their own work clearly. */
  cursor: not-allowed;
}

#workspace.is-locked #editor {
  background-image: repeating-linear-gradient(
    135deg, transparent, transparent 14px,
    rgba(128, 128, 128, 0.05) 14px, rgba(128, 128, 128, 0.05) 28px);
}
