
/* ---- Colour system ---------------------------------------------------------
   Every colour on the page flows from the tokens below; nothing else in this
   sheet (or in widgets.js) names a raw colour. Tuned via the in-page palette
   dialer (palette.js, dev-only — bottom-right 🎨), which writes these same
   dials/anchors as inline vars — keep the two in sync.

   Only the DIALS and ANCHORS are hand-picked. The neutral ramp is computed:
   one shared hue (--warmth) and chroma profile (stop weights × --tint) in
   OKLCH — at tint 1.0 the profile reproduces the original warm-paper palette;
   the shipped page runs it at 0.30 ("crisp", Oz's pick 2026-07-07). Role
   colours derive from their anchors via relative colour syntax (chroma ×
   --role-chroma, plus lightness lift in dark mode), and the remaining
   neutrals are color-mix blends. Requires oklch(from …) — Chrome 119+ /
   Safari 16.4+ / Firefox 128+, same vintage as the :has() this page leans on. */
:root {
  color-scheme: light;

  /* Dials */
  --warmth: 84.6;       /* neutral-ramp hue (deg) */
  --tint: 0.30;         /* neutral-ramp chroma × (1.0 = the old warm paper) */
  --role-chroma: 1.2;   /* field/accent chroma × — flat paper earns punch */
  --dark-lift: 0;       /* dark mode lifts role lightness (see dark block) */
  --role-dim: 1;        /* …and trims role chroma a touch */
  --mute-cap: 34%;      /* read-only colour: caps, quoted values, ro on-cells */
  --mix-formula: 62%;   /* formula terms: role colour toward muted */
  --selection-tint: 16%;

  /* Anchors (pre-boost; the IEEE-754 triad is Wikipedia's palette) */
  --a-accent: #47993f;  /* links, focus, progress — the "understood" green */
  --a-sign: #2f6fb0;    /* sign/integer blue */
  --a-exp:  #2f8f43;    /* exponent green */
  --a-frac: #cc3f86;    /* significand/fraction pink */
  --a-neg:  #b5503a;    /* error / selection sienna */
  --a-sky:  #1f2638;    /* progress-strip navy */
  --gold: #ffd166;      /* aced stars */
  --t-res: #7fb2e8;     /* typed REPL results (numbers and booleans alike) */

  /* Neutral ramp, light to dark: oklch(stop-L, stop-C × tint, warmth) */
  --face: #fff;                                                  /* interactive faces */
  --bg:       oklch(0.988 calc(0.0029 * var(--tint)) var(--warmth)); /* the page */
  --surface:  oklch(0.994 calc(0.0057 * var(--tint)) var(--warmth)); /* cards */
  --wash:     oklch(0.944 calc(0.0144 * var(--tint)) var(--warmth)); /* tinted fills */
  --rule:     oklch(0.911 calc(0.0157 * var(--tint)) var(--warmth)); /* hairline rules */
  --hairline: oklch(0.835 calc(0.0207 * var(--tint)) var(--warmth)); /* widget cell borders */
  --muted:    oklch(0.625 calc(0.0177 * var(--tint)) var(--warmth)); /* secondary text */
  --ink-soft: oklch(0.396 calc(0.0127 * var(--tint)) var(--warmth)); /* tutor replies, notes */
  --ink:      oklch(0.282 calc(0.0061 * var(--tint)) var(--warmth)); /* text */

  /* Roles: anchor, chroma dialled, lifted in dark mode. One formula serves
     both themes — the dark block only flips the lift/dim dials. */
  --accent: oklch(from var(--a-accent) calc(l + var(--dark-lift)) calc(c * var(--role-chroma) * var(--role-dim)) h);
  --c-sign: oklch(from var(--a-sign)   calc(l + var(--dark-lift)) calc(c * var(--role-chroma) * var(--role-dim)) h);
  --c-exp:  oklch(from var(--a-exp)    calc(l + var(--dark-lift)) calc(c * var(--role-chroma) * var(--role-dim)) h);
  --c-frac: oklch(from var(--a-frac)   calc(l + var(--dark-lift)) calc(c * var(--role-chroma) * var(--role-dim)) h);
  --neg:    oklch(from var(--a-neg)    calc(l + var(--dark-lift)) calc(c * var(--role-chroma) * var(--role-dim)) h);

  /* Dark islands (REPL terminal, progress strip) */
  --t-bg: var(--ink);   /* terminal face; its own surface in dark mode */
  --t-ink: oklch(0.935 calc(0.0173 * var(--tint)) var(--warmth));
  --t-edge: transparent;/* terminal border — invisible until dark mode */
  --sky: var(--a-sky);
  --star-dim: #8f8f8f;
  --star-lit: #fff;

  --on-ink: #fff;       /* text on a full role/accent fill */
  --shadow-ink: var(--ink);

  /* Derived neutrals — ramp blends, no new anchors */
  --c-plain: color-mix(in srgb, var(--muted) 62%, var(--ink));    /* neutral field role */
  --ro-face: color-mix(in srgb, var(--wash) 55%, var(--face));    /* read-only cell face */
  --ro-ink: color-mix(in srgb, var(--muted) 65%, var(--hairline));/* read-only cell digits */
  --ro-edge: color-mix(in srgb, var(--rule) 50%, var(--hairline));/* read-only on-cell border */
  --ghost: color-mix(in srgb, var(--hairline) 75%, var(--muted)); /* ledger +/= operator */
  --neg-ink: color-mix(in srgb, var(--neg) 76%, var(--ink));      /* error text */

  --accent-ink: var(--accent);
  --ok: var(--accent);           /* muted green — "understood" */
  --serif: "Palatino Linotype", Georgia, Cambria, serif;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;

  /* Prose and its margin notes form one centred column. The checkpoints
     (.c-widget) break out wider; this column is ~3/4 of that width. */
  --measure: 38rem;                 /* prose text column */
  --sidenote-w: 13rem;              /* right-hand margin-note column */
  --sidenote-gap: 1.5rem;           /* gutter between the two */
  --content-w: calc(var(--measure) + var(--sidenote-gap) + var(--sidenote-w));
}

/* Dark theme: the ramp mirrored (its own L/C stops, same warmth/tint dials),
   roles re-derived by flipping the lift/dim dials — no per-role overrides.
   Every color-mix-derived neutral is relational (mixes toward ink/face), so
   it recomputes correctly too. data-theme is stamped by a tiny head script
   (build.sh) from localStorage("fp-theme"), falling back to
   prefers-color-scheme; the palette dialer (palette.js) flips it live. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --dark-lift: 0.10;
  --role-dim: 0.92;
  --selection-tint: 30%;

  --face:     oklch(0.256 calc(0.0071 * var(--tint)) var(--warmth));
  --bg:       oklch(0.198 calc(0.0038 * var(--tint)) var(--warmth));
  --surface:  oklch(0.225 calc(0.0061 * var(--tint)) var(--warmth));
  --wash:     oklch(0.279 calc(0.0107 * var(--tint)) var(--warmth));
  --rule:     oklch(0.320 calc(0.0126 * var(--tint)) var(--warmth));
  --hairline: oklch(0.398 calc(0.0188 * var(--tint)) var(--warmth));
  --muted:    oklch(0.629 calc(0.0208 * var(--tint)) var(--warmth));
  --ink-soft: oklch(0.776 calc(0.0245 * var(--tint)) var(--warmth));
  --ink:      oklch(0.914 calc(0.0186 * var(--tint)) var(--warmth));

  --t-bg:   oklch(0.245 calc(0.0080 * var(--tint)) var(--warmth)); /* elevated terminal */
  --t-ink: var(--ink);
  --t-edge: oklch(0.330 calc(0.0110 * var(--tint)) var(--warmth));
  --sky: oklch(from var(--a-sky) calc(l - 0.04) c h);
  --star-dim: #6f6f6f;

  --on-ink: var(--bg);
  --shadow-ink: #000;
}

html {
  background: var(--bg);
  font-size: 18px;
  }

body {
  margin: 0 auto;
  /* Prose + sidenotes are one centred column ~3/4 the checkpoints' width; the
     checkpoints themselves break out wider (see .c-widget). */
  max-width: min(var(--content-w), 94vw);
  padding: 4.5rem 1.25rem 9rem;
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1.16rem;
  line-height: 1.62;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

::selection { background: color-mix(in srgb, var(--neg) var(--selection-tint), transparent); }

#title-block-header { margin: 0 0 3rem; }
.title {
  font-family: var(--sans);
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin: 0;
}

h2 {
  font-family: var(--sans);
  font-size: 1.4rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.25;
  margin: 3.25rem 0 1rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}

p { margin: 0 0 1.2rem; }
em { font-style: italic; }

a {
  color: var(--accent-ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

code {
  font-family: var(--mono);
  font-size: 0.88em;
  background: var(--wash);
  padding: 0.06em 0.32em;
  border-radius: 3px;
}

/* ---- Evaluated-code examples (```r blocks, built by build.sh) -------------
   A quiet dark REPL box: `>` prompt rows, type-coloured results (numbers blue,
   booleans purple), optional language badge in the corner. C examples use a
   trailing `// =>` comment instead of a prompt, since C has no REPL.
   Settings chosen via repl-tuner.html — keep the two in sync. */
.repl {
  box-sizing: border-box;
  width: fit-content;
  /* All blocks share a floor of 2/3 measure (so short examples don't shrink-wrap
     and they read as one consistent element); a long line can still grow to the
     full measure. On screens narrower than the floor, take the full width. */
  min-width: min(calc(var(--measure) * 2 / 3), 100%);
  max-width: var(--measure);      /* never exceed the prose column */
  margin: 0.4rem 0;
  padding: 0.6rem 0.6rem;
  border-radius: 8px;
  background: var(--t-bg);
  /* outline, not border: the edge only shows in dark mode (t-edge is
     transparent in light) and must not shift layout when it appears */
  outline: 1px solid var(--t-edge);
  outline-offset: -1px;
  color: var(--t-ink);
  font-family: var(--mono);
  font-size: 0.82em;
  line-height: 1.5;
  overflow-x: auto;
}
/* The badge is a flex item on the first row (build.sh puts it there), so it
   counts toward the shrink-wrapped width instead of overlapping the code. */
.repl__badge {
  flex: none;
  /* Pin to the row's top rather than the code baseline, so the badge keeps
     even top/right spacing from the box corner (the block's 0.6rem padding). */
  align-self: flex-start;
  line-height: 1;
  margin-left: auto;
  padding-left: 1.6em;
  font-family: var(--sans);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  user-select: none;
}
.repl__row { display: flex; gap: 0.6em; align-items: baseline; }
.repl__gutter { flex: none; white-space: pre; color: var(--muted); user-select: none; }
.repl__code { white-space: pre; }
.repl__comment { color: var(--muted); }
.repl__res--number,
.repl__res--boolean { color: var(--t-res); } /* one result colour for all types */
.repl__res--plain   { color: var(--t-ink); }

hr {
  border-top: 1px solid var(--rule);
  max-width: var(--measure);
  margin-left: 0;        /* left-align the rule, not the UA's auto-centring */
}

/* ---- Prose tables ---------------------------------------------------------
   Quiet sans "rows" style: a light rule under each body row (none under the
   last), small muted headers with no rule of their own, tabular digits.
   Settings chosen via table-tuner.html (Oz's pick, 2026-07-07) — keep the
   tuner's defaults in sync. */
table {
  border-collapse: collapse;
  margin: 0 0 1.4rem;
  width: auto;            /* shrink-wrap: rows read better when columns stay
                             close (the .enc-grid spans the measure instead) */
  display: table;         /* pandoc --standalone injects table CSS into the   */
  font-family: var(--sans);
  font-size: 0.84rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.45;
}
tbody { border: none; }   /* built page (display:block, width:100%, tbody/th
                             rules) — this sheet loads after it, so contradict
                             each one here */
/* inline code in tables reads as a row label, not a chip */
table code { background: none; padding: 0; font-size: 0.92em; }
th {
  border: none;           /* pandoc default draws a border-top */
  font-size: 0.7em;
  font-weight: 500;
  color: var(--muted);
  padding: 0.3rem 1.8rem 0.3rem 0;
}
td {
  padding: 0.3rem 1.8rem 0.3rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}
tr:last-child td { border-bottom: none; }
th:last-child, td:last-child { padding-right: 0; }

/* The headerless encoding grid (```::: {.enc-grid}``` in index.md): spans the
   full measure; mono row labels (backticked in the markdown, de-chipped here);
   the redundant all-zero n=0 column and the ⋮ continuation row fade back.
   The wrapper scrolls sideways on screens narrower than the grid. */
.enc-grid { max-width: var(--measure); overflow-x: auto; }
.enc-grid table { width: 100%; }
.enc-grid td { padding-right: 0.6rem; }  /* tighter floor; 100% width spreads it */
.enc-grid td:last-child { padding-right: 0; }
.enc-grid tr:nth-child(5) td { color: var(--muted); }   /* the ⋮ row */

/* ---- Inline widget-values -------------------------------------------------
   [3,2]{.wv} spans in index.md (expanded by build.sh) render as fused
   read-only cells echoing the widget strips: white face, hairline border, a
   2px field-colour cap, muted digits. Chosen via inline-value-styles.html
   (variant F) — keep the two in sync. inline-block cells put the DIGIT's
   baseline on the text baseline (inline-flex would synthesize the box edge
   as the baseline instead). */
.wv {
  display: inline;
  font-family: var(--mono);
  font-size: 0.66em;
  font-style: normal;
  line-height: 1;
  white-space: nowrap; /* a fused value never breaks between its cells */
}
.wv-c {
  --role: var(--c-plain); /* field colours below, as in widgets.js ROLE_COLOR */
  display: inline-block;
  min-width: 1.55em;
  padding: 0.3em 0.24em 0.28em;
  text-align: center;
  background: var(--face);
  border: 1px solid var(--hairline);
  /* the cap wears the ledger's disabled mix: field identity without the
     full interactive colour — these are read-only quotations, not widgets */
  border-top: 2px solid color-mix(in srgb, var(--role) var(--mute-cap), var(--hairline));
  color: var(--muted);
}
.wv-c + .wv-c { margin-left: -1px; } /* fields abut on a shared hairline */
.wv-c[data-role="sig"], .wv-c[data-role="frac"] { --role: var(--c-frac); }
.wv-c[data-role="exp"]  { --role: var(--c-exp); }
.wv-c[data-role="sign"] { --role: var(--c-sign); }
.wv-c i { font-style: italic; font-family: var(--serif); }
table .wv { font-size: 0.88em; }
table .wv .wv-c { padding-top: 0.28em; padding-bottom: 0.24em; }
/* The boxed label is its own row marker: no rule under a wv row-header
   column — the row separators span only the data columns. */
table:has(td:first-child .wv) td:first-child { border-bottom: none; }

/* ---- Page envelope -------------------------------------------------------
   Hold running prose to a readable measure and pin it LEFT, so its left edge
   lines up with the checkpoints' left edge and the right side stays open as a
   margin column for sidenotes. Content inside a checkpoint (.c-widget) is
   exempt — the chat pane has its own internal layout. */
:is(p, h2, h3, ul, ol, blockquote, pre, table, .w-widget, .q-widget):not(.c-widget *),
#title-block-header {
  max-width: var(--measure);
}

/* ---- Sidenotes (Tufte-style margin notes) --------------------------------
   Authored as pandoc footnotes and hoisted inline by sidenotes.js. A small
   numbered marker stays in the text; the note itself floats into the right
   gutter on wide screens and collapses to a tap-to-reveal note on narrow ones.
   Marker and note share one CSS counter so their numbers always agree. */
body { counter-reset: sidenote; }

.sidenote-number {
  counter-increment: sidenote;
  cursor: pointer;
}
.sidenote-number::after {
  content: counter(sidenote);
  font-family: var(--sans);
  font-size: 0.62em;
  font-weight: 600;
  vertical-align: super;
  line-height: 0;
  color: var(--accent-ink);
  padding: 0 0.1em 0 0.05em;
}

/* The toggle checkbox is never shown; it only carries the reveal state. */
.sidenote-toggle { display: none; }

.sidenote {
  position: relative; /* anchors the hanging number (see ::before) */
  float: right;
  clear: right;
  width: var(--sidenote-w);
  /* The note floats to the right edge of its host paragraph (--measure wide),
     then this negative margin carries it across the gutter into the reserved
     margin column, so its right edge meets the centred column's right edge.
     The column is a fixed width, so this offset is fixed too — no viewport math.
     Because the margin column is always reserved (prose is held to --measure
     regardless), a note never steals width from its own paragraph. Below the
     breakpoint there's no column and the note collapses inline (see media). */
  margin: 0.3rem calc(-1 * (var(--sidenote-gap) + var(--sidenote-w))) 1rem 0;
  font-family: var(--sans);
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--ink-soft);
  text-align: left;
}
.sidenote::before {
  content: counter(sidenote);
  /* Hang the number out in the gutter so every line of the note — including
     the first — starts flush at the same left edge. */
  position: absolute;
  left: -1.4em;
  top: 0.18em;
  font-size: 0.72em;
  font-weight: 600;
  color: var(--accent-ink);
}
.sidenote p { margin: 0; }
.sidenote p + p { margin-top: 0.5em; }
.sidenote .katex-display { margin: 0.4em 0; }

/* Below the gutter threshold there's no room to float, so hide notes until the
   marker is tapped, then drop the note inline as a tinted block. */
/* Breakpoint is in px on purpose: rem in a media query is 16px, but the layout's
   rem is the 18px root font-size, so a rem breakpoint here would fire ~900px
   while the column needs ~1008px (52.5rem + padding at 18px) to fit. Below this
   the centred column can't hold its margin notes, so they collapse inline. */
@media (max-width: 1008px) {
  /* No room for a margin column: the note hides until its marker is tapped,
     then drops in inline as a tinted block. With no right margin to fill, the
     prose re-centres so it isn't stranded against the left edge. */
  :is(p, h2, h3, ul, ol, blockquote, pre, table, .w-widget, .q-widget):not(.c-widget *),
  #title-block-header,
  hr {
    margin-left: auto;
    margin-right: auto;
  }
  /* .repl is a <div>, so the auto-margin recentring above skips it — it was
     left stranded at the page edge while the prose moved. Auto margins would
     centre its shrink-wrapped width and break the shared left edge, so give
     it the same left offset the centred measure-wide prose gets instead. */
  .repl {
    margin-left: max(0px, calc((100% - var(--measure)) / 2));
  }
  .sidenote {
    display: none;
    float: none;
    width: auto;
    margin: 0.9rem 0;
    padding: 0.7rem 0.9rem;
    border-left: 3px solid var(--rule);
    background: color-mix(in srgb, var(--rule) 28%, transparent);
  }
  /* Inline (tap-to-reveal) notes have no gutter to hang the number in; let it
     flow with the text again. */
  .sidenote::before {
    position: static;
    vertical-align: super;
    line-height: 0;
    margin-right: 0.35em;
  }
  .sidenote-toggle:checked + .sidenote { display: block; }
}


.q-widget {
  font-family: var(--sans);
  margin: 2.75rem 0;
  padding: 0.1rem 0 0 1.25rem;
  border-left: 3px solid var(--rule);
  transition: border-color 0.4s ease;
}
.q-widget:focus-within { border-left-color: var(--accent); }

.q-widget__log {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: 1rem;
}
.q-widget__log:empty { display: none; }

.q-widget__msg { line-height: 1.5; }
.q-widget__msg--user {
  align-self: flex-end;
  max-width: 85%;
  background: var(--wash);
  color: var(--ink);
  padding: 0.45rem 0.7rem;
  border-radius: 12px 12px 3px 12px;
  font-size: 0.92rem;
  white-space: pre-wrap; /* keep the student's own line breaks */
}
.q-widget__msg--assistant {
  font-family: var(--serif);
  font-size: 1.06rem;
  color: var(--ink-soft);
  max-width: 100%;
}
.q-widget__msg--pending { color: var(--muted); }

/* Rendered Markdown inside tutor replies (richtext.js). Tighten block spacing so
   short answers stay compact, and give code / lists / math room to breathe. */
.q-widget__msg--assistant > :first-child { margin-top: 0; }
.q-widget__msg--assistant > :last-child { margin-bottom: 0; }
.q-widget__msg--assistant p { margin: 0.5em 0; }
.q-widget__msg--assistant ul,
.q-widget__msg--assistant ol { margin: 0.5em 0; padding-left: 1.4em; }
.q-widget__msg--assistant li { margin: 0.15em 0; }
.q-widget__msg--assistant code {
  font-family: var(--mono);
  font-size: 0.85em;
  background: color-mix(in srgb, var(--rule) 45%, var(--face));
  padding: 0.1em 0.3em;
  border-radius: 3px;
}
.q-widget__msg--assistant pre {
  background: color-mix(in srgb, var(--rule) 45%, var(--face));
  padding: 0.6rem 0.7rem;
  border-radius: 6px;
  overflow-x: auto;
  font-size: 0.85em;
}
.q-widget__msg--assistant pre code { background: none; padding: 0; font-size: 1em; }

/* Wide display math scrolls rather than overflowing its column (chat or prose). */
.katex-display { overflow-x: auto; overflow-y: hidden; }
.q-widget__msg--assistant .katex-display { margin: 0.6em 0; }

/* KaTeX defaults to 1.21em, which makes inline numbers tower over the serif
   text around them; pull it close to the prose size. */
.katex { font-size: 1.05em; }

/* A failed turn (network down, server error) renders as a clearly-marked
   error note rather than a tutor reply. */
.q-widget__msg--error {
  font-family: var(--sans);
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--neg-ink);
  background: color-mix(in srgb, var(--neg) 8%, var(--face));
  border: 1px solid color-mix(in srgb, var(--neg) 28%, var(--face));
  border-radius: 8px;
  padding: 0.5rem 0.7rem;
}

/* Composer: a single rounded box holding the (borderless, auto-growing)
   textarea and a circular arrow send-button, with an accent ring on focus. */
.q-widget__form {
  display: flex;
  align-items: flex-end;
  gap: 0.4rem;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--face);
  padding: 0.4rem 0.4rem 0.4rem 0.85rem;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.q-widget__form:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
}
.q-widget__input {
  flex: 1;
  /* border-box so the autogrow height (scrollHeight, which includes padding)
     is the box's real height — content-box double-counted the padding, adding
     phantom space below the text the moment autogrow first ran. */
  box-sizing: border-box;
  resize: none;
  font: inherit;
  font-size: 0.96rem;
  color: var(--ink);
  background: transparent;
  border: 0;
  padding: 0.35rem 0;
  line-height: 1.45;
}
.q-widget__input::placeholder { color: var(--muted); }
.q-widget__input:focus { outline: none; }

.q-widget__send {
  flex: 0 0 auto;
  width: 1.95rem;
  height: 1.95rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--on-ink);
  background: var(--accent);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  transition: background 0.15s ease, opacity 0.2s ease, transform 0.2s ease;
}
.q-widget__send svg { display: block; }
/* hover mixes toward ink: darkens in light mode, lightens in dark mode */
.q-widget__send:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 86%, var(--ink)); }
/* Empty / waiting: clearly disabled — greyed, faded and shrunk — so it scales
   and fades up to the full accent button the moment there's something to send. */
.q-widget__send:disabled {
  background: color-mix(in srgb, var(--rule) 70%, var(--face));
  color: var(--muted);
  cursor: default;
  opacity: 0.55;
  transform: scale(0.8);
}

/* The understanding meter now lives in the combined checkpoint's footer (see
   .c-meter), driven by chat.js's onScore callback. */

/* ---- Encoding widget ------------------------------------------------------ */

.w-widget {
  /* Field colours live at :root (--c-sign/--c-exp/--c-frac/--c-plain);
     widgets.js's ROLE_COLOR references the same vars. */
  position: relative; /* anchors the offscreen measuring probe */
  font-family: var(--sans);
  margin: 2.5rem 0;
  padding: 0.85rem 0.95rem;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--surface);
}
.w-widget--missing {
  color: var(--muted);
  font-size: 0.85rem;
  font-style: italic;
}

/* Bits render as fused strips — CSS grids built by widgets.js: a colour cap
   and label above each field, neutral-bordered cells, then per-digit
   contributions below (vulgar fractions for fractional bits), staggered onto
   a second row only where they'd otherwise collide. Wide float layouts split
   into two strips. All sizing is computed in JS (LAYOUT in widgets.js, tuned
   via widget-tuner.html); the running value sits beneath the strips. */
.w-strip { display: grid; align-items: stretch; }
.w-strip + .w-strip { margin-top: 10px; }

.w-flabel {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1.6;
  white-space: nowrap;
  overflow: hidden;
  padding-left: 1px;
}

/* The colour cap: a slim band sitting flush on its field's cells (they have
   no top border), casting a slight shadow onto them. Its left offset is set
   per band in widgets.js: interior fields shift −1px to track their cells'
   shared borders; the first field's cells don't shift, so nor does its cap. */
.w-band {
  position: relative;
  z-index: 3;
  box-shadow: 0 1.5px 2.5px color-mix(in srgb, var(--shadow-ink) 30%, transparent);
}

/* Mass set/clear gestures (attachGestures in widgets.js): a binary field's
   label and cap fill/clear the whole field on click. */
.w-flabel--fill, .w-band--fill { cursor: pointer; }
.w-flabel--fill:hover { text-decoration: underline dotted; }
.w-band--fill:hover { filter: brightness(1.3); }

.w-cell {
  --role: var(--c-plain); /* set per cell by widgets.js */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-family: var(--mono);
  line-height: 1;
  color: var(--muted);
  background: var(--face);
  /* Neutral borders: with the fields fused, the caps and fills carry the
     colour; borders just draw the grid. */
  border: 1px solid var(--hairline);
  border-top: 0; /* the colour cap is the top edge */
  border-radius: 0;
  margin-left: -1px;
  cursor: pointer;
  position: relative;
  text-align: center; /* buttons centre via flex; input cells need this */
  user-select: none; /* double-click floods a field; don't select instead */
  -webkit-user-select: none;
}
.w-cell--first { margin-left: 0; }
.w-cell:hover { border-color: var(--role); z-index: 2; }
.w-cell--on {
  background: var(--role);
  border-color: var(--role);
  color: var(--on-ink);
  font-weight: 600;
  z-index: 1;
}
.w-cell--input { cursor: text; user-select: text; -webkit-user-select: text; }
.w-cell--input:focus {
  outline: none;
  border-color: var(--role);
  background: color-mix(in srgb, var(--role) 18%, var(--face));
  z-index: 2;
}
.w-cell--input::selection { background: color-mix(in srgb, var(--role) 35%, var(--face)); }

/* The digit's contribution below its cell (blank when zero); fractions render
   as sup/sub stacks. A dropped (staggered) label carries a leader tick back
   up to its cell. */
.w-part {
  font-family: var(--mono);
  text-align: center;
  white-space: nowrap;
  line-height: 1.3;
  position: relative;
  overflow: visible;
}
.w-part sup, .w-part sub { line-height: 0; }
.w-tick { position: absolute; left: 50%; width: 1px; background: var(--hairline); }

/* Radix point between integer and fraction (fixed mode), on its own slim
   grid track, sitting on the cells' baseline. */
.w-radix {
  font-family: var(--mono);
  font-size: 1rem;
  font-weight: 700;
  color: var(--ink);
  align-self: end;
  justify-self: center;
  padding-bottom: 2px;
}
/* Leading "0." on fraction-only widgets: centre on the cell digits rather
   than sitting on the cells' baseline. */
.w-radix--lead { align-self: center; padding-bottom: 0; }

/* Offscreen probe widgets.js uses to measure label widths (stagger decisions). */
.w-measure {
  position: absolute;
  visibility: hidden;
  white-space: nowrap;
  font-family: var(--mono);
}

/* Running value, beneath the strips. */
.w-valuebox { margin-top: 0.55rem; }
.w-value {
  font-family: var(--mono);
  font-size: 1.05rem;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.w-formula {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--muted);
  margin-top: 0.1rem;
  overflow-wrap: anywhere;
}
.w-formula sup { font-size: 0.78em; }
/* Colour marks what the stored bits contribute; the implicit lead, base and
   bias stay neutral. Every part explains itself on hover. */
.w-formula .w-fsign { color: color-mix(in srgb, var(--c-sign) var(--mix-formula), var(--muted)); }
.w-formula .w-fsig { color: color-mix(in srgb, var(--c-frac) var(--mix-formula), var(--muted)); }
.w-formula .w-fexp { color: color-mix(in srgb, var(--c-exp) var(--mix-formula), var(--muted)); }
.w-formula [title] { cursor: help; }

/* Compare mode: two encodings stacked, with the computed (in)equality sign
   between them. The sign is the punchline: a solid disc riding a divider
   rule that ties the two rows together — quiet neutral grey in every state
   (equal, unequal, unordered NaN); the symbol alone carries the verdict.
   widgets.js still stamps data-cmp for tests/tooling. */
.w-versus {
  --cmp: var(--muted);
  display: flex;
  align-items: center;
  gap: 0.8rem;
  margin: 0.55rem 0;
}
.w-versus::before,
.w-versus::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: color-mix(in srgb, var(--cmp) 40%, var(--rule));
}
.w-versus__sym {
  flex: none;
  width: 2.2rem;
  height: 2.2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--cmp);
  color: var(--on-ink);
  font-family: var(--mono);
  font-size: 1.3rem;
  font-weight: 700;
  line-height: 1;
}

/* Size / bias controls. The rule above them only appears when they exist. */
.w-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.2rem;
  margin-top: 0.85rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--rule);
}
.w-controls:empty { display: none; }
.w-control { display: flex; align-items: center; gap: 0.5rem; }
.w-control__label {
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.w-stepper { display: flex; align-items: center; gap: 0.4rem; }
.w-step {
  width: 1.5rem;
  height: 1.5rem;
  font-family: var(--mono);
  font-size: 0.95rem;
  line-height: 1;
  color: var(--accent-ink);
  background: var(--face);
  border: 1px solid var(--rule);
  border-radius: 4px;
  cursor: pointer;
}
.w-step:hover { border-color: var(--accent); }
.w-step__count {
  font-family: var(--mono);
  font-size: 0.85rem;
  min-width: 1.2rem;
  text-align: center;
}
.w-bias {
  width: 4.5rem;
  font: inherit;
  font-family: var(--mono);
  font-size: 0.85rem;
  padding: 0.2rem 0.35rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
}
.w-bias:focus { outline: none; border-color: var(--accent); }

/* The adder (sum mode): decimal inputs drive machine-written strips; a rounding
   annotation sits under each running value (the exact intermediate sum is
   folded into the result's annotation). */
.w-cell--ro { cursor: default; }
.w-cell--ro:hover { border-color: var(--hairline); z-index: auto; }
.w-decimal {
  width: 8rem;
  font: inherit;
  font-family: var(--mono);
  font-size: 0.9rem;
  padding: 0.2rem 0.4rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
}
.w-decimal:focus { outline: none; border-color: var(--accent); }
.w-note {
  font-family: var(--sans);
  font-size: 0.8rem;
  color: var(--ink-soft);
  margin-top: 0.3rem;
}
.w-note--ok { color: var(--accent); } /* nothing rounded — the happy annotation */

/* The adder ledger: a shared field-label header, then one row per operand —
   [operator] [decimal input] [read-out strip] [stored value + rounding note].
   All three strips share the middle column, so their cells line up; baseline
   alignment then lands each stored value on its strip's cell digits, with the
   note hanging below (and pushing the next row down a touch). */
.w-ledger {
  display: grid;
  /* The strip column is max-content (not 1fr) so the stored value sits right
     after the last bit instead of across a gap of stretched track. */
  grid-template-columns: auto auto max-content auto;
  align-items: baseline;
  column-gap: 0.7rem;
  row-gap: 0.7rem;
  margin: 0.3rem 0 0.2rem;
}
.w-ledger__head { align-self: end; }
.w-ledger__labels { width: max-content; }
.w-ledger__op {
  font-family: var(--mono);
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1;
  color: var(--ghost);
  justify-self: center;
}
.w-ledger__in { justify-self: stretch; }
.w-ledger__in .w-decimal { width: 3.6rem; }
.w-ledger__res { display: flex; flex-direction: column; gap: 0; min-width: 0; }
.w-ledger__res .w-value { white-space: nowrap; font-size: 0.9375rem; } /* 15px, per adder-tuner */
.w-ledger__res .w-note { max-width: 20rem; margin-top: 0.05rem; line-height: 1.35; }
/* The read-out strips carry no colour cap (labels/bands live once in the header)
   and wear a clearly-disabled look — cream cells, muted on-bits — so they read
   as machine-written and un-clickable, distinct from the interactive widgets. */
.w-ledger .w-cell {
  border-top: 1px solid var(--hairline);
  background: var(--ro-face);
  color: var(--ro-ink);
}
.w-ledger .w-cell--on {
  background: color-mix(in srgb, var(--role) var(--mute-cap), var(--wash));
  border-color: color-mix(in srgb, var(--role) var(--mute-cap), var(--ro-edge));
  border-top-color: color-mix(in srgb, var(--role) var(--mute-cap), var(--ro-edge));
  color: var(--on-ink);
}

/* Narrow panes: when even minimum-width cells can't fit beside the ledger's
   other columns (widgets.js flips this class from the same cell-width math
   that sizes the strips — it tracks the pane, not the viewport), the ledger
   stacks. Each row becomes [operator + input] / full-width strip / stored
   value + note; the strips still share the header's cell grid, so the
   columns keep lining up vertically. */
.w-ledger--narrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 0.5rem;
  row-gap: 0.35rem;
}
.w-ledger--narrow .w-ledger__head,
.w-ledger--narrow .w-strips { width: 100%; } /* force each onto its own line */
.w-ledger--narrow .w-ledger__res {
  width: 100%;
  margin-bottom: 0.55rem; /* breathing room between operands */
}
.w-ledger--narrow .w-ledger__res:last-child { margin-bottom: 0; }
.w-ledger--narrow .w-ledger__res .w-value { white-space: normal; overflow-wrap: anywhere; }
.w-ledger--narrow .w-ledger__res .w-note { max-width: none; }

.c-widget {
  /* Breaks out wider than the centred prose+notes column, centred on the page. */
  width: min(70rem, 94vw);
  margin: 3rem 0;
  margin-left: 50%;
  transform: translateX(-50%);
  clear: both; /* drop below any floated sidenote so the two never overlap */
  background: var(--surface);
  border-radius: 8px;
  box-shadow: 0 14px 34px -10px color-mix(in srgb, var(--shadow-ink) 25%, transparent),
              0 3px 8px color-mix(in srgb, var(--shadow-ink) 7%, transparent);
  overflow: hidden; /* clip the segmented footer to the rounded corners */
}

.c-body {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 1.1rem;
  padding: 0.9rem 1.0rem 0.7rem;
}

/* Playground on the left, chat on the right; each can grow, and they stack on
   narrow screens. */
.c-left {
  flex: 1 1 15rem;
  min-width: 0;
}
.c-right {
  flex: 2 2 30rem;
  min-width: 0;
  min-height: 14rem;
  display: flex;
}
.c-left .c-mount { height: 100%; }

/* A divider between the two panes, but only when they sit side by side — when
   they stack on narrow screens the gap alone separates them. */
@media (min-width: 56rem) {
  .c-right {
    border-left: 1px solid var(--rule);
    padding-left: 1.1rem;
  }
  /* Wide (split, 32-bit) widgets earn a bigger share of the box, so their
     fraction strip isn't cramped into a third of the width. */
  .c-left:has(.w-widget--wide) { flex: 2.2 2 24rem; }
}

/* The adder is a wide horizontal ledger — a row per operand across the full
   width — so its checkpoint stacks the playground above the chat rather than
   sitting side by side (the ledger would be unreadable squeezed into a third
   of the box). Keyed on the sum widget's own class. */
.c-body:has(.w-widget--sum) {
  flex-direction: column;
}
.c-body:has(.w-widget--sum) .c-left,
.c-body:has(.w-widget--sum) .c-right {
  flex: 0 0 auto;
  width: 100%;
  min-width: 0;
}
.c-body:has(.w-widget--sum) .c-right {
  border-left: 0;
  padding-left: 0;
  border-top: 1px solid var(--rule);
  padding-top: 1rem;
}

/* Drop the standalone card styling from the embedded widget — it lives inside
   the combined box now. */
.c-left .w-widget {
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

/* The embedded chat fills its pane: a scrolling log above, the form pinned to
   the bottom. */
.c-right .q-widget {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  padding: 0;
  border-left: 0;
  display: flex;
  flex-direction: column;
}
.c-right .q-widget__log {
  flex: 1 1 auto;
  overflow-y: auto;
  max-height: 22rem;
  margin-bottom: 0.75rem;
}
.c-right .q-widget__form { margin-top: auto; }

/* A closed conversation: once the student engages a later checkpoint's chat,
   earlier ones lose their composer (the transcript becomes part of the
   tutor's frozen record — see chat.js freeze()) and this line takes its
   place. */
.q-frozen__note {
  font-family: var(--sans);
  font-size: 0.78rem;
  color: var(--muted);
  margin-top: auto;
  padding-top: 0.5rem;
  border-top: 1px solid var(--hairline);
  line-height: 1.45;
}

/* ---- Solo mode (no tutor) --------------------------------------------------
   The anonymous experience: same pane, but one written answer instead of a
   conversation. The note under the composer is empty (and collapsed) until
   the answer freezes to a confirmation, or a submit fails to an error.
   No understanding meter — nothing is graded. */
.c-widget--solo .c-meter { display: none; }
.q-solo__note {
  font-family: var(--sans);
  font-size: 0.78rem;
  color: var(--muted);
  margin-top: 0.5rem;
  line-height: 1.45;
}
.q-solo__note:empty { display: none; }
.q-solo__note--done { color: var(--accent-ink, var(--accent)); }
.q-solo__note.q-widget__msg--error { color: var(--neg); }

/* The budget-degradation banner: a signed-in user's checkpoint falling back
   to self-paced while the tutor is over its daily budget. */
.q-solo__banner {
  font-family: var(--sans);
  font-size: 0.8rem;
  color: var(--muted);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--neg);
  border-radius: 6px;
  padding: 0.5rem 0.7rem;
  margin-bottom: 0.6rem;
  line-height: 1.45;
}

/* ---- The chooser ------------------------------------------------------------
   An anonymous visitor's fork in the road, sitting where the chat would be:
   tutor (sign in) or solo. Two stacked cards, same skeleton whether <a> or
   <button>. */
.c-choose {
  font-family: var(--sans);
  flex: 1 1 auto;   /* fill the pane, like the chat it stands in for */
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  justify-content: center;
}
.c-choose__lead {
  margin: 0 0 0.35rem;
  font-size: 0.92rem;
  color: var(--ink-soft);
}
.c-choose__opt {
  display: block;
  width: 100%;
  box-sizing: border-box;
  text-align: left;
  font: inherit;
  text-decoration: none;
  color: var(--ink);
  background: var(--face);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  padding: 0.7rem 0.9rem;
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.c-choose__opt:hover {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent);
}
.c-choose__opt b { display: block; font-size: 0.95rem; margin-bottom: 0.15rem; }
.c-choose__opt span.c-choose__down {
  color: var(--neg);
  margin-top: 0.35rem;
  font-size: 0.78rem;
}
.c-choose__opt span {
  display: block;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--muted);
}

/* Account chip: the quiet crossover affordance (sign out / go sign in).
   Bottom-left — clear of the centred progress strip and the dev/palette
   chips (top-right / bottom-right). */
.c-account {
  position: fixed;
  left: 1rem;
  bottom: 0.9rem;
  z-index: 900;
  font-family: var(--sans);
  font-size: 0.72rem;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 0.35rem 0.6rem;
  opacity: 0.7;
  transition: opacity 0.15s ease;
}
.c-account:hover { opacity: 1; }
.c-account a { color: inherit; }

/* Understanding meter: one continuous bar, inset from the card edges so it
   floats clear of the rounded corners, with fully-rounded outer ends. Gap-less
   cells are divided by hairlines in the card colour, so it reads as a single
   progress bar split into five. */
.c-meter {
  display: flex;
  gap: 0;
  padding: 0.25rem 1rem 0.8rem;
}
.c-seg {
  flex: 1 1 0;
  height: 11px;
  background: color-mix(in srgb, var(--rule) 55%, var(--face));
  transition: background 0.4s ease;
}
.c-seg:first-child { border-radius: 999px 0 0 999px; }
.c-seg:last-child  { border-radius: 0 999px 999px 0; }
.c-seg + .c-seg { box-shadow: inset 1.5px 0 0 var(--surface); } /* card-coloured divider */
.c-seg--on { background: var(--accent); }
.c-widget--passed .c-seg--on { background: var(--ok); }

/* ---- Progressive reveal --------------------------------------------------
   Each checkpoint's section stays hidden until the previous checkpoint is
   graded at UNLOCK_SCORE (see combined.js). The FIRST locked section isn't
   fully hidden, though: its opening lines show as a teaser — blurred, faded,
   dissolving into the page background — so the student can see the article
   continues past the checkpoint without being able to read ahead (the next
   paragraph usually spoils the answer). Settings chosen via teaser-styles.html
   (variant B) — keep the two in sync. */
.c-section--locked { display: none; }

.c-section--tease {
  display: block;
  position: relative;
  max-height: 12rem;      /* roughly four lines of prose */
  overflow: hidden;
  pointer-events: none;   /* combined.js also sets `inert` (focus, selection) */
  user-select: none;
}
/* Blur the content, not the veil — per-child, so the ::after gradient (and
   its fade to the page background) stays crisp. */
.c-section--tease > * {
  filter: blur(2px);
  opacity: 0.55;
}
.c-section--tease::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, transparent 50%, var(--bg) 96%);
}
/* While a teaser is dissolving the bottom of the page, the body's big
   end-of-article padding (9rem, clearance for the fixed progress strip) would
   read as "that's all there is" — shrink it so the strip sits right on the
   fade. Once every section is revealed there's no tease and the full padding
   returns for the real ending. */
body:has(.c-section--tease) { padding-bottom: 1.5rem; }

/* A teased section being unlocked: the clip grows to the section's real height
   (driven from combined.js — it needs scrollHeight), while the blur, dimming
   and veil melt away. */
.c-section--unveiling {
  display: block;
  position: relative;
  overflow: hidden;
  transition: max-height 0.9s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.c-section--unveiling > * {
  filter: none;
  opacity: 1;
  transition: filter 0.7s ease, opacity 0.7s ease;
}
.c-section--unveiling::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, transparent 50%, var(--bg) 96%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.7s ease;
}

/* Sections revealed without having been teased (shouldn't normally happen)
   still slide in the old way. */
.c-section--revealed {
  animation: c-section-reveal 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@keyframes c-section-reveal {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: translateY(0); }
}

.c-widget--justpassed .c-meter {
  animation: c-meter-pulse 1.3s ease 2;
  border-radius: 2px;
}
@keyframes c-meter-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 45%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ok) 45%, transparent); }
}

@media (prefers-reduced-motion: reduce) {
  .c-section--revealed { animation: none; }
  .c-section--unveiling,
  .c-section--unveiling > *,
  .c-section--unveiling::after { transition: none; }
  .c-widget--justpassed .c-meter { animation: none; }
  .q-widget__send { transition: none; }
  .q-widget__send:disabled { transform: none; } /* stay greyed/faded, just don't animate */
}

/* ---- Global tutorial progress ---------------------------------------------
   A night-sky strip fixed at the bottom of the viewport: one star per
   checkpoint. Stars sit dull grey until their checkpoint passes (score ≥ 4),
   light up white when it does, and blaze gold at a perfect 5. Deliberately
   unlike the green understanding meters. Parameters were chosen with the
   constellation.html tuner — keep the two in sync when adjusting. */
.g-progress {
  --star: 18px;      /* base star size */
  --aced-scale: 1.3; /* gold stars grow by this factor */
  position: fixed;
  bottom: 0.9rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 900;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 6px 16px;
  background: var(--sky);
  border: 1px solid rgba(0, 0, 0, 0.25);
  border-radius: 999px;
  box-shadow: 0 5px 18px color-mix(in srgb, var(--shadow-ink) 22%, transparent);
}
.g-box {
  /* Every cell reserves the aced size, so a star growing gold shifts nothing. */
  width: calc(var(--star) * var(--aced-scale));
  height: calc(var(--star) * var(--aced-scale));
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--star);
  line-height: 1;
  color: var(--star-dim);
  opacity: 0.3;
  background: none;
  border: 0;
  cursor: pointer;
  transition: color 0.3s ease, opacity 0.3s ease, text-shadow 0.3s ease,
              font-size 0.3s ease, transform 0.15s ease;
}
.g-box::after { content: "★"; }
.g-box:hover:not(.g-box--locked) { transform: scale(1.2); }
.g-box:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; border-radius: 50%; }
.g-box--locked { cursor: default; }
/* The frontier star glows a touch brighter: "you are here", statically. */
.g-box--current { opacity: 0.55; }
.g-box--passed { color: var(--star-lit); opacity: 1; }
/* A perfect 5: bigger, gold, and lit from within. */
.g-box--aced {
  color: var(--gold);
  font-size: calc(var(--star) * var(--aced-scale));
  text-shadow: 0 0 4px var(--gold);
}

@media (max-width: 560px) {
  .g-progress { --star: 12px; gap: 1px; padding: 5px 10px; }
}

/* Local-dev-only toggle to reveal every section at once (combined.js). It sits
   top-right so it never collides with the bottom-centre progress strip. */
.c-devtoggle {
  position: fixed;
  right: 1rem;
  top: 1rem;
  z-index: 1000;
  font-family: var(--sans);
  font-size: 0.7rem;
  letter-spacing: 0.02em;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 0.4rem 0.6rem;
  cursor: pointer;
  opacity: 0.55;
  box-shadow: 0 2px 8px color-mix(in srgb, var(--shadow-ink) 12%, transparent);
  transition: opacity 0.15s ease;
}
.c-devtoggle:hover { opacity: 1; }
.c-devtoggle--on { color: var(--accent-ink); border-color: var(--accent); }

/* ---- Auth screens ---------------------------------------------------------
   The sign-in page (login.html) and the server-generated email-sent / error
   pages (server.py's _auth_page + message.html): small standalone screens in
   the tutorial's own voice — sans display heading, serif prose, and the chat
   composer's rounded-box form. All three share these classes. */
.auth {
  max-width: 26rem;
  margin: 0 auto;
  padding-top: 6vh;
}
.auth__title {
  font-family: var(--sans);
  font-size: 1.7rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 0 0 1rem;
}
.auth p { margin: 0 0 1.4rem; font-size: 1.02rem; line-height: 1.55; }

.auth__google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.55rem 1rem;
  border: 1px solid var(--rule);
  border-radius: 10px;
  background: var(--face);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 0.95rem;
  text-decoration: none;
  transition: border-color 0.15s ease;
}
.auth__google:hover { border-color: var(--muted); }
.auth__google svg { flex: none; }

.auth__or {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  margin: 1.4rem 0;
  color: var(--muted);
  font-family: var(--sans);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.auth__or::before, .auth__or::after { content: ""; flex: 1; border-top: 1px solid var(--rule); }

/* The email form is the tutor composer, verbatim: same box, same focus ring
   (see .q-widget__form) — the send button is labelled rather than an arrow. */
.auth__form {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--face);
  padding: 0.4rem 0.4rem 0.4rem 0.85rem;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.auth__form:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
}
.auth__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 0.95rem;
  padding: 0.35rem 0;
}
.auth__input:focus { outline: none; }
.auth__input::placeholder { color: var(--muted); }
.auth__send {
  flex: none;
  border: 0;
  border-radius: 10px;
  padding: 0.45rem 0.85rem;
  background: var(--accent);
  color: var(--on-ink);
  font-family: var(--sans);
  font-size: 0.88rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.15s ease;
}
.auth__send:hover { background: color-mix(in srgb, var(--accent) 86%, var(--ink)); }

.auth__note {
  margin-top: 0.8rem;
  color: var(--muted);
  font-family: var(--sans);
  font-size: 0.78rem;
  line-height: 1.5;
}
.auth__alt {
  margin-top: 2rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--rule);
  font-size: 0.98rem;
  color: var(--ink-soft);
}
.auth__dev {
  display: inline-block;
  margin-top: 1.1rem;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--muted);
}
