/* ============================================================
   AccessibleIDE - Styles
   WCAG 2.1 AA: high contrast, large targets, clear focus states
   Calm, low-distraction design for dyslexic & neurodivergent users
   ============================================================ */

/* The browser hides [hidden] with a UA rule that any author `display`
   beats. Several things here are toggled by the `hidden` attribute --
   the update download row, the error panel, the setup wizard's steps --
   and the ones that also carry a `display` would silently stay visible
   forever. This is the standard guard, and it has to come before them. */
[hidden] {
  display: none !important;
}

:root {
  --bg: #0d0d0d;
  --fg: #ffffff;
  --accent: #ffd93d;
  --accent-fg: #0d0d0d;
  --panel-bg: #1a1a1a;
  --panel-border: #333333;
  --muted: #aaaaaa;
  --error-bg: #2b0a0a;
  --error-border: #ff6b6b;
  --error-fg: #ffb3b3;
  --error-line: rgba(255, 107, 107, 0.16);
  --focus-ring: #ffd93d;
  /* The default face. OpenDyslexic is the font this app exists for, so it
     is what a reader who has not chosen anything gets. It ships Regular
     and Bold, and the weight 600 the buttons ask for resolves to the real
     Bold face rather than a synthesised one: CSS takes the nearest face at
     or above the requested weight, so a declared bold is used, not faked.
     Atkinson and Nunito stay in the stack so a reader who picked either
     still gets it back before the script fallbacks are reached. */
  --font-family: "OpenDyslexic3", "OpenDyslexic", "Atkinson Hyperlegible", "Nunito", "Mukta", "Almarai", sans-serif;
  /* Code areas that are not the editor. This was "Courier New" hardcoded in
     seven places, which is a 1980s screen face: thin strokes, wide gaps, and
     the first thing on the page that reads as unfinished. Consolas ships with
     every copy of Windows, so the installer and the hosted site both get a
     real monospace with nothing downloaded. */
  --font-mono: "Consolas", "Cascadia Mono", ui-monospace, "Mukta", "Almarai", monospace;
  /* Which font each part of the window is drawn in. Every one of these
     starts out pointing at a default above, so nothing moves until the
     reader ticks that area for the font they chose. app.js writes the
     chosen family onto the token, and takes it off again when the area is
     unticked, which is what makes an unticked area fall back to the
     default named here instead of having to be told that default by name.
     The two transcripts are listed separately rather than sharing one
     token, so the output and the shell can each be decided on their own. */
  --font-ui: var(--font-family);
  --font-code: var(--font-family);
  --font-output: var(--font-mono);
  --font-shell: var(--font-mono);
  --font-size: 16px;
  /* How heavy the reading text is. 400 is the normal weight every font
     has; the reader can move it up or down from Settings. */
  --font-weight: 400;
  --line-height: 1.6;
  --letter-spacing: 0.5px;
  --radius: 14px;
  --radius-sm: 9px;

  /* ---------- Spacing scale ---------- */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 40px;

  /* ---------- Elevation (calm, dyslexia-friendly depth) ---------- */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.35);
  --shadow-2: 0 4px 14px rgba(0, 0, 0, 0.4);
  --shadow-3: 0 10px 30px rgba(0, 0, 0, 0.5);
  --ease: cubic-bezier(0.25, 0.6, 0.4, 1);
  --motion-fast: 120ms;
  --motion-med: 200ms;
  /* The far end of the background gradient: the theme's background
     washed 12% toward its foreground. Subtle on purpose - a gradient
     that competes with the code is the one thing it must not do. */
  --bg-grad: color-mix(in srgb, var(--bg) 88%, var(--fg));
}

/* ---------- Theme: High Contrast (default) ---------- */
body[data-theme="high-contrast"] {
  --bg: #0b0b0b;
  --fg: #ffffff;
  --accent: #ffd93d;
  --accent-fg: #0b0b0b;
  --panel-bg: #161616;
  --panel-border: #3c3c3c;
  --muted: #c9c9c9;
  --error-bg: #2a0d0d;
  --error-border: #ff6b6b;
  --error-fg: #ffc1c1;
  --error-line: rgba(255, 107, 107, 0.20);
  --focus-ring: #ffd93d;
}

/* ---------- Theme: Dark ---------- */
body[data-theme="dark"] {
  --bg: #17181c;
  --fg: #e6e6e6;
  --accent: #6bc1ff;
  --accent-fg: #0b1e2e;
  --panel-bg: #1f2126;
  --panel-border: #34373f;
  --muted: #9aa3ad;
  --error-bg: #2d1717;
  --error-border: #f48771;
  --error-fg: #ffb4a0;
  --error-line: rgba(244, 135, 113, 0.18);
  --focus-ring: #6bc1ff;
}

/* ---------- Theme: Pastel ---------- */
body[data-theme="pastel"] {
  --bg: #fbf6ec;
  --fg: #453f3a;
  /* Warm bronze accent. The foreground is deep brown, not white:
     white on this accent only reaches 3.61:1 and fails WCAG AA. */
  --accent: #b07d2c;
  --accent-fg: #241804;
  --panel-bg: #f2ecdf;
  --panel-border: #d9cfb8;
  /* Darkened from #7d7468 (4.27:1) to clear 4.5:1 on both bg and panel. */
  --muted: #736a5f;
  --error-bg: #fbe9e9;
  --error-border: #c0523f;
  --error-fg: #8f2f1a;
  --error-line: rgba(192, 82, 63, 0.14);
  --focus-ring: #b07d2c;
}

/* ---------- Theme: Light ---------- */
body[data-theme="light"] {
  --bg: #fcfcfc;
  --fg: #2b2b2b;
  --accent: #0057b8;
  --accent-fg: #ffffff;
  --panel-bg: #f2f2f2;
  --panel-border: #d9d9d9;
  --muted: #5f5f5f;
  --error-bg: #fdeaea;
  --error-border: #cc0000;
  --error-fg: #8f1a1a;
  --error-line: rgba(204, 0, 0, 0.12);
  --focus-ring: #0057b8;
}

/* ---------- Base ---------- */
* { box-sizing: border-box; }

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

body {
  background: linear-gradient(180deg, var(--bg) 0%, var(--bg-grad) 100%);
  color: var(--fg);
  font-family: var(--font-ui);
  font-size: var(--font-size);
  font-weight: var(--font-weight);
  line-height: var(--line-height);
  letter-spacing: var(--letter-spacing);
  display: flex;
  flex-direction: column;
  transition: background 0.2s ease, color 0.2s ease;
}

/* The gradient is a choice, not a default the reader has to live with.
   Off means a flat background, exactly as the theme table paints it. */
body[data-gradient='off'] {
  background: var(--bg);
}

/* ---------- Accessibility helpers ---------- */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: -100px;
  left: 8px;
  z-index: 1000;
  background: var(--accent);
  color: var(--accent-fg);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  font-weight: bold;
  text-decoration: none;
}

.skip-link:focus {
  top: 8px;
}

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

/* ---------- Top bar ---------- */
.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--panel-bg);
  border-bottom: 1px solid var(--panel-border);
  box-shadow: var(--shadow-1);
}

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

/* The mark is the OpenDyslexic "a" - the letterform this app exists for -
   in the accent, on a neutral square. The accent is a mark here, not a
   fill: the square is the same surface as the top bar, so the logo reads
   as part of the chrome rather than a sticker on it. The "a" is large
   text (bold, ~21px), so the accent clears 3:1 even in the pastel theme,
   where the accent is darkest. */
.brand-logo {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  background: var(--panel-bg);
  color: var(--accent);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
  font-family: var(--font-family);
  font-size: 1.35em;
  font-weight: bold;
  line-height: 1;
  flex-shrink: 0;
}

/* The mark is drawn here, not in the template: it is decoration with no
   information the words do not carry, so it is aria-hidden and lives in
   CSS where the i18n test cannot mistake it for untranslated text. */
.brand-logo::before {
  content: "a";
}

.brand-name {
  margin: 0;
  font-size: 1.3em;
  font-weight: bold;
  letter-spacing: normal;
}

/* The version is information, not an action, so it is muted rather than
   accent-filled: the accent is reserved for things the reader can do.
   Muted clears 4.5:1 on the panel surface in every theme. */
.version-badge {
  font-size: 0.72em;
  background: var(--panel-bg);
  color: var(--muted);
  border: 1px solid var(--panel-border);
  padding: 2px 10px;
  border-radius: var(--radius-sm);
  font-weight: bold;
  letter-spacing: normal;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}

.tool-group {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-left: var(--space-3);
  border-left: 1px solid var(--panel-border);
}

.tool-group:first-of-type {
  border-left: none;
  padding-left: 0;
}

.tool-select {
  font-family: inherit;
  font-size: 0.95em;
  padding: var(--space-2) var(--space-3);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--fg);
  cursor: pointer;
  min-height: 40px;
  transition: border-color 0.2s ease;
}

.tool-select:hover {
  border-color: var(--accent);
}

.tool-label {
  font-size: 0.85em;
  color: var(--muted);
  min-width: 40px;
  text-align: center;
  letter-spacing: normal;
}

input[type="range"] {
  width: 120px;
  accent-color: var(--accent);
  cursor: pointer;
}

/* ---------- Buttons ---------- */
/* The base owns the appearance, so a button written as class="btn" still
   looks like a control. It used to set only padding and a transparent
   border and left the rest to the variants, which meant the one bare
   .btn in the template rendered as invisible text with no box around it. */
.btn {
  font-family: inherit;
  font-size: 0.95em;
  font-weight: 600;
  /* A row that holds an icon and a label, so the gap is one value for every
     button rather than spacing on the icon that only the icon buttons get. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  background: var(--bg);
  color: var(--fg);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  /* 44px is the height every other control in the app already uses: the
     selects, the steppers, the TTS toggle. It used to be 40 here and the
     setup screen had to override it back up to 44, which is the same
     control at two heights depending on where it sits. */
  min-height: 44px;
  /* Flat on purpose: the 2px border is the affordance, not a shadow.
     A shadow on every button is the stock-UI default, and the design
     bar rules out thin borders paired with wide shadows. Elevation is
     reserved for the top bar and the dialogs, which sit above the page. */
  /* Press feedback is a settle, not a bounce: dyslexia users rely on
     steady, predictable chrome, so we translate rather than scale. */
  transition: transform var(--motion-fast) var(--ease),
              background var(--motion-med) var(--ease),
              border-color var(--motion-med) var(--ease);
}

/* The icon box. It does not shrink, so a long label in a narrow toolbar
   cannot squeeze the glyph, and the SVG takes its size from the button's
   own font size so a small button gets a small icon without a second rule. */
.btn-icon {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}

.btn-icon svg {
  width: 1.15em;
  height: 1.15em;
  display: block;
}

/* The label is a plain span so it carries the text the tests look for and
   the words still wrap as words if the button is ever given a narrow width. */
.btn-label {
  white-space: nowrap;
}

.btn:hover {
  border-color: var(--accent);
}

.btn:active {
  transform: translateY(1px);
}

/* One elevation for every button. The primary used shadow-2 on top of a
   2px border, which is the thin-border-plus-wide-shadow combination the
   design bar rules out, and it made the Run button the only control that
   looked lifted. The accent fill is what marks it as the action. */
.btn-primary {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
}

.btn-toggle.active,
.btn-toggle.active:hover {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
}

/* The smaller variant steps down in size only. It takes its colours from
   the base like everything else, so a small button and a normal one are
   the same control at two sizes rather than two controls. */
.btn-small {
  font-size: 0.85em;
  padding: var(--space-1) var(--space-3);
  min-height: 40px;
}

/* The armed state of a two-step destructive action. The error tokens are
   the ones the app already uses for "this needs attention", so the button
   says in colour what its label says in words. */
.btn.is-armed,
.btn.is-armed:hover {
  background: var(--error-bg);
  color: var(--error-fg);
  border-color: var(--error-border);
}

/* ---------- Workspace ---------- */
/* A split view: the editor on the left, and a column on the right holding
   the output and, when it is open, the shell. The widths and heights are
   CSS variables written by app.js when a divider is dragged, so the layout
   itself stays in the stylesheet and a narrow screen can fall back to
   stacked panes without the script knowing about it. */
.workspace {
  flex: 1;
  display: flex;
  flex-direction: row;
  min-height: 0;
}

.editor-pane {
  flex: 0 0 var(--editor-width, 50%);
  min-width: 0;
  min-height: 0;
  position: relative;
  /* A column, because the pane now carries its own small toolbar above the
     editor. The editor used to be absolutely positioned to fill the pane;
     as a flex child it takes the space that is left rather than all of it. */
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

/* ---------- Trace table ---------- */
/* The table a reader opens after a run, one row per change to a variable, on
   the line that made the change. It is a native dialog so Escape and focus
   come for free, but it is the one dialog here that is shown non-modally:
   checking a row against the line that produced it means reading the code at
   the same time, and a modal makes the page behind it inert.

   It floats free of the layout, so opening it cannot reflow the code
   underneath, and it is moved by transform only. That last part is not
   decoration: a drag writes a transform on every pointer move, so animating
   top or left would relayout the page at pointer speed, and the animation
   below is opacity-only for the same reason. An animated transform would hold
   the value for the duration and the window would refuse to be dragged. */
.trace-window {
  position: fixed;
  inset-block-start: calc(var(--space-7) + var(--space-4));
  inset-block-end: auto;
  inset-inline-start: auto;
  inset-inline-end: var(--space-5);
  /* Cleared because the UA centres a dialog with margin: auto, and a window
     that starts centred then jumps on the first drag is a worse start than
     one that starts where it will stay. */
  margin: 0;
  padding: 0;
  width: min(560px, calc(100vw - var(--space-5) * 2));
  max-height: min(70vh, 560px);
  /* No display here. The UA hides a closed dialog with
     `dialog:not([open]) { display: none }`, and an author `display` beats the
     UA rule no matter how the selectors compare, so setting it here would
     leave the empty table floating on screen before any run, and draggable by
     a pointer that can reach it. It is set on [open] below instead. */
  /* Opaque on purpose, and deliberately not in the glass list with .settings
     and .setup-panel. That list's own rule is that nothing needing to be read
     may sit directly on a translucent surface, and this surface is nothing but
     a dense table of values: the code behind it must not show through them. */
  background: var(--panel-bg);
  color: var(--fg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-3);
}

.trace-window[open] {
  /* Flex only while open, so the closed state stays display: none. */
  display: flex;
  flex-direction: column;
  animation: trace-arrive var(--motion-fast) var(--ease) both;
}

@keyframes trace-arrive {
  from { opacity: 0; }
  to { opacity: 1; }
}

.trace-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
}

.trace-heading {
  flex: 1 1 auto;
  margin: 0;
  font-size: 1em;
  font-weight: 600;
  line-height: 1.4;
}

.trace-grip,
.trace-close {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The same 44px target every other control in the app gets. A drag handle
     smaller than that is one a tremor cannot take hold of. */
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  background: transparent;
  color: var(--muted);
  border: 0;
  border-radius: var(--radius-sm);
  font: inherit;
  cursor: pointer;
}

.trace-grip {
  cursor: grab;
}

.trace-grip:active {
  cursor: grabbing;
}

.trace-grip:hover,
.trace-close:hover {
  color: var(--fg);
}

.trace-grip:focus-visible,
.trace-close:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.trace-hint,
.trace-note {
  margin: 0;
  padding: var(--space-1) var(--space-4) var(--space-2);
  color: var(--muted);
  line-height: 1.5;
}

.trace-note {
  color: var(--fg);
  font-weight: 600;
}

.trace-scroll {
  flex: 1 1 auto;
  /* Without this a flex child refuses to shrink past its content, and a long
     table would push the dialog off the bottom of the screen instead of
     scrolling inside it. */
  min-height: 0;
  overflow: auto;
  border-top: 1px solid var(--panel-border);
}

.trace-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono);
}

.trace-table th,
.trace-table td {
  padding: var(--space-2) var(--space-4);
  text-align: start;
  vertical-align: top;
  border-bottom: 1px solid var(--panel-border);
}

.trace-table thead th {
  /* Held in place while the rows scroll, so somebody following one variable
     down a long table does not lose the column names on the way. */
  position: sticky;
  top: 0;
  background: var(--panel-bg);
  color: var(--muted);
  font-family: var(--font-ui);
  font-weight: 600;
}

.trace-line {
  width: 1%;
  white-space: nowrap;
  color: var(--muted);
}

.trace-value {
  /* A value can be one long token with nothing to break on, and it must wrap
     rather than push the column wider than the window. */
  overflow-wrap: anywhere;
}


/* Each pane carries one of these at its top: a single arrow that opens a row
   of compact sliders for that pane's own text size, line spacing and letter
   spacing. Closed it is one thin line, and open it is still one line, so a
   reader with code to read past pays for it once.
   The arrow is the only thing that moves, and it moves by transform, not by
   width or position: that keeps it off the layout, so opening the toolbar
   never reflows the line the reader was on. The global reduced-motion rule
   already stops it, so there is no second switch here. */
.pane-mini {
  flex: 0 0 auto;
  display: flex;
  /* The editor's strip now carries the line-number control next to the arrow
     and, when open, three sliders. Wrapping is what keeps that from being
     pushed off the edge of a narrow pane, where the reader would lose a
     control with no sign it had ever been there. */
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  /* The strip is as tall as the target inside it. A control smaller than the
     app's 44px would be the one button in the app a tremor cannot hit, and
     the reader reaches for this arrow without reading around it first. */
  min-height: 44px;
  padding: 0 var(--space-3);
  background: var(--panel-bg);
  border-bottom: 1px solid var(--panel-border);
  color: var(--muted);
  font-size: 0.85em;
  letter-spacing: normal;
}

.pane-mini-toggle {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 44px is the accessible minimum, so the arrow is usable with a finger,
     a tremor or a stylus - the same target the steppers and the buttons use. */
  min-width: 44px;
  min-height: 44px;
  margin-left: calc(-1 * var(--space-1));
  padding: 0;
  background: transparent;
  /* Full foreground rather than the strip's muted grey. The arrow is the only
     thing on a closed pane that says a toolbar is there at all, and at the
     muted grey and 0.7rem it was drawn at, readers told us they walked past
     it. Its own colour does not change on hover: what moves is the plate
     behind it, so the glyph never gets harder to see at the moment the
     reader is reaching for it. */
  color: var(--fg);
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--motion-fast) var(--ease);
}

.pane-mini-toggle:hover,
.pane-mini-toggle:focus-visible,
.pane-mini-toggle[aria-expanded="true"] {
  background: var(--bg);
}

.pane-mini-toggle:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.pane-caret {
  display: block;
  font-size: 1rem;
  line-height: 1;
  transition: transform var(--motion-fast) var(--ease);
}

.pane-caret::before {
  content: "\25B8";
}

.pane-mini-toggle[aria-expanded="true"] .pane-caret {
  transform: rotate(90deg);
}

/* The line-number tick. One control, because the setting has one thing to
   say: the numbers are showing, or they are not. It lives in the pane's strip
   above the code, so it is reachable without opening the sliders first, and
   it is drawn small on purpose: it sits beside the code a reader is reading,
   and should be findable rather than loud. The label is bound to the box, so
   the label is what carries the target the box itself is too small to carry. */
.pane-gutter {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
}

.pane-gutter-check {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.pane-gutter-label {
  color: var(--fg);
  font-size: 0.75rem;
  white-space: nowrap;
  cursor: pointer;
}

/* The fade tick and its slider, in the same strip as the line-number tick and
   drawn the same size, because they are the same kind of decision about the
   same code. The slider is only in the strip while the tick is on, so a reader
   who does not use it is not handed a control they have to read past. */
.pane-fade {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
}

.pane-fade-check {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.pane-fade-label {
  color: var(--fg);
  font-size: 0.75rem;
  white-space: nowrap;
  cursor: pointer;
}

.pane-fade-amount {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

/* display:flex above would otherwise beat the hidden attribute's own
   display:none, and the slider would show while fade is off. */
.pane-fade-amount[hidden] {
  display: none;
}

.pane-fade-slider {
  width: 88px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* The readout sits to the right of the thumb and keeps a fixed width, so
   moving the slider does not push the rest of the strip sideways. */
.pane-fade-value {
  min-width: 4ch;
  color: var(--muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.pane-mini-panel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}

/* display:flex above would otherwise beat the hidden attribute's own
   display:none, and the panel would show while it is meant to be closed. */
.pane-mini-panel[hidden] {
  display: none;
}

.mini-field {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
}

/* The glyph is a label, not a word, so it is drawn here and stays out of the
   catalogues. It sits in a fixed box so the three sliders in a row line up
   under each other rather than starting wherever the glyph is wide. */
.mini-icon {
  width: 1.1em;
  font-size: 0.95em;
  line-height: 1;
  text-align: center;
  color: var(--muted);
}

.pane-mini input[type="range"] {
  width: 96px;
  min-width: 0;
}

/* The value reads as a number, so the digits are held to one width and the
   slider does not shuffle sideways as the number changes length. */
.mini-value {
  min-width: 3.1em;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--fg);
}


.right-column {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.editor {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  font-size: var(--font-size);
  font-weight: var(--font-weight);
  line-height: var(--line-height);
  letter-spacing: var(--letter-spacing);
}

/* CodeMirror overrides */
.CodeMirror {
  height: 100%;
  font-family: var(--font-code) !important;
  font-size: var(--font-size) !important;
  font-weight: var(--font-weight) !important;
  line-height: var(--line-height) !important;
  letter-spacing: var(--letter-spacing) !important;
}

.CodeMirror-lines {
  padding: var(--space-3) 0;
}

/* Gutter: keep line numbers aligned with the code.
   Letter-spacing must NOT apply here, or the numbers drift
   and overlap the code. */
.CodeMirror-gutters {
  border-right: 1px solid var(--panel-border);
  letter-spacing: normal !important;
}

.CodeMirror-linenumber {
  padding: 0 var(--space-2) 0 var(--space-3);
  color: var(--muted);
}

/* CodeMirror's vendor stylesheet hardcodes the active line to a pale
   blue (#e8f2ff), which glares on the dark themes. Tie it to the
   panel color so the caret line reads as a calm surface instead. */
.CodeMirror-activeline-background {
  background: var(--panel-bg);
}

/* Focus mode: hide gutter is handled natively in app.js via
   CodeMirror's lineNumbers option, NOT with CSS display:none.
   Hiding the gutter with CSS leaves CodeMirror's internal width
   math stale, which makes the line numbers ghost over the code. */

/* Focus mode: fade the other lines only while the pointer rests on one
   line. At rest every line is fully legible, so the page is calm; hovering
   singles one line out and fades the rest. The line number fades with its
   line, so the gutter does not stay loud while the code goes quiet.
   --blur-amount is written by app.js from the "How much to fade" slider.
   It runs the other way round from the slider: 1 is no fade and 0.15 is the
   most a line is ever taken down. So the bottom of the slider is off, and
   the floor below keeps a hand-edited config from making the other lines
   invisible. */
body[data-focus-mode="lines"] .CodeMirror-line,
body[data-focus-mode="lines"] .CodeMirror-linebackground,
body[data-focus-mode="lines"] .CodeMirror-gutter-wrapper {
  transition: opacity var(--motion-med) var(--ease);
}

body[data-focus-mode="lines"] .CodeMirror-lines:hover .CodeMirror-line,
body[data-focus-mode="lines"] .CodeMirror-lines:hover .CodeMirror-linebackground,
body[data-focus-mode="lines"] .CodeMirror-lines:hover .CodeMirror-gutter-wrapper {
  opacity: clamp(0.15, var(--blur-amount, 0.5), 1);
}

body[data-focus-mode="lines"] .CodeMirror-lines:hover .CodeMirror-line:hover,
body[data-focus-mode="lines"] .CodeMirror-lines:hover .CodeMirror-code > div:hover .CodeMirror-linebackground,
body[data-focus-mode="lines"] .CodeMirror-lines:hover .CodeMirror-code > div:hover .CodeMirror-gutter-wrapper {
  opacity: 1;
}

/* ---------- Output pane ---------- */
.output-pane {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--panel-bg);
}

.output-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--panel-border);
}

.output-title {
  margin: 0;
  font-size: 0.9em;
  font-weight: bold;
  color: var(--muted);
  letter-spacing: normal;
}

.output {
  flex: 1;
  margin: 0;
  padding: var(--space-3) var(--space-4);
  overflow: auto;
  font-family: var(--font-output);
  font-size: 0.95em;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--fg);
  /* The output takes the reading setting the way the shell transcript beside
     it already does, so a reader who widened their letter spacing gets it on
     their program's output too. A pane given its own spacing overrides this
     with an inline value. */
  letter-spacing: var(--letter-spacing);
}

/* ---------- Error panel ---------- */
.error-panel {
  margin: 0 var(--space-4) var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--error-bg);
  border: 2px solid var(--error-border);
  border-radius: var(--radius-sm);
  color: var(--error-fg);
  box-shadow: var(--shadow-1);
}

.error-title {
  margin: 0 0 var(--space-1);
  font-size: 1em;
  font-weight: bold;
  letter-spacing: normal;
  /* The title is a warning glyph and a word on one line. The gap lives here
     rather than on the SVG so the icon keeps currentColor from the heading. */
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* The warning triangle in the title. Sized against the heading's own font
   size, and it inherits the error colour through currentColor. */
.error-icon {
  flex: 0 0 auto;
  width: 1.15em;
  height: 1.15em;
  display: block;
}

.error-message {
  margin: 0;
  font-size: 1em;
  line-height: 1.5;
  letter-spacing: normal;
}

/* Error line highlight in the editor. The tint is themed rather than a
   fixed salmon so it stays quiet on light themes and visible on dark. */
.cm-error-line {
  background: var(--error-line) !important;
  box-shadow: inset 3px 0 0 var(--error-border);
}

/* ---------- The Python shell ---------- */
/* On the right of the editor, below the output, which is where somebody
   who has just run something and wants to try a variation of it will look
   next. Hidden by default because a page that has not asked for a shell
   should not pay for one.

   It is drawn as a terminal: the transcript and the line you type on share
   one surface, in the editor's own monospace, so it looks like the output
   pane above it with a prompt at its foot. It opens at half the column, so
   the output and the shell sit one above the other. */
.shell-pane {
  flex: 0 0 var(--shell-height, 50%);
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--panel-bg);
}

/* Clear and close only. No title and no explanation: the prompt below says
   what the pane is better than a label would. */
.shell-bar {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-bottom: 1px solid var(--panel-border);
}

/* The transcript. It fills the pane and scrolls; the prompt line below it
   stays put. The monospace and the line height come from the reader's
   settings like everywhere else, so the shell is as readable as the editor. */
.shell-output {
  flex: 1 1 auto;
  min-height: 0;
  margin: 0;
  padding: var(--space-3) var(--space-4) 0;
  overflow: auto;
  font-family: var(--font-shell);
  font-size: 0.95em;
  line-height: var(--line-height, 1.5);
  letter-spacing: var(--letter-spacing, normal);
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--fg);
  /* New output is appended constantly, and scroll anchoring would yank the
     view while somebody is reading further up the transcript. */
  overflow-anchor: none;
}

/* The line the reader types on, with the prompt in front of it. It carries
   no border of its own and shares the transcript's type and colour, so the
   two read as one surface, the way a terminal does.

   The output pane's answer row is this same line seen again: a question
   already printed above it and a place to answer it on. They share one rule
   on purpose, so that the two cannot drift into looking like different parts
   of the same app. */
.shell-form,
.answer-form {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-4) var(--space-3);
  font-family: var(--font-shell);
  font-size: 0.95em;
  line-height: var(--line-height, 1.5);
  letter-spacing: var(--letter-spacing, normal);
  color: var(--fg);
}

/* The prompt is punctuation, not prose, so it lives in the stylesheet and
   stays out of the markup and the catalogues. Its colour is the muted text
   colour, which is measured against the panel behind it like every other
   label in the app. */
.shell-prompt::before {
  content: ">>>";
  color: var(--muted);
}

.shell-input,
.answer-input {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  padding: 0 var(--space-1);
  border: 0;
  background: transparent;
  color: var(--fg);
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  letter-spacing: inherit;
}

/* The label on the answer row. It takes the row's own size rather than a
   smaller one: a label set below the thing it labels is the wrong way round,
   and it keeps the reader's letter spacing because they set that for reading.
   The weight and the muted colour are what mark it as a label. */
.answer-label {
  font-weight: bold;
  color: var(--muted);
}

/* ---------- Pane dividers ---------- */
/* The workspace is a split view, and each divider is a separator: a wide
   enough grab area for a shaky hand, a visible line, and a focus ring for
   the keyboard path. The line is the only decoration; the grab area around
   it is invisible so the panes keep their full width. */
.divider {
  flex: 0 0 12px;
  position: relative;
  touch-action: none;
  background: transparent;
}

.divider::after {
  content: '';
  position: absolute;
  background: var(--panel-border);
  transition: background var(--motion-fast) var(--ease);
}

.divider-vertical::after {
  top: 0;
  bottom: 0;
  left: 5px;
  width: 2px;
}

.divider-horizontal::after {
  left: 0;
  right: 0;
  top: 5px;
  height: 2px;
}

.divider:hover::after,
.divider:focus-visible::after {
  background: var(--accent);
}

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

.divider-vertical {
  cursor: col-resize;
}

.divider-horizontal {
  cursor: row-resize;
}

/* ---------- Footer ---------- */
.footer {
  padding: var(--space-2) var(--space-4);
  background: var(--panel-bg);
  border-top: 1px solid var(--panel-border);
  font-size: 0.85em;
  color: var(--muted);
  text-align: center;
  letter-spacing: normal;
}

.footer a {
  color: var(--accent);
  /* Always underlined: links must not rely on color alone to be
     distinguishable (WCAG 1.4.1 Use of Color). */
  text-decoration: underline;
  text-underline-offset: 2px;
}

.footer a:hover {
  text-decoration-thickness: 2px;
}

.quit-btn {
  margin-top: var(--space-2);
}

/* Text selection uses the accent at low strength so selected code stays
   legible instead of flipping to a high-glare block of color. */
::selection {
  background: var(--accent);
  color: var(--accent-fg);
}

/* ---------- Extra-high contrast ----------
   An opt-in boost for readers whose words still blend together. It only
   widens the gap between the text and its background; it never shifts
   the hue, so the theme stays recognisable. The code colours are handled
   in app.js so the editor and the chrome always agree. */
body[data-contrast="high"][data-theme="dark"],
body[data-contrast="high"][data-theme="high-contrast"] {
  --fg: #ffffff;
  --muted: #d4d4d4;
  --panel-border: #6a6a6a;
  --error-border: #ff9c9c;
  --error-fg: #ffdada;
}

body[data-contrast="high"][data-theme="light"],
body[data-contrast="high"][data-theme="pastel"] {
  --fg: #000000;
  --muted: #333333;
  --panel-border: #8a8a8a;
  --error-border: #8f0000;
  --error-fg: #5c0000;
}

/* ---------- Settings dialog ----------
   A native <dialog> so focus trapping, Escape and the inert background
   come from the browser rather than hand-rolled JavaScript. */
.settings {
  width: min(720px, calc(100vw - 2 * var(--space-4)));
  max-height: calc(100vh - 2 * var(--space-4));
  padding: 0;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
  box-shadow: var(--shadow-3);
  overflow: hidden;
}

.settings::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

/* The title across the top, then the sections down the left with the panel
   they point at filling the rest. The list runs down rather than across
   because a wrapped row of nine links is the thing that read as clutter:
   in a column each one is on its own line and can be found by position
   instead of by reading every one of them. */
.settings-form {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  max-height: calc(100vh - 2 * var(--space-4));
}

.settings-header {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: 1px solid var(--panel-border);
  background: var(--panel-bg);
}

/* Jump links to each group. The panel beside them scrolls, so these stay in
   view while the reader moves between sections, and they scroll themselves
   on a window too short to show all nine. */
.settings-nav {
  grid-column: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-4);
  border-right: 1px solid var(--panel-border);
  background: var(--panel-bg);
}

.settings-nav-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
  white-space: nowrap;
}

.settings-nav-list a {
  color: var(--accent);
  font-size: 0.9em;
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.settings-nav-list a:hover {
  text-decoration-thickness: 2px;
}

.settings-title {
  margin: 0;
  font-size: 1.25em;
  font-weight: bold;
  letter-spacing: normal;
}

/* The panel scrolls beside the section list, so the header and the status
   line stay put. */
.settings-body {
  grid-column: 2;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-4);
}

.settings-group {
  margin: 0 0 var(--space-5);
  padding: 0 0 var(--space-5);
  border: 0;
  border-bottom: 1px solid var(--panel-border);
}

.settings-group:last-of-type {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
}

.settings-legend {
  padding: 0;
  margin-bottom: var(--space-1);
  font-size: 1.05em;
  font-weight: bold;
  color: var(--accent);
  letter-spacing: normal;
}

.settings-hint,
.field-help {
  margin: 0 0 var(--space-2);
  font-size: 0.9em;
  line-height: 1.5;
  color: var(--muted);
  letter-spacing: normal;
}

.field {
  margin-bottom: var(--space-4);
}

.field:last-child {
  margin-bottom: 0;
}

.field > label,
.field-label-static {
  display: block;
  margin-bottom: var(--space-1);
  font-weight: 600;
  letter-spacing: normal;
}

/* The four places the chosen font can be used. Laid out as one wrapping row
   of ticks, because they are one decision with four answers rather than four
   separate settings. Each keeps a full-height hit area so it can be hit with
   a thumb as well as a pointer, the way the pane toolbars' ticks do. */
.font-areas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
}

.font-area {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 44px;
}

.font-area input[type="checkbox"] {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.font-area label {
  font-weight: 400;
  letter-spacing: normal;
  cursor: pointer;
}

.field select,
.field input[type="range"] {
  width: 100%;
}

.field select {
  font-family: inherit;
  font-size: 1em;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  background: var(--bg);
  color: var(--fg);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
}

/* Text fields were the one control in the app still drawn by the browser.
   That handed them the browser's own field background while their text
   inherited the page colour, which is the theme's foreground: on a dark
   theme that is light text on a light box, and the reader cannot see what
   they typed. They now take the same two tokens as the select above them,
   the pair the theme table is measured against, so they clear AA by the
   same check. The :where() adds no weight of its own, which leaves
   .hex-input free to keep the monospace face the colour fields want.

   The search box in the dictionary is in this list for the same reason. It
   is the same defect, and a search box whose text cannot be seen is a
   dictionary that cannot be searched. */
.field :where(input[type="text"], input[type="search"]) {
  font-family: inherit;
  font-size: 1em;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  background: var(--bg);
  color: var(--fg);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
}

.field :where(input[type="text"], input[type="search"]):hover {
  border-color: var(--accent);
}

.field :where(input[type="text"], input[type="search"]):focus-visible {
  border-color: var(--accent);
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.field select:hover {
  border-color: var(--accent);
}

.field select:focus-visible,
.field input[type="range"]:focus-visible {
  border-color: var(--accent);
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Slider and its live value sit on one line so the number never moves. */
.field-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.field-row input[type="range"] {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  accent-color: var(--accent);
  cursor: pointer;
}

/* A key press and the button that records a different one sit on one line,
   the same line pattern the slider above uses. The list takes the room and
   the button keeps its own width, so the button does not grow wider as the
   reader picks a longer name. The list is not full width here, which is what
   .field select asks for on its own: it shares the line. */
.key-row select {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
}

.key-row .btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* Space for one line, always taken. A message that arrived by being shown
   would push everything below it down while the reader is reading, so the
   line is held open whether it says anything or not. It matches the line the
   help text uses, being the same class at the same size. */
.key-status {
  min-height: 1.5em;
}

/* A ring is the conventional mark for "this waits for your press", which is
   what the button does. Drawn at the size of the words beside it rather than
   at a fixed number of pixels, so it grows with the reader's font size like
   every other mark in the app. The button's own words carry the meaning, so
   the ring is decoration and is hidden from assistive technology. */
.record-icon {
  width: 1em;
  height: 1em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

/* The same accent fill the toolbar toggles use when they are on, so a state
   that is happening now looks like the one state it is everywhere else
   rather than being invented a second time for these two buttons. */
.btn.is-recording,
.btn.is-recording:hover {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
}

/* A slider between two buttons, so the value can be notched along one step
   at a time. Dragging a thumb is a small target for a shaky hand and a poor
   one behind a screen magnifier, and the arrow keys only work once the
   slider itself has focus. The buttons are a second way in, not a
   replacement: the slider still drags, and still takes arrow keys. */
.stepper {
  display: flex;
  flex: 1;
  min-width: 0;
  align-items: center;
  gap: var(--space-1);
  padding: 2px;
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
  background: var(--bg);
}

.stepper:hover {
  border-color: var(--accent);
}

/* The focus ring goes on whichever control inside is being used, so a
   reader can always see which one the arrow keys are about to move. */
.stepper:focus-within {
  border-color: var(--accent);
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.stepper input[type="range"] {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  accent-color: var(--accent);
  cursor: pointer;
}

.stepper-btn {
  flex: 0 0 auto;
  /* 44px is the accessible minimum, so the button is usable with a
     finger, a tremor or a stylus. */
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  font-family: inherit;
  font-size: 1.25rem;
  line-height: 1;
  color: var(--fg);
  background: var(--panel-bg);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.stepper-btn:hover:not(:disabled) {
  border-color: var(--accent);
}

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

/* At the end of its travel a button is disabled rather than silently doing
   nothing, so a reader is told the value cannot go further this way. */
.stepper-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* ---------- Try it out panel ---------- */

.preview-block {
  margin-bottom: var(--space-4);
}

.preview-label {
  margin: 0 0 var(--space-1);
  font-weight: 600;
  letter-spacing: normal;
  color: var(--muted);
  font-size: 0.9em;
}

/* The preview is shown on the editor's real background and in the real
   text colour, both applied from app.js, so what the reader picks
   against here is exactly what they get in the code. The values below
   are only the pre-script fallback. */
.font-preview {
  margin: 0;
  padding: var(--space-3);
  min-height: 4.5em;
  display: flex;
  align-items: center;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  overflow-wrap: break-word;
}

.preview-status {
  margin: var(--space-2) 0 0;
  font-size: 0.9em;
  line-height: 1.5;
  color: var(--muted);
  letter-spacing: normal;
}

.field-error {
  margin: 0 0 var(--space-2);
  font-size: 0.9em;
  line-height: 1.5;
  font-weight: 600;
  color: var(--error-fg);
  letter-spacing: normal;
}

.hex-input {
  font-family: var(--font-mono);
  letter-spacing: normal;
  min-width: 0;
}

.field-row input[type="color"] {
  flex: 0 0 auto;
  width: 56px;
  height: 44px;
  padding: 2px;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.field-row input[type="color"]:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.field-value {
  flex: 0 0 auto;
  min-width: 4.5ch;
  padding: var(--space-1) var(--space-2);
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.95em;
  color: var(--muted);
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
}

/* A disabled field stays visible and legible; it is dimmed with opacity
   rather than the low-contrast placeholder grey, which would fail AA. */
.field.is-disabled .field-row,
.field.is-disabled .field-help {
  opacity: 0.6;
}

.field.is-disabled input[type="range"] {
  cursor: not-allowed;
}

.field-inline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.field-inline > label {
  margin-bottom: 0;
}

/* Switch: a real <button role="switch">, so it is reachable by keyboard
   and announced correctly. The track is decorative; the state lives in
   the accessible name and aria-checked. */
[role="switch"] {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-1) var(--space-3);
  background: var(--bg);
  color: var(--fg);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
  box-shadow: none;
  transition: background var(--motion-med) var(--ease),
              border-color var(--motion-med) var(--ease);
}

[role="switch"]:hover {
  border-color: var(--accent);
}

.switch-track {
  position: relative;
  display: inline-block;
  width: 48px;
  height: 26px;
  flex-shrink: 0;
  background: var(--panel-border);
  border-radius: 999px;
  transition: background var(--motion-med) var(--ease);
}

.switch-thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  background: var(--fg);
  border-radius: 50%;
  transition: transform var(--motion-med) var(--ease);
}

[role="switch"].active {
  background: var(--bg);
  color: var(--fg);
  border-color: var(--accent);
}

[role="switch"].active .switch-track {
  background: var(--accent);
}

[role="switch"].active .switch-thumb {
  transform: translateX(22px);
  background: var(--accent-fg);
}

.switch-text {
  min-width: 2.5ch;
  font-weight: 600;
}

.settings-status {
  margin: 0;
  padding: var(--space-2) var(--space-4);
  min-height: 1.5em;
  font-size: 0.9em;
  color: var(--muted);
  background: var(--panel-bg);
  border-top: 1px solid var(--panel-border);
}

.settings-status.is-error {
  color: var(--error-fg);
  background: var(--error-bg);
  border-top-color: var(--error-border);
}

/* ---------- Responsive ---------- */
@media (max-width: 768px) {
  .topbar {
    flex-direction: column;
    align-items: stretch;
  }
  .toolbar {
    margin-left: 0;
    justify-content: center;
  }
  .tool-group {
    border-left: none;
    padding-left: 0;
  }
  .brand {
    justify-content: center;
  }
  /* A phone has no width to spare for a split view, so the panes stack the
     way they always did: editor, output, shell. The dividers belong to the
     split layout and are hidden with it. */
  .workspace {
    flex-direction: column;
  }
  .editor-pane {
    flex: 1 1 auto;
  }
  .right-column {
    flex: 1 1 auto;
  }
  .output-pane {
    border-top: 1px solid var(--panel-border);
  }
  /* Only cap the output when the shell is also on screen. With the shell
     closed it is the only pane in the column, so it takes the whole of it
     instead of leaving the space below it empty. */
  .right-column:has(.shell-pane:not([hidden])) .output-pane {
    max-height: 45%;
  }
  .shell-pane {
    flex: 1 1 auto;
    max-height: 45%;
    border-top: 1px solid var(--panel-border);
  }
  .divider {
    display: none;
  }
  /* A phone has no width to spare for a column beside the sections, so the
     sections go back above them. They keep their own line each rather than
     wrapping back into the row that read as clutter. */
  .settings-form {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr);
  }
  .settings-header {
    grid-column: 1;
  }
  .settings-nav {
    grid-column: 1;
    overflow-y: visible;
    padding: var(--space-3) var(--space-4);
    border-right: 0;
    border-bottom: 1px solid var(--panel-border);
  }
  .settings-nav-list {
    gap: var(--space-2);
    white-space: normal;
  }
  .settings-body {
    grid-column: 1;
  }
}

/* ============================================================
   POLISH - calm depth and crafted control states.
   These selectors already have a home block above, so only the
   genuinely new component states live here. Dyslexia-first: clear
   targets, steady press states, no flashy motion
   (respects prefers-reduced-motion).
   ============================================================ */

/* ---------- Toolbar controls: bigger, calmer, clearer states ---------- */
.tool-select,
.tool-group select {
  font-family: inherit;
  font-size: 0.98em;
  padding: var(--space-2) var(--space-3);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--fg);
  cursor: pointer;
  min-height: 44px;
  transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
}

.tool-select:hover,
.tool-group select:hover {
  border-color: var(--accent);
}

.tool-select:focus-visible,
.tool-group select:focus-visible {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px var(--focus-ring);
}

/* ---------- Scrollbars: visible, dyslexia-friendly, calm ---------- */
::-webkit-scrollbar {
  width: 14px;
  height: 14px;
}

::-webkit-scrollbar-track {
  background: var(--panel-bg);
}

::-webkit-scrollbar-thumb {
  background: var(--panel-border);
  border-radius: 999px;
  border: 3px solid var(--panel-bg);
}

::-webkit-scrollbar-thumb:hover {
  background: var(--muted);
}

* {
  scrollbar-width: thin;
  scrollbar-color: var(--panel-border) var(--panel-bg);
}

/* ---------- Reduced motion ---------- */
/* Motion can be turned off two ways, and they are not the same setting.
 *
 * prefers-reduced-motion is the reader's operating system asking on their
 * behalf. It is honoured here, and it is honoured before any script runs,
 * so nothing moves on the way to the first paint.
 *
 * The body attribute is the choice made in Settings. It is a separate
 * switch, and it is deliberately able to say "no" to the media query: a
 * reader whose computer is set to reduce motion system-wide may still
 * want a hint of movement in this one app, and the reverse holds too.
 * Until they choose, app.js leaves the attribute on "unset" and the media
 * query has the final say.
 */
/* The "unset" qualifier is the whole reason the comment above is true. A
   reader who has deliberately answered "no, I want movement here" must not
   be overruled by the media query, or the switch would look broken in
   exactly the case where it matters most. With the attribute left on
   "unset" - which is what the server renders and what app.js leaves until
   a choice is made - the system still decides. */
@media (prefers-reduced-motion: reduce) {
  body[data-reduce-motion='unset'] *,
  body[data-reduce-motion='unset'] *::before,
  body[data-reduce-motion='unset'] *::after {
    transition: none !important;
    animation: none !important;
  }
}

body[data-reduce-motion='true'] *,
body[data-reduce-motion='true'] *::before,
body[data-reduce-motion='true'] *::after {
  transition: none !important;
  animation: none !important;
}

/* ============================================================
   Panel materials and movement
   Three optional materials for the chrome, plus some movement.
   All of it is off unless asked for.

   The rule that matters here is a negative one: the editor and
   the page behind the text never become translucent. Those are
   where the reading happens, and a see-through surface behind
   words is a contrast cost for no benefit. Only chrome is
   affected - the top bar, the panes and the dialogs.

   The names borrow from Windows' acrylic and mica, but these
   are imitations and are described as such in Settings. The
   real materials are a Windows composition API that a WebView2
   window cannot reach. What is here is CSS that looks the part:
   mica is quiet and nearly solid, acrylic is heavier, more
   colourful, and grained.
   ============================================================ */

/* The surfaces allowed to go translucent, named rather than
   picked up by a broad descendant rule, so the list stays
   auditable. If you add a surface here, check that nothing which
   needs to be read sits directly on it. */
body:not([data-glass-material='off']) :is(.topbar, .output-pane, .shell-pane,
                                            .error-panel, .settings,
                                            .setup-panel) {
  /* Defaults, and what a material that names no values falls back to. */
  --glass-alpha: 72%;
  --glass-blur: 16px;
  --glass-sat: 150%;

  /* A custom tint is mixed into the theme's own panel colour rather
     than replacing it. A tint is meant to shift the hue, not to take
     over, and mixing keeps the contrast anchor where the theme put it -
     so even a deliberately awful colour cannot leave the panel
     unreadable. */
  --glass-fill: color-mix(in srgb, var(--glass-tint, var(--panel-bg)) 45%, var(--panel-bg));

  /* The opaque declaration comes first on purpose: a browser that does
     not know color-mix() keeps a solid panel rather than losing its
     background entirely. Better plain than unreadable. */
  background: var(--panel-bg);
  background: color-mix(in srgb, var(--glass-fill) var(--glass-alpha), transparent);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
}

/* Each material only sets the three numbers that make it different, so
   the surface list above stays the single place that decides what is
   allowed to go translucent. */
body[data-glass-material='mica'] :is(.topbar, .output-pane, .shell-pane,
                                        .error-panel, .settings,
                                        .setup-panel) {
  /* Quiet and almost solid. Mica tints itself with whatever is behind
     it; there is no wallpaper behind a window, so it borrows the
     page's own background and stays subtle. */
  --glass-alpha: 88%;
  --glass-blur: 8px;
  --glass-sat: 112%;
}

body[data-glass-material='frosted'] :is(.topbar, .output-pane, .shell-pane,
                                          .error-panel, .settings,
                                          .setup-panel) {
  /* An even frost, no grain. The middle of the three. */
  --glass-alpha: 72%;
  --glass-blur: 16px;
  --glass-sat: 150%;
}

body[data-glass-material='acrylic'] :is(.topbar, .output-pane, .shell-pane,
                                          .error-panel, .settings,
                                          .setup-panel) {
  /* Heaviest, and the most see-through of the three. The grain below
     is what separates it from a merely blurry panel. */
  --glass-alpha: 60%;
  --glass-blur: 34px;
  --glass-sat: 175%;
}

/* Grain, on the surfaces large enough for it to read. Kept off the
   panes: a texture over scrolling output is noise for its own sake. */
body[data-glass-material='acrylic'] :is(.topbar, .settings, .setup-panel) {
  position: relative;
}

body[data-glass-material='acrylic'] :is(.topbar, .settings, .setup-panel)::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Flat to the eye and it does not move, so it adds no animation and
     no reading of its own. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)' opacity='0.055'/%3E%3C/svg%3E");
}

/* Dialogs sit over the page rather than inside the layout, so a thin
   fill needs its edge restated or the boundary disappears. */
body:not([data-glass-material='off']) :is(.settings, .setup-panel) {
  box-shadow: var(--shadow-3), 0 0 0 1px var(--panel-border);
}

/* Somebody who has asked their operating system for less transparency
   gets none of this, whatever the app is set to. The !important is the
   same reasoning as the reduced-motion block above: this has to win
   against the material rules, and in exactly the case where it matters. */
@media (prefers-reduced-transparency: reduce) {
  body:not([data-glass-material='off']) :is(.topbar, .output-pane, .shell-pane,
                                              .error-panel, .settings,
                                              .setup-panel) {
    background: var(--panel-bg) !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
  }

  body[data-glass-material='acrylic'] :is(.topbar, .settings, .setup-panel)::before {
    content: none !important;
  }
}

/* Movement, when it is wanted, is small: a few pixels and a fade. No
   scaling, no sliding across the screen, nothing that moves a long way
   past the eye. Long travel is what causes trouble, not motion itself. */
@keyframes panel-arrive {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

@keyframes dialog-arrive {
  from { opacity: 0; transform: translateY(6px) scale(0.99); }
  to   { opacity: 1; transform: none; }
}

@keyframes sweep-in {
  from { opacity: 0; transform: translateY(3px); }
  to   { opacity: 1; transform: none; }
}

dialog[open] { animation: dialog-arrive var(--motion-med) var(--ease) both; }
.error-panel:not([hidden]) { animation: panel-arrive var(--motion-med) var(--ease) both; }
.shell-pane:not([hidden]) { animation: panel-arrive var(--motion-med) var(--ease) both; }

/* This used to include .btn and .btn-toggle, which quietly replaced the
   translateY(1px) that .btn:active sets with a scale. The two rules
   disagreed about what a button does when pressed, and the one that won
   was the one with no comment explaining it. .btn keeps its own
   transform transition; the switch track is a slider and is not a button,
   so it still needs both of these. */
.switch-track:active { transform: scale(0.97); }
.switch-track { transition: transform var(--motion-fast) var(--ease); }

@media (prefers-reduced-motion: reduce) {
  body[data-reduce-motion='unset'] dialog[open] {
    animation: none !important;
  }
}

/* ============================================================
   First-run setup
   ============================================================
   Wider than Settings, because a font sample needs room to be
   judged honestly. Everything else -- colours, the focus ring,
   the 44px target -- is borrowed from the settings panel so the
   two screens feel like the same app.
   ============================================================ */

.setup {
  width: min(680px, calc(100vw - 2 * var(--space-4)));
}

/* The wizard has no section list, so it does not inherit the dialog's
   two-column grid. Borrowing it put the step content in the narrow right-hand
   column and left the footer beside it, so the question sat off to one side
   with the buttons next to it instead of under it. One column, in the order
   they are read; the step keeps the height so the footer stays at the foot. */
.setup .settings-form {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
}

.setup .settings-body {
  grid-column: 1;
}

/* The title is long in some languages, so the progress line drops
   under it rather than squeezing it into an unreadable column. */
.setup-header {
  flex-wrap: wrap;
  row-gap: var(--space-1);
}

.setup-progress {
  margin: 0;
  font-size: 0.95em;
  font-weight: bold;
  color: var(--fg);
  opacity: 0.85;
}

.setup-panel {
  margin: 0;
}

/* A step change fades the new question in rather than swapping it. The
   animation is opacity and transform only, so it cannot shift the layout,
   and the reduced-motion rules above turn it off entirely for a reader
   who asked for stillness. */
.setup-panel:not([hidden]) {
  animation: setup-enter var(--motion-med) var(--ease);
}

@keyframes setup-enter {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.setup-legend:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 4px;
}

/* ---------- The choice rows ---------- */

.setup-choices {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.setup-choice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-3);
  border: 2px solid var(--panel-border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  cursor: pointer;
}

/* The row is the target, not just the little radio in it, so a
   missed click still lands on the option the reader aimed at. */
.setup-choice:hover {
  border-color: var(--fg);
}

/* The focus ring is drawn on the row when the radio inside it takes
   focus. Without this the ring would sit on a 20px circle in the
   corner of a much larger clickable area, which misrepresents where
   the reader's attention has to go. */
.setup-choice:focus-within {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.setup-choice:has(input:checked) {
  border-color: var(--accent);
  background: var(--panel-bg);
}

.setup-choice input[type='radio'] {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  margin: 0;
  accent-color: var(--accent);
}

.setup-choice-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.setup-choice-name {
  font-weight: bold;
}

.setup-choice-note {
  font-size: 0.9em;
  opacity: 0.8;
}

/* The sample wraps rather than being clipped: a long localised
   string in a narrow font must not push the layout sideways. */
.setup-sample {
  display: block;
  font-size: 1.05em;
  line-height: 1.5;
  overflow-wrap: anywhere;
  color: var(--fg);
}

/* The theme swatch is a strip of the theme's own colours, drawn
   server-side from the same table the editor reads. Purely decorative,
   so the cells carry no text and need no contrast of their own. */
.setup-swatch {
  display: flex;
  gap: 2px;
  margin-top: var(--space-1);
}

.setup-swatch-cell {
  width: 22px;
  height: 22px;
  border-radius: 4px;
  border: 1px solid var(--panel-border);
}

/* ---------- The theme builder ---------- */

.theme-builder-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-3);
  margin: var(--space-3) 0;
}

.theme-builder-field {
  margin: 0;
}

/* ---------- The tour ---------- */

.setup-tour {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.setup-tour li {
  padding: var(--space-3);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  background: var(--panel-bg);
}

.setup-tour-name {
  margin: 0 0 var(--space-1);
  font-weight: bold;
}

.setup-tour-text {
  margin: 0;
  line-height: 1.6;
}

/* ---------- The Python dictionary ---------- */

/* Same construction as the settings dialog, because it is the same kind of
   thing: a panel somebody opens, reads and closes.

   Nothing here sets a font family or a size. The entries inherit the reading
   font, size and line spacing, and that inheritance is the reason the
   dictionary is in the app and not only in the README: a README is laid out
   by the browser or the code host, so a reader who needs OpenDyslexic, or a
   larger size, or more room between lines cannot have any of them there. */

.glossary {
  width: min(680px, calc(100vw - 2 * var(--space-4)));
  max-height: calc(100vh - 2 * var(--space-4));
  padding: 0;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
  box-shadow: var(--shadow-3);
  overflow: hidden;
}

/* display is set only while the panel is open. A bare `display` on the class
   would beat the browser's own `dialog:not([open]) { display: none }`, which
   is the only thing hiding a closed dialog, and the dictionary would sit on
   the page permanently. The [hidden] guard at the top of this file covers the
   attribute; a closed dialog has no attribute to cover. */
.glossary[open] {
  display: flex;
  flex-direction: column;
}

.glossary::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

/* Header, intro and search box stay put; only the entries scroll. A search
   box that scrolls away from the list it filters is worse than none. */
.glossary-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: 1px solid var(--panel-border);
  background: var(--panel-bg);
}

.glossary-intro {
  margin: 0;
  padding: var(--space-4) var(--space-4) 0;
}

.glossary-english {
  margin: 0;
  padding: var(--space-2) var(--space-4) 0;
  color: var(--muted);
}

.glossary-searchbar {
  padding: var(--space-4);
  border-bottom: 1px solid var(--panel-border);
}

.glossary-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 0 var(--space-4) var(--space-4);
}

.glossary-heading {
  margin: var(--space-5) 0 var(--space-2);
  font-size: 1.05em;
  font-weight: 700;
}

/* The first heading would otherwise carry the margin the scroll area's own
   padding already provides, and the panel would open on a gap. */
.glossary-section:first-child .glossary-heading {
  margin-top: var(--space-4);
}

.glossary-aside {
  margin: 0 0 var(--space-3);
  color: var(--muted);
}

/* A description list, because that is what an entry is: a term and what it
   means. Rows are separated by a line, not boxed. Forty cards in a column is
   the clutter this app exists to avoid, and a border round each one would
   also compete with the code inside it. */
.glossary-list {
  margin: 0;
}

.glossary-entry {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--panel-border);
}

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

/* The term is code, so it takes the reader's code font rather than the mono
   fallback the transcripts use. Somebody who put OpenDyslexic on the code
   area did it because they read code in it, and this panel is entirely code
   they are reading. Styled as code, not merely coloured as code: the entry
   has to look like something you could type. */
.glossary-term {
  font-family: var(--font-code);
  font-weight: 700;
}

.glossary-term code {
  font-family: inherit;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  padding: 1px var(--space-2);
}

.glossary-meaning code {
  font-family: var(--font-code);
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-1);
}

.glossary-meaning {
  margin: var(--space-1) 0 0;
}

.glossary-empty {
  margin: var(--space-4) 0 0;
  color: var(--muted);
}

/* ---------- Quitting ---------- */

/* One question, one way back, one way out. No display is set, for the reason
   given above the dictionary. */

.confirm {
  width: min(440px, calc(100vw - 2 * var(--space-4)));
  padding: var(--space-5);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--fg);
  box-shadow: var(--shadow-3);
}

.confirm::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

.confirm-body {
  margin: var(--space-3) 0 0;
}

.confirm-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

/* ---------- The bar along the bottom ---------- */

.setup-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-4);
  border-top: 1px solid var(--panel-border);
  background: var(--panel-bg);
}

.setup-nav {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-inline-start: auto;
}

/* The buttons carry the reader between the questions, so they have to be
   as easy to hit as the answer rows above them. This used to say that .btn
   was 40px and .btn-small was 32px, so the wizard carried its own override
   to push them to 44. The base .btn is 44px now, which is what every other
   control in the app uses, so the override is gone. Two rules for one
   height is how the topbar and the setup screen drifted apart to begin
   with. The wizard's own answer rows are still set below. */
