
  @font-face {
    font-display: swap;
    font-family: "EB Garamond";
    font-weight: 400 800;
    src: url("/assets/fonts/eb-garamond-5.3.0-cyrillic.woff2") format("woff2");
    unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
  }

  @font-face {
    font-display: swap;
    font-family: "EB Garamond";
    font-weight: 400 800;
    src: url("/assets/fonts/eb-garamond-5.3.0-cyrillic-ext.woff2") format("woff2");
    unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
  }

  @font-face {
    font-display: swap;
    font-family: "EB Garamond";
    font-weight: 400 800;
    src: url("/assets/fonts/eb-garamond-5.3.0-greek.woff2") format("woff2");
    unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
  }

  @font-face {
    font-display: swap;
    font-family: "EB Garamond";
    font-weight: 400 800;
    src: url("/assets/fonts/eb-garamond-5.3.0-greek-ext.woff2") format("woff2");
    unicode-range: U+1F00-1FFF;
  }

  @font-face {
    font-display: swap;
    font-family: "EB Garamond";
    font-weight: 400 800;
    src: url("/assets/fonts/eb-garamond-5.3.0-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
  }

  @font-face {
    font-display: swap;
    font-family: "EB Garamond";
    font-weight: 400 800;
    src: url("/assets/fonts/eb-garamond-5.3.0-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
  }

  @font-face {
    font-display: swap;
    font-family: "EB Garamond";
    font-weight: 400 800;
    src: url("/assets/fonts/eb-garamond-5.3.0-vietnamese.woff2") format("woff2");
    unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
  }

  @font-face {
    font-display: swap;
    font-family: "Inter";
    font-weight: 100 900;
    src: url("/assets/fonts/inter-5.3.0-cyrillic.woff2") format("woff2");
    unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
  }

  @font-face {
    font-display: swap;
    font-family: "Inter";
    font-weight: 100 900;
    src: url("/assets/fonts/inter-5.3.0-cyrillic-ext.woff2") format("woff2");
    unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
  }

  @font-face {
    font-display: swap;
    font-family: "Inter";
    font-weight: 100 900;
    src: url("/assets/fonts/inter-5.3.0-greek.woff2") format("woff2");
    unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
  }

  @font-face {
    font-display: swap;
    font-family: "Inter";
    font-weight: 100 900;
    src: url("/assets/fonts/inter-5.3.0-greek-ext.woff2") format("woff2");
    unicode-range: U+1F00-1FFF;
  }

  @font-face {
    font-display: swap;
    font-family: "Inter";
    font-weight: 100 900;
    src: url("/assets/fonts/inter-5.3.0-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
  }

  @font-face {
    font-display: swap;
    font-family: "Inter";
    font-weight: 100 900;
    src: url("/assets/fonts/inter-5.3.0-latin-ext.woff2") format("woff2");
    unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
  }

  @font-face {
    font-display: swap;
    font-family: "Inter";
    font-weight: 100 900;
    src: url("/assets/fonts/inter-5.3.0-vietnamese.woff2") format("woff2");
    unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
  }

  @font-face {
    font-display: swap;
    font-family: "Fira Code";
    font-weight: 300 700;
    src: url("/assets/fonts/firacode-6.2.0.woff2") format("woff2");
  }
@font-face {
  font-display: swap;
  font-family: "STIX Two Math";
  src: url("/assets/fonts/stix-two-math-5.3.0.woff2") format("woff2");
}
/*
 * The content layer: everything that styles authored content and exercise
 * forms. Loaded by the app shell *and* by the standalone content documents
 * served into iframes, so the two render identically.
 *
 * Assembled into a served stylesheet by ./styles.ts, which puts the generated
 * @font-face blocks above this file and the exercise-group and theory-panel
 * rules below it. Order matters at exactly one seam, documented there.
 */

/* The interface families are generated in ./ui-fonts (their URLs are hashed)
   and joined in above this file by ./styles.ts. They belong to this layer and
   not to the chrome one because a content document loads only this layer, and
   the widgets inside its shadow roots ask for Fira Code. */

/*
 * Every color in the application is one of these, and nothing outside this
 * block picks a color of its own: a rule elsewhere that wants a tint reaches
 * for color-mix over a token rather than respelling the token's channels at
 * an alpha.
 *
 * Each colour is written once, as a light-dark() pair, and `color-scheme:
 * light dark` at the bottom is what selects between the halves: the browser
 * takes the reader's OS preference, and a rule that declares `color-scheme`
 * further down the tree pins everything under it — the second half of every
 * pair, the widgets' shadow roots (color-scheme inherits across that
 * boundary) and the form controls the browser draws itself. That is the one
 * line an author who has moved the page's ground has to write:
 * `:root { color-scheme: light; }`. It used to be a second copy of this block
 * under `@media (prefers-color-scheme: dark)`, which nothing an author wrote
 * could switch off, so a white `body` under a dark desktop got cream ink.
 *
 * Warm charcoal at night rather than a neutral or an inversion: the light
 * palette's subject is paper, and paper in a dark room is warm and dim, not
 * grey. The dark ratios are tuned to sit near the light ones rather than as far
 * from the ground as they will go — --ink-muted lands at 4.76:1 on the
 * lightest ground it may be drawn on, against 4.58:1 in light, because muted
 * text that clears AA comfortably stops reading as muted.
 * tests/a11y/contrast.test.ts holds every one of these pairings, in both
 * halves.
 *
 * The exercise widgets do not read these. They read the `--exercise-*`
 * tokens the block after this one maps onto them, through the defaults the
 * exercise kit declares (`exercise-kit/tokens.css`), so that an author has a
 * named surface to restyle and a renamed palette token breaks nothing in a
 * shadow root. Elsewhere a `var(--token, <fallback>)` must repeat the light
 * half exactly; tests/a11y/tokens.test.ts enforces both rules.
 */
:root {
  /* Surfaces, from the ground the page sits on up to a control's own fill.
     Stacked the same way in both halves: --surface-soft is the recessed fill,
     a step *below* the sheet, and --control-surface the raised one — which in
     light coincides with --surface and in dark is what keeps a button from
     dissolving into the sheet it sits on. */
  --paper: light-dark(#f1eadf, #17140f);
  --paper-top: light-dark(#f5efe6, #1d1913);
  --surface: light-dark(#fbf7ef, #221d16);
  --surface-soft: light-dark(#f8f2e8, #1e1a13);
  /* The header and footer's fill, laid over content scrolling beneath. */
  --surface-translucent: color-mix(in srgb, var(--surface) 96%, transparent);
  /* A control's fill. The same value as --surface in light, and its own token
     anyway: these seven uses mean "the thing you press", and in dark a
     control has to sit visibly off the sheet instead of dissolving into it.
     A token whose only sin is coinciding in one palette still earns its
     keep in the other. */
  --control-surface: light-dark(#fbf7ef, #2a241b);
  --rule: light-dark(#d8d0c3, #3d352a);
  --rule-soft: light-dark(#e5ded3, #2e281f);
  --ink: light-dark(#16324a, #e9e1d3);
  /* Clears WCAG AA 4.5:1 on --surface (4.58:1) and --paper-top, which is
     where nearly all secondary text sits. It does *not* clear it on the
     warmer fills — --surface-soft, --paper, --rule-soft — so muted text
     drawn on one of those reaches for --ink-muted-strong. */
  --ink-muted: light-dark(#5f7388, #988e7e);
  /* --ink-muted for the warmer fills: same hue, dark enough for AA down to
     --rule-soft (4.9:1). Only for text that actually sits on one of them;
     used app-wide it reads as ordinary ink rather than as secondary. */
  --ink-muted-strong: light-dark(#4d5f72, #aca291);
  /* Decoration only — separators, a disclosure triangle. At ~2.8:1 on the
     lightest surface it is below AA, so nothing a reader has to read (or
     click) may be drawn in it; that is what --ink-muted is for. */
  --ink-faint: light-dark(#8797a7, #6e6557);
  --blue: light-dark(#0b66d8, #5b9bef);
  /* The emphatic blue: a hover, a pressed state, an active nav item. Named
     for the emphasis and not for the lightness, because under a dark palette
     more emphasis means lighter rather than darker. */
  --blue-strong: light-dark(#074f9f, #8fbcff);
  --blue-soft: light-dark(#eaf3ff, #16273d);
  --green: light-dark(#1b7048, #46b87c);
  --green-soft: light-dark(#edf9f2, #12281d);
  --red: light-dark(#b42318, #f58a7c);
  --red-soft: light-dark(#fff0ed, #2e1815);
  --gold: light-dark(#9a6400, #c9a043);
  --gold-soft: light-dark(#fff7df, #2b2110);
  /* Text drawn on a filled accent — a pressed chip, a selected option. One of
     two tokens that invert outright, which is the whole reason it has a name
     rather than a value: an accent light enough to read on charcoal cannot
     also carry white text. */
  --on-accent: light-dark(#ffffff, #17140f);
  /* What a dialog casts, at whatever alpha the rule wants. Its own token
     rather than --ink at an alpha, and the other one that inverts: a shadow
     derived from the ink would be a highlight at night. */
  --shadow-color: light-dark(#102235, #000000);
  /* The faint wash of colour up in the page's top right. Whole-colour rather
     than an alpha applied at the use site, because how much of it reads is a
     property of the ground it lies on and so belongs to the palette. It
     carries further on a dark ground than the same wash does on cream, but
     starts from so much less light that it needs the extra to read at all. */
  --body-glow: light-dark(
    color-mix(in srgb, var(--blue) 5.5%, transparent),
    color-mix(in srgb, var(--blue) 9%, transparent)
  );
  /* The scope lines a Fitch proof draws down its subproofs. Relative to the
     text it accompanies, so it follows the palette on its own; the widget
     reads it as --exercise-scope-line. */
  --fitch-scope: color-mix(in srgb, currentColor 72%, transparent);
  --logo-font: "EB Garamond", Garamond, "Iowan Old Style", Georgia, serif;
  /* Fira Code sits in the body stack, after every sans face and before the
     generic: prose here quotes ⊢ and ∀ mid-sentence, and Inter has neither
     (nor does any system sans reliably). It is reached for one character at a
     time, so a page of ordinary English never loads it — and a lesson that
     does quote an operator was going to load it for its formulas anyway. */
  --body-font:
    Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
    "Segoe UI", "Fira Code", sans-serif;
  /*
   * The face for everything that is language rather than prose about it: a
   * truth-table grid, a model's goal and its field labels, a translation box
   * and its readback, the proof editors, the theory panel, code listings, the
   * revision editor's source field.
   *
   * One token because there was one face's worth of intent and four spellings
   * of it — the grid drew its headings in Inter, the proof editors in Fira
   * Code, the tree widgets in a serif math face, and `code` in a fourth stack
   * with the same names in a different order. A student meeting the same
   * connective in three exercises met three different letters.
   *
   * Monospace is not only for the resemblance to source. A truth-table column
   * is a stack of one-character cells that has to read as a column, and a
   * proof editor is a line editor whose indentation is its subproof structure.
   *
   * Ligatures follow one line, and it is not the boundary between widgets —
   * it is between doing logic and quoting source.
   *
   * **Every exercise leaves them on.** Fira Code joins `->` into `→`, `/\`
   * into `∧`, `\/` into `∨` through `calt`, so a student typing ascii watches
   * it become the symbol they meant. The proof editors have always relied on
   * that; the grid, the model fields and the translation box now do too, which
   * is what stops the same connective from looking like two different things
   * in two exercises on one page. It is a glyph substitution and nothing more:
   * the DOM text, the `aria-label`s and anything copied out are still the
   * spelling the language declares.
   *
   * **Anything quoting source switches them off** — `code` and `pre` below,
   * the theory panel's `<pre>` along with them, and the revision editor's
   * source field over in `chrome.css`. A lesson explaining that you type a
   * hyphen and a greater-than cannot have the example drawn as an arrow, or
   * the sentence becomes its own counterexample; an author editing Markdown
   * needs the characters they actually typed. Those surfaces are about the
   * text, not about the logic it spells.
   */
  --mono-font: "Fira Code", ui-monospace, monospace;
  /* Horizontal padding shared by the header, page shell, and footer, so
     their content edges align. */
  --shell-gutter: clamp(1.25rem, 4vw, 3.75rem);
  /* How wide a page's content actually gets: the shell's cap less its two
     gutters. Anything that has to line up with an ordinary page's measure
     reaches for this, as the breadcrumb above the split columns does. */
  --page-measure: calc(85rem - 2 * var(--shell-gutter));
  /* The content document's two line lengths: prose, and the wider column
     its figures fill (content-document.css). Author-facing, like the
     exercise tokens: a document's own :root takes either back. */
  --prose-measure: 38rem;
  --figure-measure: 54rem;
  color-scheme: light dark;
}

/*
 * The exercise tokens, mapped onto the palette. This is the whole of what the
 * exercise widgets — the shadow roots of the eight interactive kinds and the
 * three review widgets, and the kit's own chrome in the light DOM (the action
 * bar, the correctness mark, the legend, the two text kinds' fields) — take
 * from the page: each `--exercise-*` token names a role in an exercise, the
 * kit declares its default (`exercise-kit/tokens.css`), and this block says
 * which palette colour plays that role here. The names are the styling
 * contract an author's `:::style` sheet writes against, documented under the
 * style directive in docs/carnap-markdown-v1.md; the palette's names are not.
 *
 * Every token the kit declares is mapped here and nothing is mapped that the
 * kit does not declare — tests/a11y/tokens.test.ts holds both — so a token
 * added on one side without the other fails the build rather than silently
 * falling to its default on the page.
 */
:root {
  --exercise-text: var(--ink);
  --exercise-text-muted: var(--ink-muted);
  --exercise-text-faint: var(--ink-faint);
  --exercise-surface: var(--surface);
  --exercise-field-bg: var(--control-surface);
  --exercise-inset-bg: var(--surface-soft);
  --exercise-border: var(--rule);
  --exercise-divider: var(--rule-soft);
  --exercise-accent: var(--blue);
  --exercise-accent-text: var(--blue-strong);
  --exercise-accent-bg: var(--blue-soft);
  --exercise-on-accent: var(--on-accent);
  --exercise-correct: var(--green);
  --exercise-correct-bg: var(--green-soft);
  --exercise-incorrect: var(--red);
  --exercise-incorrect-bg: var(--red-soft);
  --exercise-warning: var(--gold);
  --exercise-highlight: var(--gold);
  --exercise-shadow: var(--shadow-color);
  --exercise-mono-font: var(--mono-font);
  --exercise-scope-line: var(--fitch-scope);
}

* {
  box-sizing: border-box;
}

a {
  color: var(--blue);
  text-decoration: none;
}

a:hover {
  color: var(--blue-strong);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h2 {
  font-size: 1.2rem;
  letter-spacing: -0.015em;
  line-height: 1.25;
  margin-bottom: 0.8rem;
}

h3 {
  font-size: 0.96rem;
  margin-bottom: 0.45rem;
}

:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--blue) 30%, transparent);
  outline-offset: 3px;
}

/* Buttons and controls */

button,
.button {
  align-items: center;
  background: var(--control-surface);
  border: 1px solid var(--blue);
  border-radius: 4px;
  color: var(--blue);
  cursor: pointer;
  display: inline-flex;
  font: inherit;
  font-weight: 650;
  gap: 0.45rem;
  justify-content: center;
  min-height: 42px;
  padding: 0.6rem 1.15rem;
  text-decoration: none;
  white-space: nowrap;
}

button:hover,
.button:hover {
  background: var(--blue-soft);
  color: var(--blue-strong);
  text-decoration: none;
}

/* A control that is on screen to show the shape of things but cannot be worked
   — the preview's submit above all. Without a rule of its own it is drawn
   exactly like a live button, because the colour set above overrides the grey
   a browser would otherwise apply, and the only way to learn it is dead is to
   press it. The faint ink is deliberate: a disabled control is exempt from the
   contrast minimum, and reading as unavailable is the whole point.

   A hover state does not match a disabled form control, so none of the hover
   rules below need to win this back. */
button:disabled,
.button:disabled {
  background: var(--surface-soft);
  border-color: var(--rule);
  color: var(--ink-faint);
  cursor: not-allowed;
}

/* .secondary is retained as an alias for the default light button so
   existing markup keeps working; the two are intentionally identical. */

button.ghost,
.button.ghost {
  background: transparent;
  border-color: var(--rule);
  color: var(--ink);
}

button.danger,
.button.danger {
  background: transparent;
  border-color: color-mix(in srgb, var(--red) 50%, transparent);
  color: var(--red);
}

button.ghost:hover,
.button.ghost:hover {
  border-color: var(--blue);
  color: var(--blue);
  filter: none;
}

button.danger:hover,
.button.danger:hover {
  background: var(--red-soft);
  border-color: var(--red);
  color: var(--red);
  filter: none;
}

input,
select,
textarea {
  background: var(--control-surface);
  border: 1px solid var(--rule);
  border-radius: 4px;
  color: var(--ink);
  font: inherit;
  /* Stated, not inherited, because the select's floor below is computed
     from it and the two have to agree by construction. */
  line-height: 1.5;
  padding: 0.55rem 0.65rem;
  width: 100%;
}

/* Chromium's stylesheet pins a drop-down select to `line-height: normal`
   with !important, which `font: inherit` cannot reach, so beside a text
   input in the same row it came out a couple of pixels shorter. The floor is
   the input's own box — its line, padding and border — so the two match
   whatever the font size. */
select {
  min-height: calc(1.5em + 1.1rem + 2px);
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--blue);
}

/* Growing a field taller is fine; dragging it wider breaks the form's
   layout, so the handle only moves vertically. */
textarea {
  resize: vertical;
}

/* Checkboxes are inline controls: the full-width block treatment above
   would stretch and misplace them inside their label text. */
input[type="checkbox"] {
  accent-color: var(--blue);
  margin-right: 0.45rem;
  width: auto;
}

input[type="hidden"] {
  display: none;
}

/* The browser's date/time control is exactly as wide as the reader's own
   locale format makes it — "07/31/2026, 11:59 PM" needs more room than
   "31.07.2026, 23:59" — and a field too narrow for its value does not
   shrink the text or show that anything is missing: it silently scrolls the
   tail, the AM/PM marker, out of sight. So the control is never squeezed
   below its intrinsic width; a tight row wraps or grows instead. */
input[type="datetime-local"] {
  min-width: min-content;
}

/* A prefilled-but-immutable field: legible, but visibly not for editing. The
   --rule-soft fill is the darkest surface in the palette, so this is the
   --ink-muted-strong case (plain --ink-muted is only 3.66:1 here). */
input[readonly] {
  background: var(--rule-soft);
  color: var(--ink-muted-strong);
}

input[readonly]:focus {
  border-color: var(--rule);
}

label {
  color: var(--ink);
  display: block;
  font-size: 0.9rem;
  font-weight: 560;
}

label + label,
form > label {
  margin-top: 0.9rem;
}

form button,
form .button {
  margin-top: 1.1rem;
}

fieldset {
  border: 1px solid var(--rule-soft);
  border-radius: 4px;
  margin: 1rem 0 0;
  padding: 0.5rem 1rem 1rem;
}

legend {
  color: var(--ink-muted);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 0 0.4rem;
  text-transform: uppercase;
}

/* Tables (ledger styling) */

table {
  border-collapse: collapse;
  width: 100%;
}

th,
td {
  border-bottom: 1px solid var(--rule-soft);
  padding: 0.82rem 0.95rem;
  text-align: left;
  vertical-align: top;
}

tr:last-child td {
  border-bottom: 0;
}

th {
  color: var(--ink-muted);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

td:first-child,
th:first-child {
  padding-left: 0;
}

td:last-child,
th:last-child {
  padding-right: 0;
}

td a {
  font-weight: 600;
}

/* Code, inline and in blocks: a lesson quoting a directive, a worked proof
   shown as text, the theory panel's verbatim MM0, the app shell's own source
   listings. The same face as the exercises, and the one place it is asked to
   stop drawing ligatures — see --mono-font. What is quoted here is the text
   itself, so a lesson teaching that you type a hyphen and a greater-than must
   show a hyphen and a greater-than. Monospace runs wider than the body face
   at one nominal size, so the notch down keeps a full line of source on
   screen — and both sizes are in rem, so a code element inside a pre does
   not shrink twice. */
code,
pre {
  font-family: var(--mono-font, "Fira Code", ui-monospace, monospace);
  font-size: 0.9rem;
  font-variant-ligatures: none;
}

/* A block reads as a quoted slab of source: the same soft fill and hairline
   the answer-review box uses, so content and chrome agree. Lines are long and
   must not be re-flowed, so they scroll sideways rather than being clipped by
   the sheet. */
pre {
  background: var(--surface-soft);
  border: 1px solid var(--rule-soft);
  border-radius: 4px;
  line-height: 1.45;
  overflow-x: auto;
  padding: 0.7rem 0.85rem;
}

/* Mathematics. The compiler stores formulas as MathML, which the browser
   lays out itself — but only well if it is given a font carrying an OpenType
   MATH table, and no platform reliably has one. See ./math-font, which is
   also where an instructor is pointed to override this. The generic 'math'
   family is the fallback: on a machine that does have such a font installed
   it is the right one, and on one that does not, nothing else would help. */

math {
  /* biome-ignore lint/a11y/useGenericFontNames: `math` is the generic family
     here — CSS Fonts 4 added it for exactly this element, and the rule's list
     of generics predates it. */
  font-family: "STIX Two Math", math;
}

/* MathML Core does not break a long equation across lines — there is no
   property that asks it to — so a displayed one that outgrows the measure
   scrolls on its own rather than widening the page. A reader on a phone
   otherwise gets a document that pans sideways. */
math[display="block"] {
  margin: 1.1rem 0;
  overflow-x: auto;
  overflow-y: hidden;
}

/* The lines Core dropped, drawn back on. MathML 3 states these as attributes —
   `columnlines`, `rowlines`, `menclose notation` — that Core does not define
   and Chromium therefore ignores; the compiler's math-core pass strips them
   and puts these classes on instead, so one set of rules draws the same figure
   in every engine. `currentColor` matters: a formula inherits the prose colour,
   including in dark mode, and a hard-coded rule would go on being black. */

mtd.math-column-rule {
  border-inline-end: 1px solid currentColor;
}

mtd.math-column-rule-dashed {
  border-inline-end: 1px dashed currentColor;
}

mtd.math-row-rule {
  border-block-end: 1px solid currentColor;
}

mtd.math-row-rule-dashed {
  border-block-end: 1px dashed currentColor;
}

/* Core dropped `columnspacing` with the rest, so a ruled table's columns would
   otherwise sit against their own rules. Every cell is padded, not just the
   ones carrying a line, or the columns land unevenly. */
mtable.math-ruled mtd {
  padding: 0.15em 0.4em;
}

mrow.math-enclose-box {
  border: 1px solid currentColor;
  padding: 0.15em 0.3em;
}

mrow.math-enclose-left {
  border-inline-start: 1px solid currentColor;
  padding-inline-start: 0.3em;
}

mrow.math-enclose-right {
  border-inline-end: 1px solid currentColor;
  padding-inline-end: 0.3em;
}

mrow.math-enclose-top {
  border-block-start: 1px solid currentColor;
  padding-block-start: 0.15em;
}

mrow.math-enclose-bottom {
  border-block-end: 1px solid currentColor;
  padding-block-end: 0.15em;
}

/* `\cancel` and `\bcancel`, as a gradient rather than a border: the line runs
   corner to corner, which no border does, and `\xcancel` is then the two
   classes together on one row. */

mrow.math-enclose-strike-up {
  background-image: linear-gradient(
    to left top,
    transparent calc(50% - 0.55px),
    currentColor calc(50% - 0.55px),
    currentColor calc(50% + 0.55px),
    transparent calc(50% + 0.55px)
  );
}

mrow.math-enclose-strike-down {
  background-image: linear-gradient(
    to left bottom,
    transparent calc(50% - 0.55px),
    currentColor calc(50% - 0.55px),
    currentColor calc(50% + 0.55px),
    transparent calc(50% + 0.55px)
  );
}

/* Exercise and submission review surfaces */

/* One exercise's box in the reading flow, and exactly one per exercise on every
   path: a `<carnap-…>` element for the eight widget kinds, a `<section>` for the
   two text kinds (see `exerciseRootAttributes`). A graded attempt wraps that box
   in `form.exercise-submission`, which is the posting envelope and not the
   exercise — so a rule written here, by us or by an author, applies once whether
   a lesson is being read, previewed, or worked through.

   `display` is load-bearing, not tidiness: an unknown element is `inline` by
   default, and vertical margins do nothing on an inline box. That is why the
   widgets used to sit flush against the paragraph after them while the two
   `<section>` kinds did not.

   Margin on both sides rather than the bottom alone, so an exercise brings its
   own space rather than depending on what precedes it having a bottom margin;
   adjacent margins collapse, so a run of exercises is spaced once, not twice. */

.exercise {
  display: block;
  margin-block: 1.1rem;
}

/* A submitted answer, as the grader or the student reviews it: a soft box
   of labeled values (selected options, response text, the correct answer
   for instructors).

   The box is one of the warm fills plain --ink-muted misses AA on (4.39:1),
   so the muted exercise token is remapped to the stronger ink for everything
   inside it — the review widgets' shadow roots included, which is the point
   of a token: the widget says "muted text" and the page, which knows what
   ground it put the widget on, says which ink that is here. */

.answer-review {
  --exercise-text-muted: var(--ink-muted-strong);
  background: var(--surface-soft);
  border: 1px solid var(--rule-soft);
  border-radius: 4px;
  padding: 0.85rem 1rem;
}

.answer-review p {
  margin: 0;
}

.answer-review dl {
  display: grid;
  gap: 0.45rem 1.25rem;
  grid-template-columns: max-content 1fr;
  margin: 0;
}

.answer-review dt {
  color: var(--ink-muted);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  padding-top: 0.14rem;
  text-transform: uppercase;
}

.answer-review dd {
  margin: 0;
  white-space: pre-wrap;
}

/*
 * Shared look for the group and its legend, in a form both the page stylesheet
 * and the widgets' shadow roots can use — a shadow root inherits no page CSS, so
 * without one constant the same legend drifts into six slightly different labels.
 *
 * The fieldset itself is reset to nothing: a border around a truth table or a
 * CodeMirror editor reads as a second card inside the card, and the exercise is
 * already separated from its neighbours by `.exercise`'s margins.
 *
 * The one colour is read both ways because the sheet is served both ways: in a
 * shadow root the kit's `tokens.css` has declared the private twin, and in the
 * light DOM — where this sheet is only ever served alongside `web/content.css`
 * — the twin is unset and the exercise token itself resolves.
 */

.exercise-group {
  border: 0;
  margin: 0;
  min-inline-size: 0;
  padding: 0;
}

.exercise-legend {
  color: var(--_text-muted, var(--exercise-text-muted));
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  margin-bottom: 0.55rem;
  padding: 0;
  text-transform: uppercase;
}

/* The legend holds a heading (see `exerciseLegendHtml`) for the reader who
   moves through a page by headings; to the eye it must stay the legend's small
   caps. `all: unset` gives every inherited property back to the legend and
   every other one its initial value, so neither the browser's own heading
   size, weight and margins (all a shadow root has) nor `content.css`'s
   `h2`/`h3` rules (in the light DOM) reach it, and it lays out inline, as
   the legend's text did when it was a bare text node. Two classes, so that an
   author's `.lesson h3` cannot outrank it either. A heading's role does not
   depend on its display, so it stays a heading. */
.exercise-legend > .exercise-heading {
  all: unset;
}

/* A visible title runs in with the prompt's first line rather than sitting on a
   line of its own. `float` is what makes that possible at all: an unfloated
   `<legend>` is the fieldset's *rendered legend*, which the browser lifts out of
   the flow, so no value of `display` will put it beside the prompt. Two
   corollaries, both load-bearing:

   - a group that wants the run-in title cannot be a grid or flex container,
     since floats do not apply to those items (see `.tt-wrap`);
   - the padding is a baseline nudge — a float aligns by its top edge, and the
     small caps would otherwise ride above the prompt's much larger text. */
/* The run-in title lives on a height budget: a float excludes text by its
   *margin* box, so that box has to stay shorter than one line of the prompt
   (1.5rem = 24px at the default size). Overshoot it and the prompt's *second*
   line clips the float and comes out indented — a ragged left edge that appears
   and vanishes with the font size. Line-height 1.15 plus the baseline padding
   spends 20px of the 24, and there is deliberately no bottom margin: 0.35rem
   there was enough to break it. Both texts are sized in rem, so the budget
   scales with the root font size rather than drifting against it. */
.exercise-legend:not(.visually-hidden) {
  float: left;
  line-height: 1.15;
  margin-bottom: 0;
  margin-right: 0.6rem;
  /* Sits the small caps on the prompt's baseline. A float aligns by its top
     edge, and the two texts differ in size, so the nudge is the difference
     between their baseline positions within their own line boxes — measured
     (0.1px residual), not derived: no CSS length expresses "one font's ascent
     minus another's". */
  padding-top: 0.4rem;
}

/* Whatever follows the prompt begins on a fresh line. A float only pushes
   *inline* content aside, so a block sibling — the grid, an editor, the action
   bar — would slide under a title taller than the prompt beside it. Two
   selectors for the two shapes the prompt takes: a named slot in the widgets'
   shadow roots, a light-DOM div in the text kinds. */
.exercise-group > slot[name="prompt"] ~ *,
.exercise-group > .exercise-prompt ~ * {
  clear: left;
}

/*
 * The kit's stylesheet: the chrome every exercise shares on the page — the
 * prompt, the status and rubric lines, the action bar `exercise-kit/actions.ts`
 * emits, the correctness mark `exercise-kit/correctness-mark.ts` defines, the
 * two text kinds' native fields, and the load-failure notice. Served inside
 * the content layer by `web/styles.ts`, after the exercise-group block: this
 * file restyles `.exercise-prompt`, which that block declares.
 *
 * Every colour here is an exercise token (`tokens.css`), read directly: this
 * sheet is only ever served alongside `web/content.css`, which maps each token
 * onto the palette, so unlike a shadow root it never needs the defaults. The
 * point of reading the tokens rather than the palette is that an author who
 * sets `--exercise-field-bg` or `--exercise-accent` restyles a widget's
 * interior and its Submit button with one declaration.
 */

.exercise-prompt {
  font-weight: 560;
  margin-bottom: 0.75rem;
}

/* A note cited from a prompt is reference apparatus, not more of the
   question: without this it inherits the prompt's weight and reads as part of
   what is being asked. */
.exercise-prompt .footnotes {
  font-weight: normal;
}

.exercise-status,
.exercise-rubric {
  color: var(--exercise-text-muted);
  font-size: 0.82rem;
  margin-top: 0.6rem;
}

/* The action bar an interactive exercise projects into its shadow card via
   slot="exercise-actions". Every control — Check, counterexample, Submit —
   sits in one light-DOM row so author CSS styles them all uniformly. Its box
   is styled here (not from a shadow ::slotted rule) precisely so custom CSS
   can reach it. */
.exercise-actions {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.8rem;
  margin-top: 0.45rem;
}

/* The action buttons: quiet and small, so they read as part of the exercise
   rather than page-level calls to action. The warm rule border keeps them
   recessive until hover brings the blue forward. Fill and ink are restated
   from the tokens rather than left to the page's `button` rule, so that the
   tokens are the whole of what colours them. */
.exercise-submit,
.exercise-actions button {
  background: var(--exercise-field-bg);
  border-color: var(--exercise-border);
  color: var(--exercise-accent);
  font-size: 0.82rem;
  font-weight: 600;
  min-height: 0;
  padding: 0.32rem 0.85rem;
}

.exercise-submit:hover,
.exercise-actions button:hover {
  background: var(--exercise-accent-bg);
  border-color: var(--exercise-accent);
  color: var(--exercise-accent-text);
}

/* The same recessed, faint look the page gives any dead control (see
   `button:disabled` in web/content.css), in the exercise's own tokens. */
.exercise-submit:disabled,
.exercise-actions button:disabled {
  background: var(--exercise-inset-bg);
  border-color: var(--exercise-border);
  color: var(--exercise-text-faint);
}

/* The base form-button margin-top would push controls down inside the bar's
   flex row; the bar owns its own spacing above itself instead. */
.exercise-actions button {
  margin-top: 0;
}

/* The instructions button, first in the row. It used to be a round pill in each
   widget's own toolbar; it is a plain button here, the same shape and height as
   Check and Submit beside it, because it is one of the exercise's controls and
   not a badge stuck to the side of one.

   Only the horizontal padding is its own: the glyph is a single character, so
   the row's full side padding would leave it swimming. Nothing vertical is set
   here — a padding shorthand, or a tighter line height, would make it the one
   short button in the row, which is exactly what it must not be. */
.exercise-actions .help-trigger {
  padding-left: 0.6rem;
  padding-right: 0.6rem;
}

/* The submission status sits inline with the buttons. */
.exercise-actions .exercise-status {
  margin: 0;
}

/* A widget's own Check speaks here: its own line under the button row, in the
   same small muted type as every other status. Under the buttons and not over
   them, because it is an answer to the Check that was just pressed — a sentence
   above the control that produced it reads as part of the exercise. */
.exercise-actions .exercise-check-status {
  flex-basis: 100%;
  margin: 0.1rem 0 0;
}

/* Nothing to say yet: the row must not open a blank line under itself before
   the first Check, nor keep one after an edit clears the verdict. */
.exercise-actions .exercise-check-status:empty {
  display: none;
}

.exercise-actions .exercise-check-status[data-state="correct"] {
  color: var(--exercise-correct);
  font-weight: 600;
}

/* The counterexample toggle, pressed. */
.exercise-actions .tt-ce[aria-pressed="true"] {
  border-color: var(--exercise-accent);
  border-width: 2px;
}

/* The correctness mark: one indicator, in one place, in one shape, for every
   exercise type. It is deliberately present from the first paint rather than
   appearing when there is something to say — a reader who has never seen a
   correct exercise should still know where the verdict will show up, and an
   indicator that materializes out of nothing also reflows the row it is in.
   So the idle state is an empty dashed ring: the place, drawn as not yet
   filled in. Every state is that one circle — the spinner runs around its
   rim, and a verdict fills it — so nothing about the mark moves or resizes
   as it changes, only what is drawn in it.

   It lives in the light-DOM action bar, not in each widget's shadow root,
   which is what makes "the same place" true across nine types that otherwise
   share no layout at all — and means this rule is the only description of it.

   Two things write to it and they answer different questions. The runtime
   sets it from the recorded evaluation (on load, and after each submit): is
   the answer the server holds correct? A widget that can grade itself in the
   browser overrides that with its own live verdict: is what is on screen
   right now correct? The live answer wins whenever there is one, because it
   is the one about the work the reader is looking at. */
.exercise-mark {
  --_mark-rim: 1.5px;
  align-items: center;
  /* The idle rim is drawn in the muted text colour, not the border token: it
     is the whole of what the mark shows before a verdict, so it has to reach
     the 3:1 a meaningful graphic needs, which a decorative rule does not. */
  border: var(--_mark-rim) dashed var(--exercise-text-muted);
  border-radius: 50%;
  box-sizing: border-box;
  color: var(--exercise-text-muted);
  display: inline-flex;
  flex: none;
  /* Named rather than inherited: the mark sits in a monospace toolbar in two
     of the widgets and beside a serif prompt in the others, and the glyph has
     to be the same glyph in all of them. */
  font-family: system-ui, sans-serif;
  font-size: 0.85rem;
  font-weight: 700;
  height: 1.5rem;
  justify-content: center;
  line-height: 1;
  /* Hard right of the button row, so it is in the same place whether the row
     holds one button or three. */
  margin-left: auto;
  position: relative;
  width: 1.5rem;
}

.exercise-mark[data-state="ok"] {
  background: var(--exercise-correct-bg);
  border-color: color-mix(in srgb, var(--exercise-correct) 45%, transparent);
  border-style: solid;
  color: var(--exercise-correct);
}

.exercise-mark[data-state="error"] {
  background: var(--exercise-incorrect-bg);
  border-color: color-mix(
    in srgb,
    var(--exercise-incorrect) 40%,
    transparent
  );
  border-style: solid;
  color: var(--exercise-incorrect);
  /* The specific problem is the title; the cursor is what says to hover. */
  cursor: help;
}

/* "Checking": the proof types compile in the background, and the wait is long
   enough to need saying. A solid arc runs round the idle ring, laid exactly
   over its rim, so the mark reads as the same empty circle, busy. Drawn here
   rather than built as an SVG in the client, so setting the mark stays a
   matter of writing attributes. */
.exercise-mark[data-state="working"]::after {
  animation: exercise-spin 0.9s linear infinite;
  border: var(--_mark-rim) solid transparent;
  border-radius: 50%;
  border-top-color: currentColor;
  content: "";
  inset: calc(-1 * var(--_mark-rim));
  position: absolute;
}

@keyframes exercise-spin {
  to {
    transform: rotate(1turn);
  }
}

@media (prefers-reduced-motion: reduce) {
  .exercise-mark[data-state="working"]::after {
    animation-duration: 2.5s;
  }
}

/* Shown when an interactive exercise's bundle fails to upgrade it in time. */
.exercise-load-error {
  color: var(--exercise-incorrect);
  font-size: 0.85rem;
  margin-top: 0.6rem;
}

/* The two text kinds answer in a native field in the light DOM. It takes the
   page's `input`/`textarea` rule for its shape and reads the exercise tokens
   for its colours, so a field in a free-response exercise and a field inside
   the model widget's shadow root answer to the same declaration. */
.exercise-group input,
.exercise-group textarea {
  background: var(--exercise-field-bg);
  border-color: var(--exercise-border);
  color: var(--exercise-text);
}

.exercise-group input:focus,
.exercise-group textarea:focus {
  border-color: var(--exercise-accent);
}

/*
 * The content layer's small utilities, after the exercise chrome: the note on
 * formula faces, and the classes everything else leans on. `.visually-hidden`
 * restyles the one the exercise-group block declares, which is why this comes
 * after it (see ./styles.ts).
 */

/* Formulas are set in --mono-font by the widget that shows them, each in its
   own shadow styles. There was a `.formula, .proof-formula` rule here setting
   them in Georgia italic; no markup has carried either class for as long as
   the classes have existed, so it was a fourth opinion about the face that
   nothing could act on. */

.small {
  color: var(--ink-muted);
  font-size: 0.82rem;
  line-height: 1.45;
}

.muted {
  color: var(--ink-muted);
}

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

/*
 * The panel's look: a closed disclosure should read as a quiet label the reader
 * may open, not as another card competing with the exercises around it.
 *
 * Closed, it is a single line — a small-caps kicker in the exercise-legend
 * idiom, then the theory's name set in the source face, since the name is an
 * identifier the author writes in the block's `name="…"` and an exercise's
 * `system="…"`. Open, the source hangs below a hairline inside the same box.
 *
 * The `<pre>` has to undo the page's own block-code framing (fill, border,
 * radius): nested inside the panel it would draw a second box a hairline inside
 * the first. What it keeps is the scroll — MM0 lines are long and a theory can
 * run to dozens of them, so it scrolls in both directions inside a bounded box
 * rather than stretching the lesson.
 */

.aufbau-theory {
  background: var(--surface-soft);
  border: 1px solid var(--rule-soft);
  border-radius: 6px;
  margin: 1.1rem 0;
}

.aufbau-theory-summary {
  align-items: baseline;
  cursor: pointer;
  display: flex;
  gap: 0.5rem;
  padding: 0.45rem 0.75rem;
  /* Both halves of removing the default triangle: the standard property and
     the WebKit pseudo-element that predates it. */
  list-style: none;
}

.aufbau-theory-summary::-webkit-details-marker {
  display: none;
}

/* A disclosure triangle drawn in borders rather than set as a glyph: "▸" is
   rendered by a fallback font on many systems, at a size and baseline nothing
   here controls. */
.aufbau-theory-summary::before {
  border-bottom: 0.24em solid transparent;
  border-left: 0.34em solid currentcolor;
  border-top: 0.24em solid transparent;
  color: var(--ink-faint);
  content: "";
  display: inline-block;
  transform-origin: 0.14em 50%;
  transition: transform 120ms ease;
}

.aufbau-theory[open] > .aufbau-theory-summary::before {
  transform: rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
  .aufbau-theory-summary::before {
    transition: none;
  }
}

/* The same small caps an exercise group's legend uses, so a theory reads as
   chrome of the same rank as the exercises it serves. */
.aufbau-theory-kicker {
  color: var(--ink-muted);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The theory's name, which is an identifier rather than a formula — so it
   keeps ligatures off, like the `<pre>` of MM0 the panel opens onto. */
.aufbau-theory-name {
  color: var(--ink);
  font-family: var(--mono-font, "Fira Code", ui-monospace, monospace);
  font-size: 0.85rem;
  font-variant-ligatures: none;
}

.aufbau-theory-summary:hover .aufbau-theory-kicker,
.aufbau-theory-summary:hover .aufbau-theory-name {
  color: var(--blue-strong);
}

.aufbau-theory-source {
  background: none;
  border: 0;
  border-radius: 0;
  border-top: 1px solid var(--rule-soft);
  margin: 0;
  max-height: 22rem;
  overflow: auto;
  padding: 0.7rem 0.85rem;
}
