/*
 * Deliberately plain. Two things this stylesheet must not do:
 * get in the way of a screen reader, and stop working at 200% zoom.
 *
 * Colour is never the only signal for anything. Focus is always visible, and
 * the focus ring is thick enough to find at low vision without a magnifier.
 */

:root {
  --ink: #12171c;
  --muted: #4b5560;
  --page: #f4f6f8;
  --panel: #ffffff;
  --line: #c9d2da;
  --accent: #0a5f8a;
  --accent-ink: #ffffff;
  --warning-bg: #fdf3d8;
  --warning-ink: #6b4a04;
  --error-ink: #98241a;
  --focus: #b2431a;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e8eef2;
    --muted: #a9b6c0;
    --page: #11161a;
    --panel: #192126;
    --line: #33414b;
    --accent: #62c3ea;
    --accent-ink: #06202c;
    --warning-bg: #33290f;
    --warning-ink: #f0cd7d;
    --error-ink: #ff9d90;
    --focus: #ffb27a;
  }
}

* { box-sizing: border-box; }

/*
 * The `hidden` attribute must win.
 *
 * Its `display: none` comes from the browser's own stylesheet, and ANY author
 * rule that sets `display` beats it -- so a single `.tab-panel { display:
 * flex }` silently un-hides every hidden panel. That is not just a layout
 * bug: a panel that is still rendered is still in the accessibility tree, so
 * a screen reader announces content the tab says is not showing, and
 * aria-selected becomes a lie.
 *
 * !important is the standard fix, and is why normalize.css carries the same
 * rule. Toggle visibility with `element.hidden`, never with a display rule.
 */
[hidden] { display: none !important; }

/*
 * Nothing on this page is smaller than 16px.
 *
 * Secondary text used to be set at 0.9-0.95rem, which is the ordinary way to
 * make a hint look like a hint — and it put the smallest type in the app on
 * the sentences that explain how to use it. A student who has to magnify
 * before they can read the instructions has been charged for the instructions.
 *
 * Rank is carried by colour and weight instead: `--muted` for secondary,
 * bold for emphasis. Both survive magnification; size does not, because
 * shrinking is the thing being avoided.
 */
body {
  margin: 0;
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--ink);
  background: var(--page);
}

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/*
 * One way of hiding something, used twice.
 *
 * The skip link is in this list rather than keeping its own `left: -9999px`,
 * which is the older trick and the only thing on the page that genuinely sat
 * outside its container — 9999px outside it. Nothing behind it can be worked
 * out from there, so a contrast checker reports the link as unverifiable, and
 * an overlap checker reports a 9999px box that overlaps everything. Both were
 * right, and neither told us anything, because the link is not there to be
 * seen until it is focused.
 *
 * Sharing the selector rather than repeating the declarations keeps it that
 * way: one technique in this file, not two that drift.
 */
.visually-hidden,
.skip-link:not(:focus) {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* And on focus it is an ordinary visible chip, in the top corner. */
.skip-link:focus {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 100;
  background: var(--accent);
  color: var(--accent-ink);
  padding: 0.6rem 1rem;
}

.app-header { padding: 0.75rem 1rem 0; }

/* The title row. The banner around it is a plain block, because it also
   carries the progress caption and the connection notes, and those are
   paragraphs that want their own lines. */
.app-title {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
}
.app-header h1 { font-size: 1.3rem; margin: 0; }
.connection-summary { margin: 0; color: var(--muted); }

.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  /* Tops aligned, not centred and not stretched. The fields are three rows
     tall now and a description can wrap to two lines, so centring left their
     labels at three different heights and `stretch` pulled every button to
     the tallest one. */
  align-items: flex-start;
}

/*
 * Nothing you have to hit is smaller than 24 by 24 (WCAG 2.5.8).
 *
 * A browser's default checkbox is about 13px square and a range track about
 * 16px tall. That is a small target for anybody and a hard one for a student
 * whose hands do not do exactly what they are told — which, in a room full of
 * students with a print disability, is a fair few of them. The controls are
 * sized here rather than left to the browser, and the checkboxes look better
 * for it.
 */
input[type="checkbox"],
input[type="radio"] {
  inline-size: 1.5rem;
  block-size: 1.5rem;
  margin: 0;
}

input[type="range"] {
  block-size: 1.5rem;
}

select {
  font: inherit;
  min-block-size: 1.75rem;
  padding: 0.3rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  max-width: 100%;
}

button {
  font: inherit;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--accent);
  border-radius: 4px;
  background: var(--accent);
  color: var(--accent-ink);
  cursor: pointer;
}
button.secondary {
  background: var(--panel);
  color: var(--ink);
  border-color: var(--line);
}
button[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}
/* The keyboard hint on a button. It used to be 0.8em, which on a 16px button
   is 12.8px — small enough to be the smallest text on the page and the only
   thing on it below a comfortable reading size. It is told apart from the
   label by weight and spacing instead. */
.shortcut {
  font-weight: 400;
  opacity: 0.85;
  margin-left: 0.45rem;
}

.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(320px, 26rem);
  gap: 1rem;
  padding: 0 1rem 1rem;
  align-items: start;
}
@media (max-width: 60rem) {
  .layout { grid-template-columns: 1fr; }
}

h2 { font-size: 1.05rem; margin: 0 0 0.35rem; }
.hint { margin: 0 0 0.5rem; color: var(--muted); }

.workspace-panel, .side-panel > section {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.75rem;
}
.side-panel { display: grid; gap: 1rem; }

.blockly-host {
  height: min(62vh, 40rem);
  min-height: 20rem;
  border: 1px solid var(--line);
  border-radius: 4px;
}

.warnings { margin: 0.5rem 0 0; min-height: 1.5em; }
.warnings.has-warnings {
  background: var(--warning-bg);
  color: var(--warning-ink);
  padding: 0.5rem 0.7rem;
  border-radius: 4px;
}

.log-options {
  display: grid;
  gap: 0.25rem;
  margin-bottom: 0.5rem;
}
.log-options label { display: flex; gap: 0.5rem; align-items: center; }

.log {
  list-style: none;
  margin: 0 0 0.5rem;
  padding: 0.5rem;
  height: 16rem;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--page);
}
.log .entry { padding: 0.2rem 0; border-bottom: 1px solid var(--line); }
.log .entry:last-child { border-bottom: none; }
/* a leading marker, so kind is not signalled by colour alone */
.log .entry-console::before { content: "printed: "; font-weight: 600; }
.log .entry-error::before { content: "error: "; font-weight: 600; }
.log .entry-warning::before { content: "warning: "; font-weight: 600; }
.log .entry-error { color: var(--error-ink); }
.log .entry-status { font-weight: 600; }

.python {
  width: 100%;
  font: 1rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--page);
  color: var(--ink);
  resize: vertical;
}

/*
 * A visible answer where a `title` tooltip used to be: what a mat teaches,
 * what a build is for, why Save behaves oddly in this browser. A tooltip
 * needs a mouse and a hover, so it said nothing to a keyboard, a touchscreen
 * or a screen reader.
 *
 * It used to span a whole toolbar row as a flex item. Inside a field's stack
 * it is simply the third line, under the control it describes.
 */
.choice-note {
  margin: 0;
  max-width: 22rem;
  color: var(--muted);
}

/* A label above the control it names, rather than beside it. */
.field-label {
  display: block;
  margin-bottom: 0.25rem;
  font-weight: 600;
}

/* --- the program toolbar ------------------------------------------------ */

/*
 * The program's fields, on a row of their own.
 *
 * Without this the three of them and the four file buttons share one wrapping
 * row, and the buttons land wherever the fields stop — New and Open marooned
 * to the right of the mat, Save and Save as on the line below. Giving the
 * fields the full width keeps each group whole.
 */
.program-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.5rem 1.5rem;
  flex: 1 1 100%;
}

/*
 * Label, field, description: stacked and left aligned, as a form is.
 *
 * Used by all three of the program's fields — its name, its robot and its
 * mat — so they read as one form rather than three arrangements.
 *
 * The three belong to each other, so they share one left edge and read top to
 * bottom in the order you need them. That is worth more than the row it
 * costs: at large magnification a label beside its field is the first thing
 * to leave the viewport, and a reader who has scrolled to the input has no
 * way to know what it was called. Stacked, panning down the left edge finds
 * the name, the box and its state in turn.
 */
.program-name-field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.2rem;
}

.program-name { font-weight: 600; }

.program-name-input,
.program-name-field select {
  font: inherit;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  max-width: 100%;
}

.program-name-input {
  width: 22rem;
}

/*
 * The menus size to their own longest option rather than to a number I chose.
 *
 * "Standard base — 56mm wheels, 160mm apart" is 22rem and a bit, and a fixed
 * width clipped it to "160mm ap". A picker that hides half of what it is
 * offering is not one, and the measurements are the whole reason a student
 * would pick one base over another.
 */
.program-name-field select {
  width: auto;
  min-width: 14rem;
  /* Room for the disclosure arrow on the right. Without it `width: auto`
     sizes to the text alone and the last couple of pixels go under the
     arrow. */
  padding-right: 2rem;
}

/* The label above a field, in the same weight as the program name's. */
.choice-label { font-weight: 600; }

/*
 * Deliberately not a live region. Whether there are unsaved changes is worth
 * knowing on demand and maddening to hear announced after every block moved.
 */
/* The third line of the stack. No min-width any more: it stopped the toolbar
   row shuffling as the text changed, and it is no longer in that row. */
.save-state {
  color: var(--muted);
}

/* Shown only when the editor is served from the web, where a browser will not
   let the page reach a simulator running on the reader's own machine. */
/* --- "something is happening" ------------------------------------------- */

/*
 * Starting the built-in simulator downloads about five megabytes of Python.
 * The status region announces each stage, which does nothing for someone who
 * is not listening to it, so the wait needs a visible form too: a progress
 * bar, a caption, and a cursor that says the page is working.
 *
 * The cursor is set on the whole document and on buttons explicitly, because
 * a button's own `cursor: pointer` would otherwise win over an inherited one.
 */
.is-busy,
.is-busy button {
  cursor: progress;
}

/* --------------------------------------------------------------------------
   System messages
   --------------------------------------------------------------------------
   The visible half of something the simulator says out loud. It has to be
   readable at a glance and from across a club room, so it is not muted text:
   a Deaf student is getting all of this message from the screen. */

.system-message {
  margin: 0.75rem 0 0;
  padding: 0.6rem 0.9rem;
  border: 2px solid var(--ink);
  border-radius: 0.4rem;
  background: var(--panel);
  color: var(--ink);
  font-size: 1rem;
  font-weight: 600;
  /* Long enough to read; it is replaced rather than queued, so it never
     stacks up into a wall of messages. */
  max-inline-size: 60ch;
}

.busy {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0.75rem 0 0;
  color: var(--muted);
}

.busy progress {
  inline-size: 12rem;
  block-size: 0.6rem;
}

/* A control that is busy.
   Shown, not stated: marking it in ARIA is a state change on whichever button
   was just pressed, and a screen reader reads that out over the top of the
   connection announcing itself. Pressing it while it works is answered in
   words instead, by explainBusy(). */
button.is-working {
  opacity: 0.55;
  cursor: progress;
}

/* A control that is genuinely unavailable, which for the connect buttons means
   you are already connected to that thing. A real `disabled`, so it leaves the
   tab order and is announced properly to somebody who tabs onto it later --
   safe because focus is moved to Run before it is set. */
button:disabled {
  opacity: 0.55;
}

/* --------------------------------------------------------------------------
   Spoken commentary
   --------------------------------------------------------------------------
   The controls that decide whether a student hears anything, and the visible
   transcript of what was said.

   The transcript is not a courtesy. A Deaf student, a student in a room full
   of other people's robots, and a coach checking what their student was told
   all read the same sentences the speech engine spoke. It is the picture's
   caption, and it is the only part of the 3D view that can be quoted.
   -------------------------------------------------------------------------- */

.commentary {
  margin-top: 0.75rem;
  padding: 0.75rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
}

.commentary-heading {
  margin: 0 0 0.35rem;
  font-size: 1rem;
}

.commentary-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1.2rem;
}

/*
   One field is one flex item, so wrapping can only ever happen *between*
   fields. When they were all siblings, a narrow panel wrapped wherever it
   liked and left "Speed" sitting beside the volume slider.

   `flex: 1 1 15rem` lets them sit in a row when there is room and take a line
   each when there is not, which in the editor's side panel is most of the
   time.
 */
.commentary-field {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex: 1 1 15rem;
}

.commentary-field input[type="range"] { flex: 1; min-width: 6rem; }
.commentary-field label { white-space: nowrap; }

.commentary-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  flex: 1 1 100%;
}

.commentary-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 1 1 100%;
}

/* The read-outs. Wide enough that the row does not shuffle as the text
   changes between "normal speed" and "2.5 times normal speed". */
#commentary-volume-value,
#commentary-rate-value {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
  min-width: 11ch;
}

.commentary-transcript {
  margin: 0.6rem 0 0;
  padding: 0.4rem 0.6rem;
  max-height: 8rem;
  overflow-y: auto;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--page);
}

.commentary-transcript:empty { display: none; }

.commentary-entry {
  padding: 0.15rem 0;
  border-bottom: 1px solid var(--line);
}

.commentary-entry:last-child { border-bottom: 0; }

/* --------------------------------------------------------------------------
   Why a connection did not happen
   --------------------------------------------------------------------------
   Pressing a connect button must always produce something a person can SEE.
   Every explanation used to go only to the visually-hidden status region, so
   "Connect to a hub" on a browser without Web Bluetooth looked like a button
   with nothing behind it. A `title` tooltip was the only visible fallback,
   and a tooltip needs a mouse: no keyboard, no touchscreen, no help.
   -------------------------------------------------------------------------- */

.connect-note {
  margin: 0.75rem 0 0;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line);
  border-left: 4px solid var(--focus);
  border-radius: 4px;
  background: var(--warning-bg);
  color: var(--warning-ink);
  max-width: 70ch;
}

.connect-note a { color: inherit; }

/* Which channel the commentary is actually using. Normally unremarkable; when
   it is anything but the browser voice, it is the answer to "why can I not
   hear anything", so it is marked as something to read. */
.commentary-channel {
  margin: 0.5rem 0 0;
  color: var(--muted);
}

.commentary-channel:empty { display: none; }

.commentary-channel.is-fallback {
  padding: 0.4rem 0.6rem;
  border-left: 4px solid var(--focus);
  background: var(--warning-bg);
  color: var(--warning-ink);
}

/* The voice picker. Long names, so let it size to its content but not run
   away with the row. */
#commentary-voice { max-width: 18rem; }

/* --------------------------------------------------------------------------
   Blockly's workspace controls
   --------------------------------------------------------------------------
   The zoom buttons and the trashcan are drawn from a sprite sheet whose icons
   are #888 — 3.5:1 on the white workspace, which would pass. Blockly then
   renders them at `opacity: .4`, which mixes them towards the background to
   about #cfcfcf and 1.6:1. WCAG 1.4.11 asks for 3:1 on a user interface
   component, and these are the controls a student with low vision most needs
   to find: they are how the blocks get bigger.

   Darkened with a filter rather than by editing the sprite, because the
   sprite is copied out of node_modules at build time and anything written
   into it would be overwritten on the next build.

   Hover and focus go darker still. Blockly's own rules take opacity *up* on
   hover, from .4 to .8, which is the right direction when low opacity is the
   resting state; once the resting state is opaque, more emphasis has to mean
   darker, or hover would wash the icon out.

   These selectors match Blockly's own, so they win on source order alone:
   Blockly injects its stylesheet at the top of <head> and this file is linked
   below it. No !important, and nothing here reaches into the workspace's
   behaviour — only how visible its own controls are.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Blockly's keyboard focus ring
   --------------------------------------------------------------------------
   Blockly marks the focused block, field, zoom button and trashcan by
   stroking them with --blockly-active-node-color, which the geras theme sets
   to #fc3. Against the white workspace that is 1.51:1, and against the pale
   field rects on a block it is 1.02:1. WCAG 1.4.11 wants 3:1 for a focus
   indicator — and this is the indicator for the one navigation mode a blind
   or low-vision student has no alternative to. A focus ring you cannot see is
   not a focus ring.

   The ring has to clear 3:1 on every surface it can land on at once, and
   those span the white workspace, the #ddd flyout, the pale field rects
   (#bddbd1) and the eight category colours, the darkest of which is #5b67a5.
   Measured, that leaves no light colour: white itself is only 2.97:1 against
   the lightest category. A near-black clears all of them — #111 is 3.5:1 on
   the darkest block and 18.9:1 on the workspace.

   These are literals rather than var(--focus), and that is the dark mode
   answer rather than an oversight. Blockly's own surfaces do not respond to
   prefers-color-scheme at all: the workspace stays #fff and the flyout #ddd
   in both themes — only the page around them changes, and that sits behind
   the workspace, never behind the ring. A token that flips would be chosen
   against a backdrop that does not.

   Custom properties, which is Blockly's own mechanism, so nothing here
   reaches past the API. The tripled class matches the specificity of
   Blockly's `.geras-renderer.<theme>.injectionDiv` without naming the
   renderer or the theme, and wins on source order because Blockly injects its
   stylesheet above ours.
   -------------------------------------------------------------------------- */

.injectionDiv.injectionDiv.injectionDiv {
  --blockly-active-node-color: #111111;
  /* The container outline, on the #ddd toolbox and flyout. Blockly's #1379f6
     is 3.04:1 there — passing, with nothing left for a rounding error. */
  --blockly-active-tree-color: #0a5f8a;
}

/*
 * Two bands, not one.
 *
 * A single dark ring clears 3:1 on every surface here, but only because those
 * surfaces happen to stay light. One band is a bet that the background will
 * not change; two is not a bet at all — a dark band and a light band together
 * cannot both disappear, whatever ends up behind them. It is also what tells
 * a ring apart from a thicker border at a glance, which is the other half of
 * the job.
 *
 * The zoom buttons and the trashcan own a separate <rect> for this, so theirs
 * can be pushed off the icon as well: scaled about its own centre rather than
 * given a fixed inset, because the trashcan's rect is 55x68 and the zoom
 * buttons' are 40x40 and one number would not suit both. `outline-offset` is
 * no use — Chrome accepts it on an SVG element and computes it to 0.
 *
 * The scale is deliberately modest. Blockly stacks the zoom controls about
 * 34px apart, so a ring much larger than this starts enclosing its neighbour,
 * which is worse than a tight ring.
 */
.blocklyZoom:focus-visible > .blocklyFocusRing,
.blocklyTrash:focus-visible > .blocklyFocusRing {
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(1.12);
  stroke: #111111;
  stroke-width: 2.5px;
  outline: 2px solid #ffffff;
}

/*
 * Blocks and fields get the same two bands, by a different route.
 *
 * Their ring is the block's own outline stroke, so there is nothing to push
 * it away from — a gap would need Blockly to draw a second path, and it draws
 * one. The light band therefore comes from a drop-shadow, which follows the
 * block's shape; an outline would follow its bounding box and draw a
 * rectangle around a puzzle piece.
 *
 * The selected rule is here because geras writes `stroke: #fc3` for
 * `.blocklySelected > .blocklyPath` as a literal, out of reach of the theme,
 * and at a higher specificity than the focus rule — so it is what actually
 * shows while a block is focused, and it fails 1.4.11 on four of our eight
 * categories. Hence the repeated class: enough specificity to beat
 * `.geras-renderer.<theme> .blocklySelected > .blocklyPath` without naming a
 * renderer or a theme that a later change would silently rename.
 */
/*
 * Which band is the crisp one depends on what it is drawn on.
 *
 * The workspace is not one surface. An ordinary block is dark since the
 * palette was lowered; a shadow block — the `hello` and the `25` that sit
 * inside other blocks — is pale, and so is a field rect, which is white at
 * 60% over its block. A ring that suits one is close to invisible on the
 * other: measured, white reads 5.6:1 on a block and 1.7:1 on a shadow block,
 * and near-black is the reverse.
 *
 * So the crisp band follows the surface and the halo takes the other side.
 * Both bands exist in every case, which is what stops any of this from being
 * a single point of failure.
 */
.injectionDiv .blocklySelected:not(.blocklyShadow) > .blocklyPath.blocklyPath.blocklyPath,
.injectionDiv :not(.blocklyShadow) > .blocklyActiveFocus.blocklyPath.blocklyPath.blocklyPath {
  stroke: #ffffff;
  stroke-width: 3px;
  filter: drop-shadow(0 0 1.5px #111111) drop-shadow(0 0 1.5px #111111);
}

.injectionDiv .blocklySelected.blocklyShadow > .blocklyPath.blocklyPath.blocklyPath,
.injectionDiv .blocklyShadow > .blocklyActiveFocus.blocklyPath.blocklyPath.blocklyPath,
.injectionDiv .blocklyActiveFocus.blocklyField > .blocklyFieldRect {
  stroke: #111111;
  stroke-width: 3px;
  filter: drop-shadow(0 0 1.5px #ffffff) drop-shadow(0 0 1.5px #ffffff);
}

/*
 * The workspace scrollbars.
 *
 * Blockly's are #ccc on the white workspace (1.6:1) and #bbb on the flyout's
 * grey (1.4:1), against the 3:1 a control needs. This used to go through the
 * theme's `scrollbarColour`, which works and writes the colour into the
 * element's style attribute — and an inline style is the one thing a reader's
 * own stylesheet cannot override without !important.
 *
 * The selectors mirror Blockly's exactly, including the odd
 * `background:hover + handle` pair it uses to catch the whole scrollbar
 * gutter, so hovering still darkens rather than falling back to the resting
 * colour.
 */
.blocklyScrollbarHandle,
.blocklyFlyout .blocklyScrollbarHandle {
  fill: #6b7681;
}

.blocklyScrollbarBackground:hover + .blocklyScrollbarHandle,
.blocklyScrollbarHandle:hover,
.blocklyFlyout .blocklyScrollbarBackground:hover + .blocklyScrollbarHandle,
.blocklyFlyout .blocklyScrollbarHandle:hover {
  fill: #4b5560;
}

/*
 * A pointer on the things that can be pressed.
 *
 * Blockly leaves the zoom buttons, the trashcan and the dropdown fields on the
 * default arrow, so the cursor says nothing about whether the thing under it
 * does anything. There is no success criterion for `cursor: pointer` — but at
 * high magnification the pointer is doing work it does not do at 100%: the
 * viewport holds a few controls at a time, hover is how you confirm you have
 * landed on one, and an arrow that never changes withholds that confirmation
 * from exactly the people who most need it.
 *
 * The text fields are deliberately left alone. Blockly gives them
 * `cursor: text`, and an I-beam over a control whose job is editing `hello` is
 * a better answer than a pointer, not a worse one.
 *
 * The dragging rules are restated below because ours would otherwise win on
 * source order and leave a field showing a pointer while it is being dragged.
 */
.blocklyZoom,
.blocklyTrash,
.blocklyField.blocklyDropdownField {
  cursor: pointer;
}

/* Blockly's own, re-asserted after the rule above so a drag still says so. */
.blocklyDragging .blocklyField.blocklyDropdownField,
.blocklyDragging .blocklyZoom,
.blocklyDragging .blocklyTrash {
  cursor: grabbing;
}

/*
 * The toolbox categories, which open the flyout when you press them.
 *
 * Guarded rather than restated. Dragging a block over the toolbox puts
 * `blocklyToolboxDelete` or `blocklyToolboxGrab` on the toolbox div, and the
 * categories inherit that cursor — a bin or a closed hand, which is the answer
 * to "what happens if I let go here". Claiming the cursor unconditionally
 * would replace that answer with a pointer, so instead this rule stops
 * matching while either state is on and Blockly's own takes over untouched.
 * That way nothing here repeats Blockly's cursor asset paths, which would rot
 * the day it renames one.
 */
.blocklyToolbox:not(.blocklyToolboxDelete):not(.blocklyToolboxGrab) .blocklyToolboxCategoryContainer,
.blocklyToolbox:not(.blocklyToolboxDelete):not(.blocklyToolboxGrab) .blocklyToolboxCategoryLabel {
  cursor: pointer;
}

/*
 * One gap of Blockly's that the change above would otherwise expose. It gives
 * the label a delete cursor while a block is being dragged for deletion, but
 * nothing for the grab state, so the label falls back to its resting rule.
 * That was invisible while the resting cursor was also an arrow. Now that it
 * is a pointer, the label would flick to an arrow mid-drag while the row
 * around it said grabbing — which reads as a bug rather than as an answer.
 */
.blocklyToolboxGrab .blocklyToolboxCategoryLabel {
  cursor: grabbing;
}

/* Blockly's tooltip is `font: 9pt sans-serif` — 12px, the smallest text it
   draws. Same reason as everything else here: a tooltip explaining a block is
   no use to somebody who has to magnify to read the explanation. */
.blocklyTooltipDiv {
  font: 1rem/1.4 system-ui, -apple-system, "Segoe UI", sans-serif;
}

.blocklyZoom > image,
.blocklyZoom > svg > image,
.blocklyTrash > image,
.blocklyTrashLid {
  opacity: 1;
  /* #888 -> about #6d6d6d, 5.2:1 on the workspace */
  filter: brightness(0.8);
}

.blocklyZoom > image:hover,
.blocklyZoom > svg > image:hover,
.blocklyZoom:focus > image,
.blocklyZoom:focus > svg > image,
.blocklyTrash:hover > image,
.blocklyTrash:focus > image,
.blocklyTrash:hover > .blocklyTrashLid,
.blocklyTrash:focus > .blocklyTrashLid,
.blocklyTrash.blocklyTrashFull > image,
.blocklyTrash.blocklyTrashFull > .blocklyTrashLid {
  opacity: 1;
  filter: brightness(0.55);
}

.blocklyZoom > image:active,
.blocklyZoom > svg > image:active {
  opacity: 1;
  filter: brightness(0.35);
}

/* --------------------------------------------------------------------------
   Dark mode reaches the workspace
   --------------------------------------------------------------------------
   Blockly has no dark theme and no opinion about prefers-color-scheme, so
   until now the page went dark around a workspace that stayed white — a lit
   slab in the middle of a dark room. Worse than ugly: the toolbox keeps its
   #ddd background while its category labels inherit our --ink, which in dark
   mode is near-white. Start, Movement, Motors and the rest were rendering at
   about 1.1:1. Effectively invisible, and only in dark mode, which is why it
   survived every check made in the light.

   The blocks themselves are the awkward part. They are mid-dark since the
   palette was lowered so their white labels could be read, and no dark
   background separates from them — measured, the best of several was 2.1:1
   against the closest category, where a component needs 3:1. Lightening them
   for dark mode would flip their labels to black and give the editor two
   different palettes to keep in step.

   So the block keeps its colour and gains an edge. #c9d2da is 11.9:1 against
   the workspace behind it and 3.4:1 against the darkest block it outlines,
   which is the pair that has to hold. Shadow blocks are excluded: they sit
   inside other blocks, not on the workspace, and already contrast with what
   is behind them.
   -------------------------------------------------------------------------- */

@media (prefers-color-scheme: dark) {
  .blocklySvg {
    background-color: #11161a;
  }

  .blocklyToolbox {
    background-color: #192126;
  }

  .blocklyFlyoutBackground {
    fill: #192126;
  }

  /* The grid is drawn as <line stroke="#ccc"> inside a <pattern>, which is a
     presentation attribute, so CSS reaches it. Dim rather than dark: it is
     orientation, not content. */
  pattern[id^="blocklyGridPattern"] line {
    stroke: #33414b;
  }

  /* The edge that makes a block findable against the workspace. */
  g.blocklyBlock:not(.blocklyShadow) > .blocklyPath {
    stroke: #c9d2da;
    stroke-width: 1px;
  }

  /* Everything below flips a colour that was chosen against a light surface
     and is now sitting on a dark one. */

  /* The zoom buttons and trashcan are on the workspace, which is dark now. */
  .blocklyZoom:focus-visible > .blocklyFocusRing,
  .blocklyTrash:focus-visible > .blocklyFocusRing {
    stroke: #ffffff;
    outline-color: #11161a;
  }

  /* Used for the flyout's labels and the workspace selection ring, both of
     which are on dark surfaces here. The block and field rules set their own
     colours explicitly and are unaffected. */
  .injectionDiv.injectionDiv.injectionDiv {
    --blockly-active-node-color: #ffffff;
    --blockly-active-tree-color: #62c3ea;
  }

  /* #4b5560 on a dark workspace is 2.4:1; hover has to go the other way. */
  .blocklyScrollbarBackground:hover + .blocklyScrollbarHandle,
  .blocklyScrollbarHandle:hover,
  .blocklyFlyout .blocklyScrollbarBackground:hover + .blocklyScrollbarHandle,
  .blocklyFlyout .blocklyScrollbarHandle:hover {
    fill: #a9b6c0;
  }

  /*
   * The zoom buttons and trashcan, for the same reason and in the same
   * direction. Darkening a #888 sprite is what makes it readable on a white
   * workspace and what buries it on a dark one — at brightness(0.8) they
   * measured 3.5:1, over the line but not by much. Lightening instead puts
   * them at 8.5:1, and emphasis has to follow: on a dark workspace more
   * attention means lighter, not darker.
   */
  .blocklyZoom > image,
  .blocklyZoom > svg > image,
  .blocklyTrash > image,
  .blocklyTrashLid {
    filter: brightness(1.3);
  }

  .blocklyZoom > image:hover,
  .blocklyZoom > svg > image:hover,
  .blocklyZoom:focus > image,
  .blocklyZoom:focus > svg > image,
  .blocklyTrash:hover > image,
  .blocklyTrash:focus > image,
  .blocklyTrash:hover > .blocklyTrashLid,
  .blocklyTrash:focus > .blocklyTrashLid,
  .blocklyTrash.blocklyTrashFull > image,
  .blocklyTrash.blocklyTrashFull > .blocklyTrashLid {
    filter: brightness(1.7);
  }

  .blocklyZoom > image:active,
  .blocklyZoom > svg > image:active {
    filter: brightness(2);
  }
}

/* --------------------------------------------------------------------------
   More contrast, when the system asks for it
   --------------------------------------------------------------------------
   Last in the file on purpose: these override element rules defined above,
   and at equal specificity the later rule wins. Written higher up, the
   border widths lost to `button { border: 1px solid }` and the whole block
   did nothing but change two colours.
   -------------------------------------------------------------------------- */

/*
 * When the operating system is asked for more contrast, give it.
 *
 * The palette above is chosen to pass AA everywhere, which is a floor, not an
 * answer: a student who has turned this on at the system level has told us
 * that AA is not enough for them. Borders go darker and thicker, muted text
 * stops being muted, and the focus ring grows — the three things that are
 * deliberately quiet in the default theme and are the first to disappear.
 */
@media (prefers-contrast: more) {
  :root {
    --muted: #24303a;
    --line: #12171c;
  }

  @media (prefers-color-scheme: dark) {
    :root {
      --muted: #f2f7fa;
      --line: #e8eef2;
    }
  }

  button, select, input, textarea, .log, .commentary, .commentary-transcript {
    border-width: 2px;
  }

  :focus-visible {
    outline-width: 4px;
  }

  /*
   * The workspace, which otherwise never hears the request.
   *
   * prefers-contrast is the only OS-level contrast signal that reaches a
   * browser on ChromeOS, macOS, iOS or Android — forced-colors answers
   * Windows and essentially nothing else, so a workspace that responds only
   * to forced-colors is a workspace that responds to none of the machines
   * this is used on. Blockly has no opinion about any of it, so the rules
   * are here.
   */
  .blocklyScrollbarHandle,
  .blocklyFlyout .blocklyScrollbarHandle {
    fill: #2b3640;
  }

  /* Blockly writes the category stripe into the element's style attribute, so
     this is the one place in this file where !important earns its keep: there
     is no selector that beats an inline declaration without it. */
  .blocklyToolboxCategory {
    border-left-color: #12171c !important;
  }

  /*
   * Darker blocks still.
   *
   * The palette is already darkened for everyone — see setHsvValue in
   * blockly.js — so this is the extra step for a reader who has asked the
   * system for more, not the fix for the default. The filter is on the
   * block's path and not on its group: on the group it would darken the
   * label along with the block and gain nothing.
   *
   * Both focused rules are restated because filter does not accumulate. A
   * second declaration replaces the first, so without these the ring's halo
   * would simply vanish exactly where it is needed most.
   */
  .blocklyPath {
    filter: brightness(0.85);
  }

  .injectionDiv .blocklySelected:not(.blocklyShadow) > .blocklyPath.blocklyPath.blocklyPath,
  .injectionDiv :not(.blocklyShadow) > .blocklyActiveFocus.blocklyPath.blocklyPath.blocklyPath {
    filter: brightness(0.85) drop-shadow(0 0 1.5px #111111) drop-shadow(0 0 1.5px #111111);
  }

  .injectionDiv .blocklySelected.blocklyShadow > .blocklyPath.blocklyPath.blocklyPath,
  .injectionDiv .blocklyShadow > .blocklyActiveFocus.blocklyPath.blocklyPath.blocklyPath {
    filter: brightness(0.85) drop-shadow(0 0 1.5px #ffffff) drop-shadow(0 0 1.5px #ffffff);
  }

  /* A heavier selection and focus ring to go with it. */
  .injectionDiv.injectionDiv.injectionDiv {
    --blockly-selection-width: 4px;
  }
}
