/* ============================================================
   Invariant — styles.

   Dark-first. Every colour flows from a token defined on :root
   here, and css/themes.css redefines that same set per
   purchasable theme. tests/browser/styles.test.mjs checks that
   each theme defines the whole set: a theme missing --text is
   invisible text, which is worth a test rather than a surprise.

   Layout is deliberately calm — one accent, a small type scale,
   a single elevation step, and a line-length limit on prose. The
   problems are the content; the chrome should not compete.
   ============================================================ */

/* The default palette, which is also what a theme preview keyed to
   "theme-dark" has to show. Declaring both selectors on one block means the
   preview can never drift from the real thing — there is only one list of
   values. Without the second selector a dark-theme preview would inherit
   whatever theme the reader is currently wearing. */
:root,
[data-theme-preview="theme-dark"] {
  /* surfaces and text */
  --bg:            #0b0e13;
  --bg-elev:       #11161f;
  --bg-elev-2:     #1a212d;
  --bg-inset:      #070a0e;
  --border:        #1f2733;
  --border-strong: #2e3947;
  --text:          #e6edf5;
  /* Secondary text is deliberately bright: --text-dim carries whole sentences
     (a track blurb, a problem's meta line) and --text-faint carries asides.
     tests/contrast.test.mjs holds dim to 4.5:1 and faint to 3:1 on every
     surface in every theme; these sit far above both, because "legal" and
     "comfortable to read for an hour" are different bars. */
  --text-dim:      #aebbcb;
  --text-faint:    #7f8da6;

  /* the accent, overridable on its own by [data-accent] */
  --accent:        #4f9dff;
  --accent-text:   #06090d;
  --accent-soft:   rgba(79, 157, 255, .13);

  --ok:            #3ddc91;
  --warn:          #ecb045;
  --danger:        #ff6b6b;

  /* difficulty — cool to hot, and always written next to the colour so
     nothing depends on telling them apart by hue alone */
  --diff-beginner:     #3ddc91;
  --diff-intermediate: #ecb045;
  --diff-advanced:     #ff6b6b;

  /* where a problem has got to */
  --st-unsolved:  #67748a;
  --st-attempted: #ecb045;
  --st-solved:    #3ddc91;
  --st-read:      #b393ff;

  /* syntax */
  --hl-keyword: #ff7bb0;
  --hl-type:    #7dd3fc;
  --hl-string:  #a5e887;
  --hl-comment: #6f7c90;
  --hl-num:     #f0b86e;
  --hl-fn:      #c3a6ff;
  --hl-literal: #f0b86e;
  --hl-preproc: #8fa7c4;

  /* scale */
  --s1: .25rem; --s2: .5rem;  --s3: .75rem; --s4: 1rem;
  --s5: 1.5rem; --s6: 2rem;   --s7: 3rem;   --s8: 4rem;

  --radius-s:  7px;
  --radius:    11px;
  --radius-l:  16px;

  --shadow-1:  0 1px 2px rgba(0, 0, 0, .3);
  --shadow-2:  0 8px 24px rgba(0, 0, 0, .35);
  --shadow-3:  0 18px 50px rgba(0, 0, 0, .45);

  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;

  --header-h: 3.5rem;
}

[data-accent="accent-amber"]   { --accent: #f0b86e; --accent-text: #1b1206; --accent-soft: rgba(240, 184, 110, .15); }
[data-accent="accent-mint"]    { --accent: #4ade80; --accent-text: #052112; --accent-soft: rgba(74, 222, 128, .15); }
[data-accent="accent-magenta"] { --accent: #f472b6; --accent-text: #250316; --accent-soft: rgba(244, 114, 182, .15); }
[data-accent="accent-cyan"]    { --accent: #22d3ee; --accent-text: #04212a; --accent-soft: rgba(34, 211, 238, .15); }

* { box-sizing: border-box; }

/* The hidden attribute must win over any class that sets display. A class
   selector outranks the browser's built-in [hidden] rule, so anything styled
   display:flex/grid would otherwise stay on screen while "hidden" — and an
   absolutely positioned overlay would then swallow every click. */
[hidden] { display: none !important; }

html, body { height: 100%; }
html { font-size: 15px; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 1rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

h1, h2, h3, h4, h5 { line-height: 1.25; margin: 0 0 var(--s2); font-weight: 640; letter-spacing: -.011em; }
h1 { font-size: 1.6rem; }
h2 { font-size: 1.2rem; }
h3 { font-size: 1.02rem; }
h4 { font-size: .95rem; }

code, kbd, pre, .mono { font-family: var(--mono); }

kbd {
  font-size: .76em; padding: .12em .42em;
  border: 1px solid var(--border-strong); border-bottom-width: 2px;
  border-radius: 5px; background: var(--bg-elev-2); color: var(--text-dim);
}

::selection { background: color-mix(in srgb, var(--accent) 30%, transparent); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

/* ---------------- buttons and controls ---------------- */

.btn {
  font: inherit; font-size: .9rem; font-weight: 500;
  padding: .45em .95em;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-s);
  background: var(--bg-elev-2); color: var(--text);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: .4em;
  transition: background .13s, border-color .13s, transform .07s, color .13s;
  white-space: nowrap;
}
.btn:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-soft); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: .42; cursor: not-allowed; }

.btn-primary {
  background: var(--accent); color: var(--accent-text);
  border-color: var(--accent); font-weight: 620;
  box-shadow: var(--shadow-1);
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.08); background: var(--accent); color: var(--accent-text); }

.btn-ghost { background: transparent; border-color: transparent; color: var(--text-dim); }
.btn-ghost:hover:not(:disabled) { color: var(--text); background: var(--bg-elev-2); border-color: transparent; }

.btn-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, transparent); }
.btn-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--danger) 12%, transparent); border-color: var(--danger); }

.btn-sm { font-size: .82rem; padding: .32em .7em; }

input, select, textarea {
  font: inherit;
  color: var(--text);
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-s);
  padding: .42em .65em;
  transition: border-color .13s;
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
select { cursor: pointer; }

/* ---------------- header ---------------- */

.topbar {
  display: flex; align-items: center; gap: var(--s4);
  height: var(--header-h);
  padding: 0 var(--s5);
  background: color-mix(in srgb, var(--bg-elev) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 40;
}

.brand { display: flex; align-items: center; gap: .55rem; flex: none; }
.brand-mark { width: 24px; height: 24px; flex: none; color: var(--accent); }
.brand-text h1 { font-size: .98rem; margin: 0; letter-spacing: -.015em; }
.brand-text h1 a { color: inherit; }
.brand-text h1 a:hover { text-decoration: none; color: var(--accent); }
.brand-text p { margin: 0; font-size: .68rem; color: var(--text-faint); letter-spacing: .01em; }

/* One entry, and a panel under it. No scrolling box around the nav any
   more: there is one button in it, and an overflow of its own would clip
   the panel it opens. */
.topbar-nav { display: flex; margin-left: var(--s4); flex: none; }

.navmenu { position: relative; }
.navmenu-btn {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .36em .75em; border-radius: 999px;
  background: transparent; border: 1px solid transparent;
  color: var(--text-dim); font: inherit; font-size: .88rem; font-weight: 550;
  cursor: pointer; white-space: nowrap;
  transition: color .13s, background .13s, border-color .13s;
}
.navmenu-btn:hover { color: var(--text); background: var(--bg-elev-2); }
.navmenu-btn[data-active="true"] { color: var(--text); }
.navmenu-btn[aria-expanded="true"] {
  color: var(--text); background: var(--bg-elev-2); border-color: var(--border);
}
.navmenu-caret { width: 10px; height: 6px; flex: none; transition: transform .15s; }
.navmenu-btn[aria-expanded="true"] .navmenu-caret { transform: rotate(180deg); }

/* The gap between the button and the panel is crossed by the pointer, so
   the panel starts flush under the bar and the padding does the spacing.
   A real gap here makes the menu shut while you are reaching for it. */
.navmenu-panel {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 60;
  display: grid; grid-auto-flow: column; grid-auto-columns: 15.5rem;
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius-l); box-shadow: var(--shadow-2);
  padding: var(--s3); gap: 0;
}
.navmenu-panel[hidden] { display: none; }
/* A rule between the columns, not around them: the panel is one object. */
.navmenu-col { padding: 0 var(--s3); min-width: 0; }
.navmenu-col + .navmenu-col { border-left: 1px solid var(--border); }
.navmenu-col-head {
  margin: .1rem 0 .5rem .5rem;
  font-size: .64rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--text-faint);
}
.navmenu-item {
  display: flex; gap: .55rem; align-items: flex-start;
  padding: .45rem .5rem; border-radius: var(--radius-s);
  color: inherit; text-decoration: none;
  transition: background .12s;
}
.navmenu-item:hover, .navmenu-item:focus-visible {
  background: var(--bg-elev-2); text-decoration: none;
}
.navmenu-item[aria-current="page"] { background: var(--bg-inset); }
.navmenu-icon {
  width: 16px; height: 16px; flex: none; margin-top: .12rem;
  color: var(--text-faint);
  fill: none; stroke: currentColor; stroke-width: 1.3;
  stroke-linecap: round; stroke-linejoin: round;
}
.navmenu-item:hover .navmenu-icon,
.navmenu-item[aria-current="page"] .navmenu-icon { color: var(--accent); }
.navmenu-text { display: flex; flex-direction: column; gap: .12rem; min-width: 0; }
.navmenu-title {
  display: flex; align-items: center; gap: .4rem;
  font-size: .87rem; font-weight: 600; color: var(--text);
}
.navmenu-badge {
  font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  padding: .1rem .35rem; border-radius: 4px;
  color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
  white-space: nowrap;
}
.navmenu-count { font-family: var(--mono); margin-left: auto; }
.navmenu-blurb { font-size: .76rem; line-height: 1.45; color: var(--text-dim); }

.purse { display: flex; align-items: center; gap: var(--s3); font-size: .84rem; flex: none; }
.purse .xp, .purse .coin { display: inline-flex; align-items: baseline; gap: .3rem; font-family: var(--mono); font-size: .8rem; }
.purse b { font-size: .9rem; font-weight: 650; }
.purse .coin { color: var(--warn); }
.purse .xp   { color: var(--text-dim); }
.purse .streak {
  color: var(--danger); font-weight: 600;
  padding: .15em .55em; border-radius: 999px;
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

.main { max-width: 1180px; margin: 0 auto; padding: var(--s5) var(--s5) var(--s8); }
.main.wide { max-width: 1480px; }

/* Inputs the reader added, beside the problem's own samples. Visibly
   theirs: a different heading, and no "expected" column, because there
   is no expected — that is the whole difference between these and the
   samples. */
.cases-head { margin: var(--s4) 0 .3rem; font-size: .8rem; color: var(--text-dim); }
.cases-head:first-child { margin-top: 0; }
.mycases { margin-top: var(--s4); padding-top: var(--s3); border-top: 1px dashed var(--border); }
.mycase-list { display: flex; flex-direction: column; gap: var(--s3); margin-top: var(--s3); }
.mycase {
  padding: var(--s3); border-radius: var(--radius-s);
  background: var(--bg-inset); border: 1px solid var(--border);
}
.mycase-top { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.mycase-in {
  width: 100%; margin-top: .3rem; font-family: var(--mono); font-size: .8rem;
  padding: .4rem; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-s);
  resize: vertical;
}
.mycase-out { margin-top: .4rem; }
.mycase-out h5 { margin: 0 0 .2rem; font-size: .7rem; color: var(--text-faint);
  text-transform: uppercase; letter-spacing: .05em; }
.mycase-out pre {
  margin: 0; padding: .4rem; max-height: 12rem; overflow: auto;
  background: var(--bg); border-radius: var(--radius-s); font-size: .78rem;
}

/* ---------------- the one dialog ----------------

   Importing is the only thing on this site that can destroy work in
   one click, so it is the only thing that gets a dialog. Everything
   else asks with a confirm() or does not need asking. */
.modal {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center;
  padding: var(--s4); background: rgba(0, 0, 0, .55);
  overflow-y: auto;
}
.modal-card {
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-2);
  padding: var(--s5); max-width: 40rem; width: 100%;
  max-height: calc(100vh - 2 * var(--s4)); overflow-y: auto;
}
.modal-card h2 { margin: 0 0 .2rem; font-size: 1.15rem; }
.imp-head { margin: var(--s4) 0 .3rem; font-size: .82rem; color: var(--text-dim); }
.imp-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0 var(--s4);
}
.imp-row {
  display: flex; justify-content: space-between; gap: var(--s3);
  padding: .18rem 0; font-size: .85rem; border-bottom: 1px solid var(--border);
}
.imp-k { color: var(--text-dim); }
.imp-v { font-family: var(--mono); color: var(--text); }
.imp-impact { margin: .3rem 0 0; padding-left: 1.1rem; }
.imp-impact li { margin: .4rem 0; font-size: .86rem; color: var(--text-dim); }
.imp-impact b { color: var(--text); }
.imp-warn { color: var(--warn); margin: var(--s3) 0 0; }

/* ---------------- scrollbars ----------------

   The reported bug: a bright scrollbar under a code block on a dark
   page. The cause was not a missing scrollbar rule — it was a missing
   `color-scheme`. Without it the browser has no idea the page is dark
   and paints its light native scrollbar, which is the single most
   visible thing on the screen in the attached screenshot.

   So, in order:

     1. `color-scheme` tells the browser what kind of page this is. It
        is what makes the NATIVE scrollbar dark, and it also fixes form
        controls, the resize handle and the space between two
        scrollbars — none of which a `::-webkit-scrollbar` rule can
        reach.
     2. `scrollbar-color` is the standard property, honoured by Firefox
        and by Chromium. It inherits, so declaring it once at the root
        reaches every scroll container on the site, including ones no
        rule here knows about.
     3. the `::-webkit-scrollbar` pseudo-elements are the only way to
        style the track, the thumb's hover and active states, and the
        corner where a horizontal and a vertical scrollbar meet. They
        do not inherit, so they are declared universally.

   Everything is a theme token, so switching theme repaints them with
   the rest of the page and no component has to be told.

   What this deliberately does NOT do: hide anything, or replace native
   scrolling with JavaScript. The wheel, the trackpad, the keyboard,
   touch, and dragging the thumb all keep working because none of them
   is touched. */

:root {
  /* The browser's own idea of the page, which is what darkens the
     native scrollbar. Every theme but one is dark. */
  color-scheme: dark;

  /* Derived from the existing palette rather than invented, so a theme
     that defines the palette gets scrollbars for free. A theme that
     wants different ones can set these three. */
  --scroll-thumb:       var(--border-strong);
  --scroll-thumb-hover: var(--text-faint);
  --scroll-track:       var(--bg-inset);
}

html[data-theme="theme-paper"],
[data-theme-preview="theme-paper"] { color-scheme: light; }

/* Firefox, and Chromium's standard path. Inherited, so this is the
   whole of it for most of the site. */
:root {
  scrollbar-width: thin;
  scrollbar-color: var(--scroll-thumb) var(--scroll-track);
}

/* WebKit and Chromium's pseudo-elements, for the parts the standard
   property cannot express. */
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--scroll-track); }
::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb);
  border-radius: 7px;
  /* A border in the track's colour is what makes the thumb look inset
     rather than filling the gutter edge to edge. */
  border: 2px solid var(--scroll-track);
}
::-webkit-scrollbar-thumb:hover  { background: var(--scroll-thumb-hover); }
::-webkit-scrollbar-thumb:active { background: var(--accent); }
/* The square where a horizontal and a vertical scrollbar meet. Left
   alone it is the one remaining patch of browser default, and on a dark
   page it is a white square in the corner. */
::-webkit-scrollbar-corner { background: var(--scroll-track); }
/* The resize handle some browsers draw in that corner. */
::-webkit-resizer { background: var(--scroll-track); }

/* High contrast and forced colours: hand it all back. The system is
   making a deliberate choice about legibility and a theme token is not
   better informed than it is. */
@media (forced-colors: active) {
  :root { scrollbar-color: auto; }
  ::-webkit-scrollbar-thumb,
  ::-webkit-scrollbar-track,
  ::-webkit-scrollbar-corner { background: Canvas; }
}

/* ---------------- shared bits ---------------- */

.card {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s4);
}

.muted  { color: var(--text-dim); }
.faint  { color: var(--text-faint); }
.small  { font-size: .86rem; }
.tiny   { font-size: .76rem; }
.row    { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: var(--s4); }
.stack  { display: flex; flex-direction: column; gap: var(--s2); }
.grid   { display: grid; gap: var(--s4); }

.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s4); margin: var(--s6) 0 var(--s3);
}
.section-head h2 { margin: 0; }
.section-head .more { font-size: .82rem; color: var(--text-dim); }

.pill {
  display: inline-flex; align-items: center; gap: .3em;
  font-size: .69rem; font-weight: 640; letter-spacing: .03em;
  padding: .22em .6em; border-radius: 999px;
  border: 1px solid currentColor;
  text-transform: uppercase; white-space: nowrap;
}
.pill-plain {
  border-color: var(--border-strong); color: var(--text-dim);
  text-transform: none; font-weight: 500; letter-spacing: 0;
}

/* a tag, for topics and keywords — quieter than a pill */
.tag {
  display: inline-flex; align-items: center;
  font-size: .72rem; color: var(--text-faint);
  padding: .14em .5em; border-radius: 5px;
  background: var(--bg-elev-2); white-space: nowrap;
}

.diff-beginner     { color: var(--diff-beginner); }
.diff-intermediate { color: var(--diff-intermediate); }
.diff-advanced     { color: var(--diff-advanced); }

/* difficulty as a dot plus a word, so it never relies on colour alone */
.diff { display: inline-flex; align-items: center; gap: .4em; font-size: .8rem; font-weight: 550; white-space: nowrap; }
.diff::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }

.status-unsolved  { color: var(--st-unsolved); }
.status-attempted { color: var(--st-attempted); }
.status-solved    { color: var(--st-solved); }
.status-read      { color: var(--st-read); }

.bar { height: 5px; border-radius: 999px; background: var(--bg-inset); overflow: hidden; }
.bar > i { display: block; height: 100%; background: var(--accent); border-radius: 999px; transition: width .4s cubic-bezier(.3, .8, .3, 1); }

.empty {
  padding: var(--s7) var(--s4); text-align: center; color: var(--text-faint);
  border: 1px dashed var(--border); border-radius: var(--radius);
}
.empty h2 { color: var(--text-dim); }

.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .9rem; }
th, td { padding: .5em .7em; border-bottom: 1px solid var(--border); text-align: left; }
th { color: var(--text-dim); font-weight: 620; font-size: .74rem; text-transform: uppercase; letter-spacing: .04em; }
tbody tr:last-child td { border-bottom: 0; }

/* a progress ring, which reads better than a bar for a percentage */
.ring { width: 44px; height: 44px; flex: none; position: relative; }
.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); display: block; }
.ring circle { fill: none; stroke-width: 4; }
.ring .ring-track { stroke: var(--bg-inset); }
.ring .ring-fill  { stroke: var(--accent); stroke-linecap: round; transition: stroke-dashoffset .5s cubic-bezier(.3, .8, .3, 1); }
.ring .ring-label {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--mono); font-size: .66rem; color: var(--text-dim); font-weight: 600;
}

/* ---------------- prose (rendered markdown) ---------------- */

.prose { max-width: 68ch; }
.prose > :first-child { margin-top: 0; }
.prose p, .prose ul, .prose ol, .prose blockquote, .prose .table-wrap { margin: 0 0 1em; }
.prose li { margin: .25em 0; }
.prose h2 { margin: 1.6em 0 .5em; font-size: 1.15rem; }
.prose h3 { margin: 1.4em 0 .4em; font-size: 1rem; }
.prose h4 { margin: 1.2em 0 .3em; font-size: .93rem; color: var(--text-dim); }
.prose strong { font-weight: 640; color: var(--text); }
.prose code {
  font-size: .87em; padding: .12em .36em; border-radius: 5px;
  background: var(--bg-inset); border: 1px solid var(--border);
}
.prose blockquote {
  margin-left: 0; padding: .3em 0 .3em 1.1em;
  border-left: 3px solid var(--accent); color: var(--text-dim);
}
.prose hr { border: none; border-top: 1px solid var(--border); margin: 1.8em 0; }
.prose a { text-decoration: underline; text-underline-offset: 2px; }
.prose table { font-size: .87rem; }

pre.code-block {
  margin: 0 0 1em;
  padding: .85rem 1rem;
  background: var(--bg-inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  overflow-x: auto;
  font-size: .85rem; line-height: 1.6;
  position: relative;
}
pre.code-block code { background: none; border: none; padding: 0; font-size: inherit; }
pre.code-block[data-lang]::before {
  content: attr(data-lang);
  position: absolute; top: 0; right: 0;
  font-size: .62rem; letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-faint); background: var(--bg-elev);
  padding: .16em .55em;
  border-left: 1px solid var(--border); border-bottom: 1px solid var(--border);
  border-bottom-left-radius: var(--radius-s);
}

.hl-keyword { color: var(--hl-keyword); }
.hl-type    { color: var(--hl-type); }
.hl-string  { color: var(--hl-string); }
.hl-comment { color: var(--hl-comment); font-style: italic; }
.hl-num     { color: var(--hl-num); }
.hl-fn      { color: var(--hl-fn); }
.hl-literal { color: var(--hl-literal); }
.hl-preproc { color: var(--hl-preproc); }

/* ---------------- dashboard ---------------- */

.hero {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s5); align-items: center; margin-bottom: var(--s5);
}
.hero h1 { font-size: 1.85rem; margin-bottom: .3rem; letter-spacing: -.02em; }
.hero p { margin: 0; color: var(--text-dim); max-width: 54ch; }

.rank-card { min-width: 17rem; }
.rank-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); margin-bottom: var(--s2); }
.rank-name { font-weight: 680; font-size: 1.05rem; color: var(--accent); letter-spacing: -.01em; }
.rank-next { font-size: .72rem; color: var(--text-faint); font-family: var(--mono); }

.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(8.5rem, 100%), 1fr)); gap: var(--s3); margin-bottom: var(--s5); }
.stat {
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--s3) var(--s4);
}
.stat .n { font-family: var(--mono); font-size: 1.5rem; font-weight: 650; line-height: 1.15; letter-spacing: -.02em; }
.stat .k { font-size: .72rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: .05em; margin-top: .15rem; }

/* the dashboard's two main actions: carry on, or do this week's.

   Equal columns rather than content-sized, so the two cards are the same
   width whatever is in them, and each one puts its button on the right at a
   fixed position. The eye should find "Continue" in the same place every
   visit. */
.actions {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
  gap: var(--s3); margin-bottom: var(--s5);
}
.action {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s4); align-items: start;
  padding: var(--s4) var(--s5);
  border: 1px solid var(--border); border-radius: var(--radius-l);
  background: var(--bg-elev);
}
/* The one the page wants you to press gets the accent wash. */
.action-primary {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
  background:
    radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, transparent 60%),
    var(--bg-elev);
}
.action-body { min-width: 0; }
.action .eyebrow {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--accent); font-weight: 700; margin-bottom: .35rem;
}
.action h2 { margin: 0 0 .35rem; font-size: 1.22rem; letter-spacing: -.015em; overflow-wrap: anywhere; }
.action h2 a { color: inherit; }
.action p { margin: 0 0 var(--s3); color: var(--text-dim); font-size: .92rem; }
.action .meta { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }
/* A link that should not shout — a track name inside a sentence. */
.action p a.quiet { color: var(--text); text-decoration: underline; text-decoration-color: var(--border-strong); text-underline-offset: 2px; }
.action p a.quiet:hover { text-decoration-color: var(--accent); }
.action-side {
  display: flex; flex-direction: column; align-items: stretch;
  gap: var(--s2); text-align: center; white-space: nowrap;
}
.action-side .btn { justify-content: center; }

/* topic cards */

/* tracks — ordered sets of problems

   Every card is a three-row grid: head, blurb, foot. The middle row takes the
   slack and the blurb is clamped to two lines, so the progress bar and the
   counts land on the same baseline across a row of cards however long each
   description is. Aligning those is the whole reason a row of cards is easier
   to read than a list. */
.tracks { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr)); gap: var(--s3); }
.track-card {
  display: grid; grid-template-rows: auto 1fr auto; gap: var(--s2);
  padding: var(--s4);
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius); color: inherit; text-decoration: none;
  transition: border-color .14s, transform .1s, box-shadow .14s;
}
.track-card:hover {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  text-decoration: none; transform: translateY(-1px); box-shadow: var(--shadow-2);
}
.track-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2); }
.track-card h3 { margin: 0; font-size: 1rem; letter-spacing: -.01em; }
.track-blurb {
  margin: 0; font-size: .84rem; color: var(--text-dim); line-height: 1.5;
  /* Two lines, with the rest still in the DOM for a screen reader. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.track-foot { display: grid; gap: var(--s1); }
.track-foot .spread { color: var(--text-dim); }
.track-card[data-state="completed"] .bar i { background: var(--ok); }

/* Not started / Attempted / Completed. The middle one has to be earned, so an
   untouched track is never labelled as under way. */
.state {
  font-size: .66rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: .14rem .45rem; border-radius: 999px; white-space: nowrap;
  border: 1px solid var(--border-strong); color: var(--text-faint); background: var(--bg-inset);
}
.state-attempted { color: var(--st-attempted); border-color: color-mix(in srgb, var(--st-attempted) 45%, var(--border)); }
.state-completed { color: var(--st-solved);    border-color: color-mix(in srgb, var(--st-solved) 45%, var(--border)); }

.track-summary { display: grid; gap: var(--s3); margin-bottom: var(--s4); }

/* the activity grid

   Compact, with one gap value doing all the spacing so the month labels line
   up with the columns instead of being evenly spaced and drifting apart from
   them. --heat-cell is read by the JS that lays the labels out. */
.heat-wrap {
  --heat-cell: 11px;
  --heat-gap: 3px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  gap: var(--s1) var(--s2);
  align-items: start;
}
.heat-months {
  grid-column: 2; grid-row: 1;
  display: grid; gap: var(--heat-gap);
  font-size: .66rem; color: var(--text-faint);
  min-width: 0; overflow: hidden;
}
.heat-months span { grid-row: 1; white-space: nowrap; }
.heat-days {
  grid-column: 1; grid-row: 2;
  display: grid; grid-template-rows: repeat(7, var(--heat-cell)); gap: var(--heat-gap);
  font-size: .62rem; color: var(--text-faint); text-align: right;
  line-height: var(--heat-cell);
}
/* Mon, Wed, Fri on rows 2, 4 and 6 of a Sunday-first week. */
.heat-days span:nth-child(1) { grid-row: 2; }
.heat-days span:nth-child(2) { grid-row: 4; }
.heat-days span:nth-child(3) { grid-row: 6; }
.heat {
  grid-column: 2; grid-row: 2;
  display: grid; grid-auto-flow: column;
  grid-template-rows: repeat(7, var(--heat-cell));
  grid-auto-columns: var(--heat-cell);
  gap: var(--heat-gap);
  overflow-x: auto; padding-bottom: .2rem;
}
.heat i {
  width: var(--heat-cell); height: var(--heat-cell);
  border-radius: 2px; background: var(--bg-inset); display: block;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 60%, transparent);
}
.heat i[data-n="1"] { background: color-mix(in srgb, var(--ok) 32%, var(--bg-inset)); }
.heat i[data-n="2"] { background: color-mix(in srgb, var(--ok) 58%, var(--bg-inset)); }
.heat i[data-n="3"] { background: color-mix(in srgb, var(--ok) 80%, var(--bg-inset)); }
.heat i[data-n="4"] { background: var(--ok); }
.heat i[data-future="true"] { opacity: .3; box-shadow: none; }

.heat-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap; margin-top: var(--s2);
}
.heat-foot p { margin: 0; }
.heat-key { display: flex; align-items: center; gap: 3px; }
.heat-key i {
  width: 11px; height: 11px; border-radius: 2px; background: var(--bg-inset); display: block;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 60%, transparent);
}
.heat-key i[data-n="1"] { background: color-mix(in srgb, var(--ok) 32%, var(--bg-inset)); }
.heat-key i[data-n="2"] { background: color-mix(in srgb, var(--ok) 58%, var(--bg-inset)); }
.heat-key i[data-n="3"] { background: color-mix(in srgb, var(--ok) 80%, var(--bg-inset)); }
.heat-key i[data-n="4"] { background: var(--ok); }
.heat-key span:first-child { margin-right: .15rem; }
.heat-key span:last-child  { margin-left: .15rem; }

/* ---------------- catalog ---------------- */

.filters {
  display: flex; gap: var(--s2); flex-wrap: wrap; align-items: flex-end;
  margin-bottom: var(--s4); padding: var(--s3) var(--s4);
  background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius);
}
.filters input[type="search"] { min-width: 13rem; flex: 1 1 13rem; }
.filters label {
  font-size: .7rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: .05em;
  display: flex; flex-direction: column; gap: .25rem;
}
.filters select { font-size: .86rem; }

.chips { display: flex; gap: var(--s1); flex-wrap: wrap; }
.chip {
  font: inherit; font-size: .8rem;
  padding: .3em .72em; border-radius: 999px; cursor: pointer;
  background: var(--bg-elev-2); border: 1px solid var(--border); color: var(--text-dim);
  transition: color .13s, border-color .13s, background .13s;
}
.chip:hover { color: var(--text); border-color: var(--border-strong); }
.chip[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; }

/* The problem list reads as a table: aligned columns scan far better than a
   stack of cards once there are more than a dozen rows. */
.plist {
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-elev); overflow: hidden;
}

/* One grid for the header and every row, so the columns actually line up.
   Status, difficulty, time and XP are fixed widths; only the title flexes. */
.plist-head,
.prow {
  display: grid;
  grid-template-columns:
    2.1rem                /* tick or index */
    minmax(0, 1fr)        /* title and meta */
    7.2rem                /* status */
    7.2rem                /* difficulty */
    4.6rem                /* estimate */
    4.4rem                /* XP */
    2rem;                 /* bookmark */
  align-items: center; gap: var(--s3);
  padding: .62rem var(--s4);
}
.plist-head {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint); font-weight: 620;
  background: var(--bg-inset); border-bottom: 1px solid var(--border);
}
.prow {
  position: relative;
  color: inherit; text-decoration: none;
  border-bottom: 1px solid var(--border);
  transition: background .12s;
}
.prow:last-child { border-bottom: 0; }
/* The language tag on a catalogue row. Same shape as the type tag it
   sits beside, in the accent, because it is the more specific of the
   two and the one somebody is scanning for. */
.prow .tag-lang {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
  color: var(--accent);
}
.prow:hover { background: var(--bg-elev-2); }
.prow:focus-within { background: var(--bg-elev-2); }
.prow .tick { font-size: .95rem; text-align: center; }
.prow-main { min-width: 0; }
.prow .title {
  font-weight: 540; font-size: .93rem; line-height: 1.35;
  display: flex; align-items: baseline; gap: .4rem; flex-wrap: wrap;
}

/* The link is stretched over the whole row so a mouse can click anywhere,
   while the bookmark button stays above it and keeps its own focus ring. A
   row cannot be an <a> any more: a button inside a link is invalid markup and
   unreachable by keyboard. */
.prow-link { color: inherit; text-decoration: none; }
.prow-link::after { content: ""; position: absolute; inset: 0; z-index: 0; }
.prow-link:hover { text-decoration: underline; text-underline-offset: 2px; }
.prow-link:focus-visible { outline: none; }
.prow:has(.prow-link:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }

.prow .meta { font-size: .74rem; color: var(--text-faint); display: flex; gap: .4rem; flex-wrap: wrap; margin-top: .18rem; align-items: center; }
.prow .status-word { font-size: .76rem; color: var(--text-dim); white-space: nowrap; }
.prow .status-word.status-unsolved { color: var(--text-faint); }
.prow .status-word.status-attempted { color: var(--st-attempted); }
.prow .status-word.status-solved { color: var(--st-solved); }
.prow .status-word.status-read { color: var(--st-read); }
.prow .est { font-family: var(--mono); font-size: .74rem; color: var(--text-faint); white-space: nowrap; }
.prow .worth { font-family: var(--mono); font-size: .78rem; color: var(--text-dim); text-align: right; white-space: nowrap; }
.prow[data-status="solved"] .title .prow-link { color: var(--text-dim); }

/* Weekly and Optional are not difficulties and must not read as one, so they
   are outlined chips next to the title rather than another coloured word in
   the difficulty column. */
.badge {
  font-size: .64rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  padding: .1rem .36rem; border-radius: 4px; white-space: nowrap;
  border: 1px solid var(--border-strong); color: var(--text-faint);
}
.badge-weekly   { color: var(--accent);        border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); background: var(--accent-soft); }
.badge-optional { color: var(--text-dim);      border-color: var(--border-strong); }
.badge-due      { color: var(--st-attempted);  border-color: color-mix(in srgb, var(--st-attempted) 45%, var(--border)); }
/* A starter that is already written and already wrong. In the warning
   colour rather than the accent: it is a fact about the exercise a
   reader wants before they open it, not something being promoted. */
.badge-debug    { color: var(--warn);          border-color: color-mix(in srgb, var(--warn) 45%, var(--border)); }
.tag-lane       { color: var(--text-dim); }

.bookmark {
  position: relative; z-index: 1;
  appearance: none; background: none; border: 0; cursor: pointer;
  font-size: 1rem; line-height: 1; padding: .25rem;
  color: var(--text-faint); border-radius: var(--radius-s);
}
.bookmark:hover { color: var(--accent); background: var(--accent-soft); }
.bookmark.on { color: var(--warn); }

/* Visible to a screen reader, not on screen. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}


/* ---------------- the reading map ----------------

   Navigation first: a progress card, then a topic strip, then the concepts.
   Prose is held to a comfortable measure rather than running the full width
   of a wide screen, which is the single biggest difference between a page you
   can read and one you skim. */

.prose { max-width: 68ch; }

.read-summary { margin-bottom: var(--s4); }
.read-figure { font-size: 1.05rem; }
.read-figure b { font-family: var(--mono); font-size: 1.45rem; font-weight: 650; letter-spacing: -.02em; }
.read-pct { font-size: 1.45rem; font-weight: 650; color: var(--accent); letter-spacing: -.02em; }

/* The topic strip. Scrolls sideways rather than wrapping into four rows on a
   narrow screen, so the concepts stay above the fold. */
.topic-nav {
  display: flex; gap: var(--s2); margin-bottom: var(--s4);
  overflow-x: auto; padding-bottom: var(--s1);
  scrollbar-width: thin;
}
.topic-chip {
  appearance: none; cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: baseline; gap: .4rem;
  padding: .32rem .7rem; border-radius: 999px;
  border: 1px solid var(--border); background: var(--bg-elev);
  color: var(--text-dim); font: inherit; font-size: .82rem;
  transition: border-color .14s, color .14s, background .14s;
}
.topic-chip:hover { border-color: var(--border-strong); color: var(--text); }
.topic-chip.on {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
  background: var(--accent-soft); color: var(--text);
}
.topic-chip-count { font-family: var(--mono); font-size: .72rem; color: var(--text-faint); }
.topic-chip.on .topic-chip-count { color: var(--accent); }

.read-group { margin-bottom: var(--s5); }

/* Unread / Partly read / Read, as a word. A number alone (3/5) does not say
   whether that is good. */
.read-state {
  font-size: .64rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  padding: .1rem .36rem; border-radius: 4px; white-space: nowrap;
  border: 1px solid var(--border-strong); color: var(--text-faint);
}
.read-partly { color: var(--st-attempted); border-color: color-mix(in srgb, var(--st-attempted) 45%, var(--border)); }
.read-read   { color: var(--st-solved);    border-color: color-mix(in srgb, var(--st-solved) 45%, var(--border)); }
.prereq[data-read="read"] h4 { color: var(--text-dim); }

/* What a chapter is for: the exercises it unblocks. Reading with no next step
   is the thing that makes a reading list feel like homework. */
.practised {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .5rem;
  margin-top: var(--s2); padding-top: var(--s2);
  border-top: 1px dashed var(--border);
  font-size: .78rem;
}
.practised-label { color: var(--text-faint); }
.practised-links { display: flex; flex-wrap: wrap; gap: .3rem .55rem; }
.practised-link { color: var(--text-dim); }
.practised-link:hover { color: var(--accent); }
.practised-link.status-solved { color: var(--st-solved); }
.practised-link.status-solved::before { content: "\2713 "; }
.practised-link.status-attempted { color: var(--st-attempted); }
.practised-more { color: var(--text-faint); font-style: italic; }

/* ---------------- a problem ---------------- */

.pwrap { display: grid; grid-template-columns: minmax(0, 1fr) 19.5rem; gap: var(--s5); align-items: start; }

.phead { margin-bottom: var(--s4); }
.phead .crumbs { display: flex; gap: .45rem; align-items: center; font-size: .76rem; color: var(--text-faint); margin-bottom: .5rem; }
.phead .crumbs a { color: var(--text-faint); }
.phead .crumbs a:hover { color: var(--accent); }
.phead h1 { font-size: 1.45rem; margin-bottom: .45rem; letter-spacing: -.02em; }
.phead .meta { display: flex; gap: var(--s2); flex-wrap: wrap; align-items: center; font-size: .78rem; color: var(--text-faint); }

.tabs { display: flex; gap: 1px; margin-bottom: var(--s4); border-bottom: 1px solid var(--border); overflow-x: auto; }
.tab {
  font: inherit; font-size: .88rem; font-weight: 520;
  padding: .55em .9em; cursor: pointer;
  background: none; border: none; border-bottom: 2px solid transparent;
  color: var(--text-dim); white-space: nowrap;
  transition: color .13s, border-color .13s;
}
.tab:hover { color: var(--text); }
.tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); font-weight: 620; }
.tab .badge {
  font-size: .66rem; font-family: var(--mono);
  margin-left: .38em; padding: .05em .4em; border-radius: 999px;
  background: var(--bg-elev-2); color: var(--text-faint);
}
.tab[data-locked="true"] { color: var(--text-faint); }

.panel { animation: fade .18s ease-out; }
@keyframes fade { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }

/* answer widgets */
.answer { margin-top: var(--s5); padding-top: var(--s4); border-top: 1px solid var(--border); }

.opts { display: flex; flex-direction: column; gap: .45rem; max-width: 68ch; }
.opt {
  display: flex; gap: .7rem; align-items: flex-start;
  padding: .7rem .9rem; cursor: pointer;
  background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-s);
  transition: border-color .13s, background .13s;
}
.opt:hover { border-color: var(--border-strong); background: var(--bg-elev-2); }
.opt input { margin: .3em 0 0; accent-color: var(--accent); flex: none; }
.opt .opt-body { flex: 1; min-width: 0; }
.opt .opt-body > :last-child { margin-bottom: 0; }
.opt[data-mark="right"]  { border-color: var(--ok); background: color-mix(in srgb, var(--ok) 8%, var(--bg-elev)); }
.opt[data-mark="wrong"]  { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 8%, var(--bg-elev)); }
.opt[data-mark="missed"] { border-color: var(--warn); border-style: dashed; }
.opt .why { font-size: .84rem; color: var(--text-dim); margin-top: .45rem; padding-top: .45rem; border-top: 1px dashed var(--border); }

.numeric-in { display: flex; align-items: baseline; gap: .5rem; }
.numeric-in input { font-family: var(--mono); font-size: 1.05rem; width: 12rem; }
.numeric-in .unit { color: var(--text-dim); font-family: var(--mono); font-size: .88rem; }

.short-in input { font-family: var(--mono); width: min(100%, 30rem); }

.order-list { display: flex; flex-direction: column; gap: .35rem; max-width: 62ch; }
.order-item {
  display: grid; grid-template-columns: 1.9rem minmax(0, 1fr) auto; gap: .6rem; align-items: center;
  padding: .6rem .75rem;
  background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-s);
  cursor: grab; transition: border-color .13s, background .13s;
}
.order-item:hover { background: var(--bg-elev-2); }
.order-item:active { cursor: grabbing; }
.order-item.dragging { opacity: .4; }
.order-item.over { border-color: var(--accent); }
.order-item .n { font-family: var(--mono); color: var(--text-faint); text-align: center; font-size: .82rem; }
.order-item .moves { display: flex; flex-direction: column; gap: 1px; }
.order-item .moves button {
  font: inherit; font-size: .58rem; line-height: 1; padding: .2em .38em;
  background: var(--bg-elev-2); border: 1px solid var(--border); color: var(--text-dim);
  cursor: pointer; border-radius: 3px;
}
.order-item .moves button:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
.order-item[data-mark="right"] { border-color: var(--ok); }
.order-item[data-mark="wrong"] { border-color: var(--danger); }

.match-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s4); }
.match-col { display: flex; flex-direction: column; gap: .35rem; }
.match-col h4 { font-size: .7rem; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); margin: 0 0 .2rem; }
.match-item {
  display: flex; align-items: flex-start; justify-content: space-between; gap: .5rem;
  padding: .55rem .75rem; cursor: pointer; font-size: .88rem; text-align: left;
  background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-s);
  color: var(--text); font-family: inherit; line-height: 1.45;
  transition: border-color .13s, background .13s;
}
.match-item:hover:not(:disabled) { border-color: var(--border-strong); background: var(--bg-elev-2); }
.match-item[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.match-item[data-paired] { border-style: dashed; }
.match-item .tagn { font-family: var(--mono); font-size: .72rem; color: var(--accent); flex: none; font-weight: 700; }
.match-item[data-mark="right"] { border-color: var(--ok); border-style: solid; }
.match-item[data-mark="wrong"] { border-color: var(--danger); border-style: solid; }
.match-item .why { display: block; font-size: .78rem; color: var(--text-dim); margin-top: .35rem; }

.locate-code {
  font-family: var(--mono); font-size: .85rem; line-height: 1.65;
  background: var(--bg-inset); border: 1px solid var(--border); border-radius: var(--radius-s);
  overflow: hidden;
}
.locate-line { display: grid; grid-template-columns: 3rem minmax(0, 1fr); cursor: pointer; border-left: 3px solid transparent; }
.locate-line:hover { background: var(--bg-elev-2); }
.locate-line .ln {
  color: var(--text-faint); text-align: right; padding: 0 .65rem 0 .4rem;
  user-select: none; border-right: 1px solid var(--border);
}
.locate-line .lc { padding: 0 .8rem; white-space: pre; overflow-x: auto; }
.locate-line[aria-pressed="true"] { background: var(--accent-soft); border-left-color: var(--accent); }
.locate-line[data-mark="right"] { background: color-mix(in srgb, var(--ok) 13%, transparent); border-left-color: var(--ok); }
.locate-line[data-mark="wrong"] { background: color-mix(in srgb, var(--danger) 13%, transparent); border-left-color: var(--danger); }

/* ---------------- verdict ---------------- */

.verdict {
  margin-top: var(--s4); padding: var(--s3) var(--s4);
  border: 1px solid var(--border); border-left-width: 3px; border-radius: var(--radius-s);
  background: var(--bg-elev); max-width: 68ch;
}
.verdict[data-kind="right"] { border-left-color: var(--ok); }
.verdict[data-kind="wrong"] { border-left-color: var(--danger); }
.verdict[data-kind="part"]  { border-left-color: var(--warn); }
/* An attempt that never ran. Not red: nothing was wrong with the answer. */
.verdict[data-kind="blocked"] { border-left-color: var(--text-faint); }
.verdict h4 { margin: 0 0 .3rem; font-size: 1rem; }
.verdict p { margin: 0 0 .4rem; }
.verdict p:last-child { margin-bottom: 0; }
.verdict .award { font-family: var(--mono); font-size: .86rem; color: var(--ok); font-weight: 600; }

/* ---------------- side rail ---------------- */

.rail { display: flex; flex-direction: column; gap: var(--s3); position: sticky; top: calc(var(--header-h) + var(--s4)); }
.rail .card h3 { font-size: .7rem; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); margin-bottom: var(--s2); }

.worth-big { font-family: var(--mono); font-size: 1.6rem; font-weight: 650; line-height: 1; letter-spacing: -.02em; }
.worth-note { font-size: .74rem; color: var(--text-faint); margin-top: .3rem; line-height: 1.5; }

.hint {
  padding: .75rem .9rem; margin-bottom: .5rem;
  background: var(--bg-elev-2); border: 1px solid var(--border);
  border-left: 3px solid var(--warn); border-radius: var(--radius-s);
  font-size: .9rem; max-width: 68ch;
}
.hint .hn { font-size: .66rem; text-transform: uppercase; letter-spacing: .06em; color: var(--warn); display: block; margin-bottom: .25rem; font-weight: 700; }
.hint > :last-child { margin-bottom: 0; }

/* prerequisites */
.prereq { padding: var(--s3) 0; border-bottom: 1px solid var(--border); max-width: 72ch; }
.prereq:last-child { border-bottom: none; }
.prereq h4 { margin: 0 0 .15rem; font-size: .95rem; }
.prereq .why { font-size: .86rem; color: var(--text-dim); margin: 0 0 .45rem; }
.prereq .one-line { font-size: .86rem; color: var(--text-faint); margin: 0 0 .5rem; line-height: 1.55; }
.prereq .chain { font-size: .72rem; color: var(--text-faint); font-family: var(--mono); margin-bottom: .4rem; }

.readings { display: flex; flex-direction: column; gap: 2px; }
.reading {
  display: flex; align-items: baseline; gap: .5rem;
  /* Wraps, because a reading is a checkbox, a kind, a title and a page
     reference, and on a phone those do not fit on one line. Without this
     the row pushed the whole page sideways. */
  flex-wrap: wrap;
  padding: .32rem .5rem; border-radius: var(--radius-s); font-size: .86rem;
  transition: background .12s;
}
.reading .title { min-width: 0; overflow-wrap: anywhere; }
.reading:hover { background: var(--bg-elev-2); }
.reading input { margin: 0; accent-color: var(--ok); flex: none; position: relative; top: 2px; }
.reading .kind {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint); border: 1px solid var(--border); border-radius: 3px;
  padding: 0 .3em; flex: none;
}
.reading .where { color: var(--text-dim); font-size: .8rem; }
.reading.done .title { color: var(--text-faint); text-decoration: line-through; }

/* ---------------- the language tabs ---------------- */

.editor-head { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-bottom: var(--s2); }
/* What the problem is compiled with, if it is anything unusual. It is a
   footnote to the editor rather than a heading over it, so it sits at the
   right-hand end of the row and leaves no gap when there is nothing to
   say. */
.ws-editor .editor-head { justify-content: flex-end; margin: .35rem 0 .15rem; }
/* ---- the language picker ----

   It sits in the workspace tab strip, next to Editor, and looks like one
   of the tabs with a caret: it is what the editor tab is showing. */
.lang-pick { position: relative; display: flex; align-items: center; }
.lang-pick-btn {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .45rem .7rem; border: 0; background: none;
  color: var(--text-dim); font: inherit; font-size: .8rem; font-weight: 550;
  cursor: pointer; white-space: nowrap;
  border-bottom: 2px solid transparent;
  transition: color .13s, background .13s;
}
.lang-pick-btn:hover:not(:disabled) { color: var(--text); background: var(--bg-elev-2); }
.lang-pick-btn:disabled { cursor: default; }
.lang-pick-btn[aria-expanded="true"] { color: var(--text); background: var(--bg-elev-2); }
.lang-pick-name { white-space: nowrap; }
.lang-caret { width: 10px; height: 6px; flex: none; transition: transform .15s; }
.lang-pick-btn[aria-expanded="true"] .lang-caret { transform: rotate(180deg); }

/* A coloured pip rather than four logos: a logo each would be four more
   files to vendor, and the pip only has to tell them apart. */
.lang-mark {
  width: 8px; height: 8px; border-radius: 2px; flex: none;
  background: var(--text-faint);
}
.lang-mark[data-lang="cpp"]    { background: #6c9fd8; }
.lang-mark[data-lang="rust"]   { background: #d08a5a; }
.lang-mark[data-lang="python"] { background: #58a861; }
.lang-mark[data-lang="js"]     { background: #d8bf5a; }

.lang-menu {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 30;
  min-width: 10rem; padding: 4px;
  display: flex; flex-direction: column; gap: 1px;
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-2);
}
.lang-menu[hidden] { display: none; }
.lang-opt {
  display: flex; align-items: flex-start; gap: .5rem;
  padding: .4rem .55rem; border: 0; border-radius: var(--radius-s);
  background: none; color: var(--text-dim);
  font: inherit; font-size: .82rem; text-align: left; cursor: pointer;
}
.lang-opt .lang-mark { margin-top: .32rem; }
.lang-opt:hover { background: var(--bg-elev-2); color: var(--text); }
.lang-opt[aria-selected="true"] { color: var(--text); font-weight: 600; }
.lang-opt-text { display: flex; flex-direction: column; gap: .05rem; min-width: 0; }
.lang-opt-name { white-space: nowrap; }
/* Where it would run, or why it would not. Under the name rather than in a
   tooltip, because a tooltip is invisible to a reader who has not hovered
   and absent altogether on a touchscreen — which is how "nothing can run
   this" used to be indistinguishable from "this is not offered". */
.lang-opt-note { white-space: nowrap; font-weight: 400; }
.lang-opt[data-ready="false"] .lang-opt-note { color: var(--warn); }

/* "C++ only" in words. The exercise accepts one language; that is a fact
   about the exercise and it is said rather than implied by a dead caret. */
.lang-pick-only {
  font-size: .68rem; font-weight: 500; letter-spacing: .02em;
  color: var(--text-faint); text-transform: lowercase;
}


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

   CodeMirror owns the inside of .ed: one scroll container with the gutter
   inside it, so the line numbers cannot drift out of alignment and the
   horizontal scrollbar is at the bottom of the editor by construction. Those
   were the three reported bugs, and they are now structural rather than
   maintained by hand.

   What is left here is the frame around it. Colours come from the page's
   tokens through the theme in js/editor.js, so a purchased theme repaints the
   code too and tests/contrast.test.mjs still governs whether a comment is
   readable in it. */

.ed {
  /* Fills the panel it is given rather than growing with the content. The old
     editor grew past the viewport, which is how the sticky action bar ended up
     over it, swallowing clicks halfway down. */
  min-height: 0;
  height: 100%;
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-s);
  overflow: hidden;
}
.ed:focus-within { border-color: var(--accent); }
.ed .cm-editor { height: 100%; }
.ed .cm-scroller { scrollbar-width: thin; }
/* CodeMirror draws its own focus treatment on the container. */
.ed .cm-editor.cm-focused { outline: none; }

/* The fallback, for a page where vendor/codemirror.js did not load. */
.ed-plain { display: flex; }
.ed-plain .ed-input {
  flex: 1 1 auto; min-width: 0;
  margin: 0; padding: .6rem .85rem; border: 0; resize: none;
  background: transparent; color: var(--text);
  font-family: var(--mono); font-size: .86rem; line-height: 1.6;
  tab-size: 2; white-space: pre; overflow: auto;
}
.ed-plain .ed-input:focus { outline: none; }

.ed-status {
  font-size: .76rem;
  padding: .35rem .7rem;
  background: var(--bg-elev);
  border: 1px solid var(--border-strong);
  border-top: 0;
  border-radius: 0 0 var(--radius-s) var(--radius-s);
  color: var(--text-faint);
  font-family: var(--mono);
}
.ed-status[data-state="clean"]   { color: var(--ok); }
.ed-status[data-state="warning"] { color: var(--warn); }
.ed-status[data-state="error"]   { color: var(--danger); }

.ed-problems { display: flex; flex-direction: column; gap: 2px; margin-top: var(--s2); }
.ed-problem {
  display: grid; grid-template-columns: 4.2rem minmax(0, 1fr) auto;
  gap: .6rem; align-items: baseline;
  font: inherit; font-size: .8rem; text-align: left;
  padding: .4rem .6rem; cursor: pointer;
  background: var(--bg-elev); border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong); border-radius: var(--radius-s);
  color: var(--text-dim);
}
.ed-problem:hover { background: var(--bg-elev-2); color: var(--text); }
.ed-problem[data-severity="error"]   { border-left-color: var(--danger); }
.ed-problem[data-severity="warning"] { border-left-color: var(--warn); }
.ed-problem-at  { font-family: var(--mono); color: var(--text-faint); }
.ed-problem-msg { font-family: var(--mono); }
.ed-problem-from {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint); border: 1px solid var(--border); border-radius: 3px; padding: 0 .3em;
}

.run-out { margin-top: var(--s3); display: flex; flex-direction: column; gap: var(--s2); }
/* There used to be one box that every failed run was poured into: a
   monospace blob with an uppercase label, used for a compiler error, a
   runner that was not running, and a browser toolchain that cannot
   build the exercise — three different events in one costume. They now
   have `.diags` and `.platform`, and the blob is gone.

   `.lbl` stays: it is the little uppercase label, and `.diags-head`
   still uses it. */
.lbl {
  display: block; font-size: .66rem; text-transform: uppercase;
  letter-spacing: .05em; color: var(--text-faint); margin-bottom: .3rem;
}

/* The summary above the cases: did it pass, how many, how long, and what
   went wrong first. The border carries the answer so it is readable before
   a word of it is. */
.rsum {
  border: 1px solid var(--border); border-left: 3px solid var(--text-faint);
  border-radius: var(--radius-s); background: var(--bg-elev);
  padding: .6rem .75rem; display: flex; flex-direction: column; gap: .55rem;
}
.rsum[data-kind="passed"] { border-left-color: var(--ok); }
.rsum[data-kind="failed"] { border-left-color: var(--danger); }
.rsum-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.rsum-head h4 { margin: 0; font-size: .95rem; }
.rsum-frac { font-family: var(--mono); color: var(--text-dim); font-weight: 600; }
.rsum-badge {
  font-size: .66rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  padding: .12rem .45rem; border-radius: 4px; border: 1px solid var(--border);
}
.rsum-badge[data-kind="passed"] {
  color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, var(--border));
  background: color-mix(in srgb, var(--ok) 12%, transparent);
}
.rsum-badge[data-kind="failed"] {
  color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--border));
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}
/* Pushed right, because it is the least important thing on the row. */
.rsum-when {
  margin-left: auto; font-family: var(--mono); font-size: .74rem; color: var(--text-faint);
}
.rsum-tiles {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .4rem;
}
.rtile {
  display: flex; flex-direction: column; gap: .1rem; min-width: 0;
  padding: .4rem .5rem; border-radius: var(--radius-s);
  background: var(--bg-inset); border: 1px solid var(--border);
}
.rtile-label {
  font-size: .6rem; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint);
}
.rtile-value { font-family: var(--mono); font-size: 1.05rem; font-weight: 600; }
.rtile[data-kind="passed"] .rtile-value { color: var(--ok); }
.rtile[data-kind="failed"] .rtile-value { color: var(--danger); }
/* Zero failures should not be shouted in red. */
.rsum[data-kind="passed"] .rtile[data-kind="failed"] .rtile-value { color: var(--text-faint); }
.rsum-why {
  margin: 0; font-size: .82rem; padding: .4rem .55rem; border-radius: var(--radius-s);
  border: 1px solid var(--border); background: var(--bg-inset);
}
.rsum-why[data-kind="failed"] {
  color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--border));
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}
.rsum-why[data-kind="warn"] {
  color: var(--warn); border-color: color-mix(in srgb, var(--warn) 35%, var(--border));
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}
.rsum-note { margin: 0; font-size: .72rem; color: var(--text-faint); }

.case { border: 1px solid var(--border); border-radius: var(--radius-s); background: var(--bg-elev); overflow: hidden; }
.case > .case-head {
  display: flex; align-items: center; gap: .6rem;
  padding: .45rem .7rem; font-size: .82rem; cursor: pointer;
  background: var(--bg-elev-2);
}
/* The results rows are buttons so they can be reached with the keyboard;
   the sample-case rows next door are plain headings. Both have to look the
   same, so the button is stripped back to the div it replaced. */
button.case-head {
  width: 100%; text-align: left; border: 0; font: inherit; color: inherit;
  font-size: .82rem;
}
button.case-head::before {
  content: "\203A"; display: inline-block; flex: none;
  color: var(--text-faint); transition: transform .15s;
}
button.case-head[aria-expanded="true"]::before { transform: rotate(90deg); }
.case-name { font-weight: 600; flex: none; }
.case-why { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.case-ms { margin-left: auto; flex: none; }
.case-status {
  flex: none; font-size: .64rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em;
}
.case-status[data-pass="true"]  { color: var(--ok); }
.case-status[data-pass="false"] { color: var(--danger); }
.case[data-pass="true"]  > .case-head { border-left: 3px solid var(--ok); }
.case[data-pass="false"] > .case-head { border-left: 3px solid var(--danger); }
.case .io { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(10.5rem, 100%), 1fr)); gap: .6rem; padding: .7rem; }
.case .io > div { min-width: 0; }
.case .io h5 { margin: 0 0 .2rem; font-size: .64rem; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); }
.case .io pre {
  margin: 0; font-family: var(--mono); font-size: .8rem; white-space: pre-wrap; word-break: break-word;
  background: var(--bg-inset); border: 1px solid var(--border); border-radius: 4px; padding: .4rem .5rem;
  max-height: 10rem; overflow: auto;
}
.case .io .diff-ok  { color: var(--ok); }
.case .io .diff-bad { color: var(--danger); }

/* The submissions list. One row per graded program, folded shut: the whole
   point is scanning the verdicts, and ten expanded programs would be a
   scrollbar rather than a list. */
.subs { list-style: none; margin: .5rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .4rem; }
.sub {
  border: 1px solid var(--border); border-radius: var(--radius-s);
  background: var(--bg-elev); overflow: hidden;
}
.sub > summary {
  display: flex; align-items: center; gap: .55rem; flex-wrap: wrap;
  padding: .45rem .7rem; font-size: .82rem; cursor: pointer;
  background: var(--bg-elev-2);
}
.sub > summary::-webkit-details-marker { display: none; }
.sub > summary::marker { content: ''; }
/* The default triangle goes, but something has to say the row opens - the
   same chevron the other foldouts use. */
.sub > summary::before {
  content: "\203A"; display: inline-block; flex: none;
  color: var(--text-faint); transition: transform .15s;
}
.sub[open] > summary::before { transform: rotate(90deg); }
.sub-verdict {
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  padding: .1rem .4rem; border-radius: 4px;
  border: 1px solid var(--border); color: var(--text-dim);
}
.sub-verdict[data-outcome="passed"] {
  color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, var(--border));
  background: color-mix(in srgb, var(--ok) 12%, transparent);
}
.sub-verdict[data-outcome="failed"] {
  color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--border));
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}
/* A build failure is not a wrong answer, so it is not coloured like one. */
.sub-verdict[data-outcome="build-failed"] {
  color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, var(--border));
  background: color-mix(in srgb, var(--warn) 12%, transparent);
}
.sub-cases { font-family: var(--mono); font-size: .78rem; color: var(--text-dim); }
.sub-lang  { color: var(--text-dim); }
.sub-when  { color: var(--text-faint); }
.sub-ms    { font-family: var(--mono); font-size: .76rem; }
/* Pushed to the end, so the numbers line up down the right of the list. */
.sub-n     { margin-left: auto; font-family: var(--mono); font-size: .76rem; }
.sub-body  { padding: .6rem .7rem; display: flex; flex-direction: column; gap: .5rem; }
.sub-src code { background: none; border: none; padding: 0; font-size: inherit; }
.sub-src {
  margin: 0; font-family: var(--mono); font-size: .78rem; line-height: 1.5;
  white-space: pre; overflow: auto; max-height: 18rem;
  background: var(--bg-inset); border: 1px solid var(--border);
  border-radius: 4px; padding: .5rem .6rem;
}

.judge-state {
  display: flex; align-items: center; gap: .5rem;
  font-size: .8rem; padding: .5rem .7rem; border-radius: var(--radius-s);
  background: var(--bg-elev-2); border: 1px solid var(--border);
}
.judge-state .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-faint); flex: none; }
.judge-state code { font-size: .76rem; }

/* Four states, four colours, and grey for "not checked" - because a red dot
   before anything has been tried reads as a failure that has not happened. */
.judge-state[data-state="unchecked"] .dot { background: var(--text-faint); }
.judge-state[data-state="checking"]  .dot { background: var(--accent); }
.judge-state[data-state="down"]      .dot { background: var(--danger); }
.judge-state[data-state="unauthed"]  .dot { background: var(--warn); }
.judge-state[data-state="ready"]     .dot { background: var(--ok); }
.judge-state[data-state="unchecked"] { color: var(--text-dim); }
.judge-state[data-state="down"]      { border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); }
.judge-state[data-state="unauthed"]  { border-color: color-mix(in srgb, var(--warn) 35%, var(--border)); }
.judge-state[data-state="ready"]     { border-color: color-mix(in srgb, var(--ok) 35%, var(--border)); }

.danger-text { color: var(--danger); }
.warn-text   { color: var(--warn); }
.ok-text     { color: var(--ok); }

/* Setup detail that only matters when something is wrong. Closed by default,
   so the common case does not read like documentation. */
.advanced { margin-top: var(--s4); border-top: 1px solid var(--border); padding-top: var(--s3); }
.advanced > summary {
  cursor: pointer; font-size: .84rem; font-weight: 620; color: var(--text-dim);
  list-style: none; display: flex; align-items: center; gap: .4rem;
}
.advanced > summary::-webkit-details-marker { display: none; }
.advanced > summary::before { content: "\203A"; display: inline-block; transition: transform .15s; }
.advanced[open] > summary::before { transform: rotate(90deg); }
.advanced > summary:hover { color: var(--text); }
.advanced > *:not(summary) { margin-top: var(--s3); }

/* ---------------- profile and shop ---------------- */

.profile-head { display: flex; align-items: center; gap: var(--s4); margin-bottom: var(--s5); }
.avatar {
  width: 4.2rem; height: 4.2rem; flex: none;
  display: grid; place-items: center;
  font-size: 2rem; color: var(--accent);
  background: var(--bg-elev); border: 2px solid var(--border-strong); border-radius: var(--radius);
}
[data-frame="frame-trace"]  .avatar { border-style: dashed; border-color: var(--accent); }
[data-frame="frame-etched"] .avatar { border-color: var(--accent); box-shadow: inset 0 0 0 3px var(--bg), inset 0 0 0 4px var(--accent); }
[data-frame="frame-glow"]   .avatar { border-color: var(--accent); box-shadow: 0 0 20px var(--accent-soft), 0 0 0 1px var(--accent); }

.badges { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); gap: var(--s2); }
.badge-card { padding: .7rem .85rem; border-radius: var(--radius-s); background: var(--bg-elev); border: 1px solid var(--border); }
.badge-card.locked { opacity: .48; }
.badge-card h4 { margin: 0 0 .1rem; font-size: .9rem; }
.badge-card p  { margin: 0; font-size: .77rem; color: var(--text-faint); line-height: 1.45; }

.shop-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr)); gap: var(--s3); }
.shop-item {
  padding: var(--s3) var(--s4); border-radius: var(--radius);
  background: var(--bg-elev); border: 1px solid var(--border);
  display: flex; flex-direction: column; gap: var(--s2);
}
.shop-item.owned { border-color: color-mix(in srgb, var(--ok) 42%, var(--border)); }
.shop-item.worn  { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.shop-item h4 { margin: 0; font-size: .94rem; }
.shop-item .price { font-family: var(--mono); font-size: .84rem; color: var(--warn); }
.shop-item .note { font-size: .77rem; color: var(--text-faint); flex: 1; line-height: 1.45; }

/* ---------------- toasts ---------------- */

.toasts {
  position: fixed; bottom: var(--s4); right: var(--s4); z-index: 60;
  display: flex; flex-direction: column; gap: var(--s2); align-items: flex-end;
  pointer-events: none;
}
.toast {
  padding: .65rem .95rem; border-radius: var(--radius-s);
  background: var(--bg-elev-2); border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-3); font-size: .88rem; max-width: 23rem;
  animation: toast-in .22s cubic-bezier(.2, .9, .3, 1);
}
.toast[data-kind="good"] { border-left: 3px solid var(--ok); }
.toast[data-kind="bad"]  { border-left: 3px solid var(--danger); }
.toast[data-kind="info"] { border-left: 3px solid var(--accent); }
.toast b { color: var(--accent); }
@keyframes toast-in { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }



/* ---------------- the solving workspace ----------------

   A code problem fills the window: a compact header, then two panes side by
   side, the right one split again between the editor and the results. Every
   pane scrolls inside its own box and nothing else scrolls at all, so there
   is no page-level scrollbar and no accidental nested one.

   The old page let the editor grow with its content. That pushed it past the
   bottom of the window, which is how the sticky action bar ended up over it
   swallowing clicks, and it is why this is a fixed-height grid rather than a
   document. */

/* The window minus the top bar. One place that number is written down. */
body[data-workspace="true"] .main {
  max-width: none;
  padding: 0;
  height: calc(100vh - var(--header-h));
  overflow: hidden;
}
body[data-workspace="true"] #view-problem,
body[data-workspace="true"] #problemHost { height: 100%; min-height: 0; }

.ws {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  height: 100%;
  min-height: 0;
}

/* ---- the header ---- */

.ws-head {
  display: flex; align-items: center; gap: var(--s3);
  padding: .4rem var(--s4);
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
  min-height: 0;
}
.ws-head-left { display: flex; align-items: center; gap: .15rem; flex: none; }
.ws-head-mid {
  display: flex; align-items: baseline; gap: var(--s2);
  min-width: 0; flex: 1 1 auto;
}
.ws-head-right { display: flex; align-items: center; gap: .3rem; flex: none; }

.ws-title {
  font-size: 1rem; font-weight: 640; letter-spacing: -.01em; margin: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ws-back, .ws-nav {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.7rem; height: 1.7rem; border-radius: var(--radius-s);
  color: var(--text-dim); text-decoration: none; font-size: 1rem;
}
.ws-back:hover, .ws-nav:hover { background: var(--bg-elev-2); color: var(--text); text-decoration: none; }
.ws-nav[aria-disabled="true"] { opacity: .3; pointer-events: none; }
.ws-count { margin: 0 .1rem; }
.ws-font { display: inline-flex; align-items: center; gap: .15rem; }

/* Scoring, reveal and revisit: real, but not what the reader is doing now. */
.ws-menu { position: relative; }
.ws-menu > summary { list-style: none; cursor: pointer; }
.ws-menu > summary::-webkit-details-marker { display: none; }
.ws-menu-body {
  position: absolute; right: 0; top: calc(100% + .35rem); z-index: 40;
  width: 15rem; display: grid; gap: var(--s2);
  padding: var(--s3); border-radius: var(--radius);
  background: var(--bg-elev-2); border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-2);
}
.ws-menu-rule { border: 0; border-top: 1px solid var(--border); margin: .2rem 0; }

/* ---- the two columns ---- */

.ws-body {
  --split: .42;
  display: grid;
  grid-template-columns: minmax(0, calc(var(--split) * 100%)) auto minmax(0, 1fr);
  min-height: 0;
}
.ws-left, .ws-right { display: grid; min-width: 0; min-height: 0; }
.ws-left { grid-template-rows: auto minmax(0, 1fr); }

/* ---- the statement, folded away ----

   The freed space goes to the editor and nowhere else: the right
   column is already the `1fr`, so taking the other two tracks to zero
   is the whole of it. Both the panel and the divider go — a divider
   with one panel either side of it is a handle for resizing nothing.

   The way back is a button that lives in the work strip permanently
   and is shown only here, so there is always something on screen
   saying where the statement went. */
.ws-body[data-desc="false"] { grid-template-columns: 0 0 minmax(0, 1fr); }
.ws-body[data-desc="false"] > .ws-left,
.ws-body[data-desc="false"] > .ws-gutter { display: none; }
/* Placed explicitly, because `display: none` takes an item OUT of the
   grid rather than leaving an empty track behind it. Auto-placement
   then moved the editor into column 1 — which is the zero-width one —
   and the panel folded away into a sliver instead of into nothing. */
.ws-body > .ws-left   { grid-column: 1; }
.ws-body > .ws-gutter { grid-column: 2; }
.ws-body > .ws-right  { grid-column: 3; }
.ws-show-desc { display: none; }
.ws-body[data-desc="false"] .ws-show-desc {
  display: inline-flex; align-items: center;
  appearance: none; border: 0; cursor: pointer;
  background: var(--accent-soft); color: var(--text);
  font: inherit; font-size: .78rem; font-weight: 600;
  padding: .45rem .7rem; white-space: nowrap;
}
.ws-show-desc:hover { background: var(--accent); color: var(--accent-text); }

/* The fold control, at the end of the statement's own tab strip. */
.ws-collapse {
  appearance: none; border: 0; background: none; cursor: pointer;
  color: var(--text-faint); font-size: .85rem; padding: .45rem .6rem;
  margin-left: auto; flex: 0 0 auto;
}
.ws-collapse:hover { color: var(--text); background: var(--bg-elev-2); }
.ws-collapse:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The strip, whichever of the two panes is showing, and the bar. The
   editor and the result pane are both always in the DOM — CodeMirror
   keeps the undo history, the cursor and the scroll position, and
   rebuilding it to glance at the test cases would throw all three away —
   so they share one grid row and only one of them is displayed. */
.ws-right { grid-template-rows: auto minmax(0, 1fr) auto; }
.ws-right > .ws-editor, .ws-right > .ws-pane { grid-row: 2; grid-column: 1; }
.ws-right[data-pane="editor"] > .ws-pane  { display: none; }
.ws-right[data-pane="result"] > .ws-editor { display: none; }

/* The one scrolling element per pane. */
.ws-pane {
  overflow: auto; min-height: 0; padding: var(--s4);
  scrollbar-width: thin;
}
/* The reported bug, and the columns were never the problem.

   Measured at 1600px, dragging the divider right: the left column went
   672 -> 941 -> 1029px and `left + divider + right` stayed exactly
   1600px every time, so the grid was doing its job. The statement
   inside it stayed 566px wide the whole way, because the cap below was
   `max-width: 70ch` on the whole block — which computes to 566px. At
   the DEFAULT split that is already 106px of dead space, and at the far
   end it is 463px. Dragging to give the statement more room gave it
   more room and it did not use any of it.

   The cap itself is not wrong: a paragraph 185 characters wide is
   unreadable, and that is what an uncapped statement becomes on a wide
   screen at a wide split. What was wrong is that it applied to
   EVERYTHING — including the code blocks and tables that are the usual
   reason for dragging in the first place. The screenshot that reported
   this has a horizontal scrollbar under a code block in a panel with
   375px of unused space beside it.

   So: the measure belongs to the prose, and the things that want width
   take the panel. */
.ws-pane > .prose { max-width: none; }
.ws-pane > .prose > p,
.ws-pane > .prose > ul,
.ws-pane > .prose > ol,
.ws-pane > .prose > blockquote,
.ws-pane > .prose > h1,
.ws-pane > .prose > h2,
.ws-pane > .prose > h3,
.ws-pane > .prose > h4 {
  /* 86ch rather than 70: wide enough that dragging moves it over the
     range anybody actually drags, narrow enough to stay readable. */
  max-width: 86ch;
}
/* Code, tables and figures take the width they are given. These are
   what the reader widened the panel for. */
.ws-pane > .prose > pre,
.ws-pane > .prose > .code-block,
.ws-pane > .prose > table,
.ws-pane > .prose > .table-wrap,
.ws-pane > .prose > figure,
.ws-pane > .prose > svg { max-width: none; width: 100%; }

.ws-tabs {
  display: flex; gap: 1px; overflow-x: auto; flex: none;
  border-bottom: 1px solid var(--border); background: var(--bg-elev);
  scrollbar-width: none;
}
.ws-tabs::-webkit-scrollbar { display: none; }
/* The work strip: Editor, the language picker, and the panes. It is the
   one row of chrome above the editor, so the tabs are a touch bigger
   than the statement's and the picker sits flush among them.

   It wraps rather than scrolling, which the other strip does. The
   language menu hangs out of the bottom of this one, and a scroll
   container clips its children on BOTH axes — CSS computes overflow-y to
   `auto` the moment overflow-x is not `visible`, so there is no way to
   scroll sideways and let a dropdown out. The menu was being cut off
   mid-list, which looked like a short menu rather than a clipped one.
   Wrapping costs a second row of tabs on a phone and clips nothing. */
.ws-tabs-work { align-items: stretch; overflow: visible; flex-wrap: wrap; }
.ws-lang-slot { display: flex; align-items: stretch; flex: none; }
.ws-tabs-work .tab { font-size: .8rem; padding: .45rem .8rem; }

/* ---- the editor column ---- */

/* The bar across the bottom: Reset and what the draft is doing on the
   left, what can run and the two buttons that do on the right. It is
   outside the scrolling pane, so it does not move. */
.ws-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); flex: none; flex-wrap: wrap;
  padding: .4rem var(--s3);
  border-top: 1px solid var(--border); background: var(--bg-elev);
}
.ws-foot-left, .ws-foot-right { display: flex; align-items: center; gap: var(--s2); }

/* Run reads as the safe one of the pair and Submit as the one that counts,
   so Run is outlined in the pass colour rather than filled like Submit. */
.btn-run {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 45%, var(--border));
  font-weight: 600;
}
.btn-run:hover:not(:disabled) {
  color: var(--ok); border-color: var(--ok);
  background: color-mix(in srgb, var(--ok) 12%, transparent);
}

/* An editor living inside a result pane rather than in the workspace
   column: it has no column height to fill, so it gets a sensible one of
   its own and scrolls inside it. */
.ed-host-sm {
  /* Enough of the pane to write in without pushing the buttons off the
     bottom of a short window, and draggable from the corner for anyone
     who wants more. */
  height: clamp(15rem, 48vh, 34rem);
  min-height: 8rem;
  resize: vertical;
  overflow: hidden;
}
.ed-host-sm .cm-editor { height: 100%; }

/* Execution status, as a word next to the buttons that cause it. */
.ws-exec { font-size: .76rem; color: var(--text-faint); white-space: nowrap; }
.ws-exec[data-state="running"] { color: var(--accent); }
.ws-exec[data-state="ok"]      { color: var(--ok); }
.ws-exec[data-state="bad"]     { color: var(--danger); }
.ws-exec[data-state="blocked"] { color: var(--warn); }
/* Rust on a problem whose codegen the browser backend cannot do: the
   front end is running on every keystroke, so this is not the same
   state as having no runner at all and does not borrow its colour. */
.ws-exec[data-state="check"]   { color: var(--text-dim); }
/* A run of the reader's own testbench. It ran; whether what it printed
   is what they hoped for is not something the site knows, so this takes
   neither the green of a pass nor the red of a failure. */
.ws-exec[data-state="ran"]     { color: var(--text-dim); }

/* Grid rows all the way down, not percentage heights through a flex chain.
   A flex item with height:100% and content taller than its container does not
   reliably shrink - measured at 1840px inside a 492px box - while a
   minmax(0, 1fr) grid row bounds its child whatever is in it. */
.ws-editor {
  min-height: 0; overflow: hidden; padding: 0 var(--s3) var(--s2);
  display: grid; grid-template-rows: minmax(0, 1fr);
}
.ws-editor .widget {
  min-height: 0; display: grid;
  /* the (now empty) head, the editor, the "what can run" line, and
     whatever is left. Only the editor gets the leftover height. */
  grid-template-rows: auto minmax(0, 1fr) auto auto;
}
/* The language picker moved into the tab strip, so the head above the
   editor holds only the two pills a problem may carry; with neither it
   takes no room at all. */
.ws-editor .editor-head:empty { display: none; }
.ws-editor .ed-host {
  min-height: 0; display: grid;
  /* the editor, its status line, its problem list */
  grid-template-rows: minmax(0, 1fr) auto auto;
}
.ws-editor .ed { min-height: 0; height: auto; }
.ws-editor .ed-status { padding-top: .3rem; }
/* Only shown when something is wrong: see mountAnswer in js/problem.js. */
.ws-editor .judge-state { margin: .4rem 0 0; font-size: .78rem; }
.ws-editor .judge-state[data-up="true"] { display: none; }
.ws-editor .ed-problems { max-height: 7rem; overflow: auto; }


/* ---- the dividers ---- */

/* It used to be a 10px box containing a 1px hairline in --border, which is
   to say: invisible. "Dragging the divider does nothing" was mostly people
   not finding it and scrolling the pane behind it instead.

   Now the element itself is 18px — that is the hit area, and it is a real
   grid column so nothing can paint over it or clip it — and the bar you can
   see is drawn 10px wide inside it by ::before. A pseudo-element overflowing
   into the neighbouring panels was tried first and gave only 3px of overhang
   on the right, because the panel after it in the DOM won. */
.ws-gutter {
  position: relative; border: 0; padding: 0; background: none;
  touch-action: none;
  width: 18px; cursor: col-resize;
}

/* The bar. */
.ws-gutter::before {
  content: ""; position: absolute;
  background: var(--bg-elev);
  transition: background .14s;
  top: 0; bottom: 0; left: 4px; right: 4px;
  border-left: 1px solid var(--border); border-right: 1px solid var(--border);
}

/* The grip: three dots, so it reads as something to take hold of. */
.ws-gutter::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  background: var(--text-faint);
  border-radius: 2px;
  transition: background .14s, box-shadow .14s;
  width: 2px; height: 26px;
  box-shadow: 0 -10px 0 var(--text-faint), 0 10px 0 var(--text-faint);
}

.ws-gutter:hover::before,
[data-dragging] > .ws-gutter::before { background: var(--accent-soft); }

.ws-gutter:hover::after, [data-dragging] > .ws-gutter::after {
  background: var(--accent);
  box-shadow: 0 -10px 0 var(--accent), 0 10px 0 var(--accent);
}

.ws-gutter:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 3px;
}

/* While a drag is in flight nothing else may select, and the resize cursor
   wins everywhere — otherwise it flickers as the pointer crosses the text. */
[data-dragging] { user-select: none; }
body[data-resizing] { user-select: none; cursor: col-resize; }
body[data-resizing] * { cursor: inherit !important; }

/* ---- focus mode ----

   Everything that is not the problem, gone. With the workspace already
   filling the window, this is purely the header strip and the chrome. */
html[data-focus] .topbar { display: none; }
html[data-focus] body[data-workspace="true"] .main { height: 100vh; }
html[data-focus] .main { padding-top: 0; max-width: none; }
html[data-focus] .ws-head-left,
html[data-focus] .rail { display: none; }

/* ---- the reading layout, for everything that is not code ---- */

.pwrap.reading { display: block; }
.pwrap.reading .pmain { max-width: 68rem; margin: 0 auto; padding: 0 var(--s4) var(--s7); }
.pwrap.reading .panel > .prose { max-width: 70ch; }
/* One measure for the whole column. The statement and the options were
   already held to a readable 68-70ch; the action bar and the verdict were
   not, so Submit sat at the left end of a bar stretching 500px past the end
   of the text it belonged to. */
.pwrap.reading .actions-bar,
.pwrap.reading .verdict,
.pwrap.reading .needs-answer { max-width: 70ch; }
.pwrap.reading .ws-head {
  margin: 0 calc(var(--s4) * -1) var(--s4);
  border-radius: 0;
}

/* ---- narrow screens ----

   Two columns stop being two columns. The statement, the editor and the
   results become one scrolling column in that order, each given a sensible
   share of the window rather than being squeezed side by side. */
@media (max-width: 900px) {
  body[data-workspace="true"] .main { height: auto; overflow: visible; }
  .ws { height: auto; }
  .ws-body { grid-template-columns: minmax(0, 1fr); }
  .ws-gutter { display: none; }
  .ws-left { grid-template-rows: auto auto; }
  .ws-right { grid-template-rows: auto auto auto; }
  .ws-pane { max-height: 60vh; }
  .ws-editor { height: 55vh; padding: 0 var(--s3) var(--s2); }
  .ws-right { border-top: 1px solid var(--border); }
  .ws-head { flex-wrap: wrap; row-gap: .3rem; }
  .ws-head-mid { order: 3; width: 100%; }
  .ws-title { white-space: normal; }
}

/* ---------------- the answer bar on a reading page ----------------

   Non-coding problems are one column, so the Submit button can sit at the
   bottom of the statement and follow it down. It is deliberately NOT sticky
   on the workspace: a sticky bar over an editor is what used to float across
   the code and swallow clicks halfway down the page. Here there is nothing
   underneath it to swallow. */
/* Not sticky, deliberately.

   It used to be `position: sticky; bottom: 0; z-index: 5`, to keep Submit
   on screen while you read a long statement. What that actually did was
   float the bar up over the answer field directly above it: on
   metric-three-metrics-compared at 1440x1000, elementFromPoint in the
   middle of the answer box returned DIV.actions-bar, so the field could
   not be clicked at all at that scroll position, and the gradient hid
   what you had typed.

   Submit sits immediately under the answer here, which is where you look
   after filling it in, so pinning it bought very little and cost the
   field. */
.actions-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap;
  margin-top: var(--s4); padding-top: var(--s3);
}
.actions-bar-main { display: flex; gap: var(--s2); flex: none; }
.actions-bar-note { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: center; color: var(--text-faint); }
.actions-bar-note #worthNow { color: var(--text-dim); }
#draftNote[data-state="saved"], #draftState[data-state="saved"] { color: var(--ok); }
#draftNote[data-state="saving"], #draftState[data-state="saving"] { color: var(--text-faint); }

/* ---------------- diagnostics ----------------

   A list of places with a severity and a message, each clickable, with the
   compiler's own words still available underneath — the caret diagrams are
   sometimes the whole point. */
/* One line in Results about how the build went, with the way to the
   pane that has the compiler's own words. Not a verdict: a build that
   failed has no score, and this must not look like one. */
.buildline {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--radius-s);
  background: var(--bg-elev);
}
.buildline[data-kind="error"] { border-left-color: var(--danger); }
.buildline[data-kind="warn"]  { border-left-color: var(--warn); }
.buildline[data-kind="ok"]    { border-left-color: var(--ok); }
.buildline h4 { margin: 0; font-size: .95rem; flex-basis: 100%; }
.buildline p  { margin: 0; flex-basis: 100%; }
.buildline .lbl { font-weight: 600; }
.buildline .btn { margin-left: auto; }

.diags {
  border: 1px solid var(--border); border-left-width: 3px;
  border-radius: var(--radius-s); background: var(--bg-inset);
  margin: var(--s3) 0; overflow: hidden;
}
.diags[data-kind="error"] { border-left-color: var(--danger); }
.diags[data-kind="warn"]  { border-left-color: var(--warn); }
.diags-head {
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  padding: var(--s2) var(--s3); border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
}
.diags-head .lbl { font-size: .8rem; font-weight: 650; }
.diags[data-kind="error"] .lbl { color: var(--danger); }
.diags[data-kind="warn"]  .lbl { color: var(--warn); }

.diags-list { list-style: none; margin: 0; padding: 0; }
.diag {
  display: grid; grid-template-columns: auto auto minmax(0, 1fr);
  gap: .5rem; align-items: baseline;
  padding: .4rem var(--s3); border-bottom: 1px solid var(--border);
  font-size: .82rem;
}
.diag:last-child { border-bottom: 0; }
.diag-where {
  appearance: none; border: 0; background: none; padding: 0; cursor: pointer;
  font-family: var(--mono); font-size: .78rem; color: var(--accent);
  text-decoration: underline; text-underline-offset: 2px;
}
.diag-where:hover { color: var(--text); }
.diag-sev {
  font-size: .66rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint);
}
.diag[data-severity="error"] .diag-sev { color: var(--danger); }
.diag[data-severity="warning"] .diag-sev { color: var(--warn); }
.diag-msg { color: var(--text); overflow-wrap: anywhere; }
.diag-code { color: var(--text-faint); font-family: var(--mono); font-size: .74rem; }
.diag-notes {
  grid-column: 3; margin: .25rem 0 0; padding-left: 1rem;
  color: var(--text-dim); font-size: .78rem;
}
.diags-full > summary {
  cursor: pointer; padding: var(--s2) var(--s3); font-size: .76rem;
  color: var(--text-faint); list-style: none;
}
.diags-full > summary::-webkit-details-marker { display: none; }
.diags-full > summary:hover { color: var(--text-dim); }
.diags-raw {
  margin: 0; padding: var(--s3); background: var(--bg-inset);
  font-family: var(--mono); font-size: .76rem; line-height: 1.55;
  white-space: pre-wrap; overflow-x: auto; color: var(--text-dim);
  border-top: 1px solid var(--border);
}

/* ---------------- theme miniatures ----------------

   A small, honest rendering of the real interface in a theme's own tokens.
   Everything here reads from var(--…), and the container carries
   data-theme-preview, which every palette block also matches — so there is one
   set of values and the miniature cannot drift from the page.

   Font sizes are tiny on purpose: this is a picture of a layout, not text to
   read, which is why it is aria-hidden and why Preview exists for judging
   readability properly. */

.tpreview {
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-s);
  overflow: hidden; margin-bottom: var(--s3);
  font-size: 7px; line-height: 1.5;
  user-select: none;
}
.tp-bar {
  display: flex; align-items: center; gap: 4px;
  padding: 4px 6px; background: var(--bg-elev);
  border-bottom: 1px solid var(--border);
}
.tp-mark { width: 6px; height: 6px; border-radius: 1px; background: var(--accent); flex: none; }
.tp-brand { font-weight: 700; letter-spacing: -.01em; }
.tp-xp { margin-left: auto; color: var(--text-faint); font-family: var(--mono); }
.tp-body { padding: 6px; display: grid; gap: 5px; }
.tp-card {
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: 3px; padding: 5px 6px; display: grid; gap: 4px;
}
.tp-title { font-weight: 600; color: var(--text); }
.tp-meta { display: flex; align-items: center; gap: 3px; }
.tp-chip {
  border: 1px solid var(--border-strong); border-radius: 2px;
  padding: 0 3px; color: var(--text-dim);
}
.tp-diff { color: var(--diff-intermediate); border-color: color-mix(in srgb, var(--diff-intermediate) 45%, var(--border)); }
.tp-faint { color: var(--text-faint); font-family: var(--mono); }
.tp-track { height: 3px; border-radius: 2px; background: var(--bg-inset); overflow: hidden; }
.tp-track i { display: block; height: 100%; background: var(--accent); }
.tp-code {
  margin: 0; padding: 5px 6px; border-radius: 3px;
  background: var(--bg-inset); border: 1px solid var(--border);
  font-family: var(--mono); font-size: 6.5px; line-height: 1.6;
  white-space: pre; overflow: hidden; color: var(--text);
}
.tp-btns { display: flex; gap: 4px; }
.tp-btn {
  border: 1px solid var(--border-strong); border-radius: 2px;
  padding: 1px 5px; color: var(--text-dim);
}
.tp-primary { background: var(--accent); color: var(--accent-text); border-color: var(--accent); font-weight: 650; }

/* The shop card, now that it holds a picture and two controls. */
.shop-grid-themes { grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr)); }
.shop-actions { display: flex; gap: var(--s2); align-items: center; margin-top: auto; flex-wrap: wrap; }
.shop-actions .btn { flex: 0 0 auto; }
.shop-item.previewing {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
/* Applied is a fact about the page, so it is not a button. */
.pill-applied {
  color: var(--st-solved);
  border-color: color-mix(in srgb, var(--st-solved) 45%, var(--border));
}
.btn-ghost.on { border-color: var(--accent); color: var(--accent); }

/* The preview bar. Fixed, because a preview changes the whole page and the way
   out has to be reachable without scrolling back to the card. */
.preview-bar {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: var(--s4); z-index: 60;
  display: flex; align-items: center; gap: var(--s4);
  max-width: min(92vw, 46rem);
  padding: var(--s3) var(--s4);
  background: var(--bg-elev-2); color: var(--text);
  border: 1px solid var(--accent); border-radius: var(--radius-l);
  box-shadow: var(--shadow-3);
  font-size: .86rem;
}
@media (max-width: 680px) {
  .preview-bar { flex-direction: column; align-items: stretch; gap: var(--s2); text-align: center; }
}

/* ---------------- responsive ---------------- */

/* The top bar is one row only for as long as one row fits. Between the
   tablet widths the brand, the subject switch, seven view links and the
   purse add up to more than the window, and the whole page scrolled
   sideways because of it. From here down the links take a row of their
   own - the same width at which the workspace stops being two columns. */
@media (max-width: 900px) {
  .topbar {
    flex-wrap: wrap; height: auto;
    padding: var(--s2) var(--s4); gap: var(--s2) var(--s3);
  }
  .topbar-nav { order: 3; width: 100%; margin-left: 0; }
  /* Columns side by side do not fit a phone, and a panel wider than the
     window is a sideways scrollbar on every page. It stacks and is pinned
     to both edges instead. */
  .navmenu { position: static; }
  .navmenu-panel {
    grid-auto-flow: row; grid-auto-columns: auto;
    grid-template-columns: minmax(0, 1fr);
    left: var(--s3); right: var(--s3);
    max-height: 70vh; overflow-y: auto;
  }
  .navmenu-col { padding: var(--s2) 0; }
  .navmenu-col + .navmenu-col { border-left: 0; border-top: 1px solid var(--border); }
  .purse { margin-left: auto; }
}

@media (max-width: 980px) {
  .pwrap { grid-template-columns: minmax(0, 1fr); }
  .rail { position: static; flex-direction: row; flex-wrap: wrap; }
  .rail > .card { flex: 1 1 14rem; }
  .hero { grid-template-columns: 1fr; }
  .action { grid-template-columns: 1fr; gap: var(--s3); }
  .action-side { align-items: stretch; }
  .match-wrap { grid-template-columns: 1fr; }
  .main { padding: var(--s4) var(--s4) var(--s7); }
}

@media (max-width: 680px) {
  /* Tighter still, on top of the 900px wrap above. */
  .topbar { padding: var(--s2) var(--s3); gap: var(--s2); }
  .plist-head { display: none; }
  /* Flex rather than grid below this width: the fixed columns stop fitting,
     and a wrapping flex row keeps every field on screen - including the XP,
     which the old layout dropped - without any column maths. */
  .prow {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: .25rem .55rem; padding: .6rem var(--s3);
  }
  .prow .tick        { order: 1; width: 1.1rem; flex: 0 0 auto; }
  .prow-main         { order: 2; flex: 1 1 62%; }
  .bookmark          { order: 3; flex: 0 0 auto; margin-left: auto; }
  .prow .status-word { order: 4; flex: 0 0 auto; margin-left: 1.35rem; }
  .prow .diff        { order: 5; flex: 0 0 auto; font-size: .74rem; }
  .prow .est         { order: 6; flex: 0 0 auto; }
  .prow .worth       { order: 7; flex: 0 0 auto; margin-left: auto; }
  .hero h1 { font-size: 1.5rem; }
  .filters { padding: var(--s2) var(--s3); }
  .filters label { flex: 1 1 8rem; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* A standfirst: the one paragraph under a page's title. */
.lede { font-size: 1.02rem; color: var(--text-dim); max-width: 68ch; }

/* ---------------- the execution setup guide ----------------

   A procedure, so it reads as one: numbered steps, a command you can copy,
   and what the command prints when it worked. The three mode cards above it
   are the only place on the site that says "here is what can and cannot run
   for you", and each of them is painted from a live probe rather than from
   what the configuration claims. */

.setup-modes {
  display: grid; gap: var(--s3); margin: var(--s4) 0;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}
.setup-mode {
  border: 1px solid var(--border); border-top-width: 3px;
  border-radius: var(--radius); padding: var(--s3);
  background: var(--bg-elev);
}
.setup-mode[data-state="ready"]   { border-top-color: var(--ok); }
.setup-mode[data-state="partial"] { border-top-color: var(--warn); }
.setup-mode[data-state="down"]    { border-top-color: var(--danger); }
.setup-mode[data-state="off"],
.setup-mode[data-state="unknown"] { border-top-color: var(--border-strong); }
.setup-mode-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s2); margin-bottom: var(--s2);
}
.setup-mode-head h3 { margin: 0; }
.setup-badge {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .05em;
  font-weight: 700; padding: .12em .5em; border-radius: 999px;
  border: 1px solid var(--border-strong); color: var(--text-dim);
  white-space: nowrap;
}
.setup-badge[data-state="ready"]   { color: var(--ok);     border-color: var(--ok); }
.setup-badge[data-state="partial"] { color: var(--warn);   border-color: var(--warn); }
.setup-badge[data-state="down"]    { color: var(--danger); border-color: var(--danger); }

/* ---- the steps ---- */

.setup-steps {
  list-style: none; margin: var(--s4) 0 0; padding: 0;
  display: grid; gap: var(--s5); min-width: 0;
}
.setup-step, .setup-cmds, .setup-verify, .setup-results { min-width: 0; }
.expect-out { max-width: 100%; overflow-x: auto; }
.setup-step h4 {
  display: flex; align-items: center; gap: var(--s2);
  margin: 0 0 var(--s2); font-size: 1rem;
}
.step-n {
  display: inline-grid; place-items: center; flex: none;
  width: 1.6rem; height: 1.6rem; border-radius: 50%;
  background: var(--accent-soft); color: var(--accent);
  font-family: var(--mono); font-size: .8rem; font-weight: 700;
}
.setup-step > * + * { margin-top: var(--s2); }

/* ---- a command, with the button that copies it ---- */

/* min-width:0 on both, or a long command line makes the grid track as wide
   as the line and pushes the whole page sideways — measured at 383px of
   overflow on a 390px phone. The scrollbar belongs to the block. */
.cmd { position: relative; margin: var(--s2) 0; min-width: 0; }
.cmd .code-block {
  margin: 0; padding-right: 5.5rem;
  min-width: 0; max-width: 100%; overflow-x: auto;
}
.copy-btn {
  position: absolute; top: .4rem; right: .4rem;
  font-size: .72rem; padding: .2em .6em;
}
.copy-btn[data-state="done"]   { color: var(--ok); border-color: var(--ok); }
.copy-btn[data-state="manual"] { color: var(--warn); border-color: var(--warn); }
.cmd-note { margin: .3rem 0 0; }
.setup-cmds { display: grid; gap: var(--s2); margin-top: var(--s2); }

.expect { margin-top: .3rem; }
.expect > summary {
  cursor: pointer; font-size: .76rem; color: var(--text-faint);
  padding: .15rem 0;
}
.expect > summary:hover { color: var(--text-dim); }
.expect-out { margin: .3rem 0 0; color: var(--text-dim); font-size: .76rem; }

/* ---- the result of a real compile ---- */

.setup-verify:empty { display: none; }
.setup-verify { margin-top: var(--s3); }
.setup-results { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.setup-result {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s2);
  align-items: start; padding: var(--s2) var(--s3);
  border: 1px solid var(--border); border-left-width: 3px;
  border-radius: var(--radius-s); background: var(--bg-inset);
}
.setup-result[data-state="ok"]   { border-left-color: var(--ok); }
.setup-result[data-state="fail"] { border-left-color: var(--danger); }
.setup-result[data-state="none"] { border-left-color: var(--border-strong); }
.setup-result-mark { font-weight: 700; font-family: var(--mono); }
.setup-result[data-state="ok"]   .setup-result-mark { color: var(--ok); }
.setup-result[data-state="fail"] .setup-result-mark { color: var(--danger); }

/* ---- what is wrong, and what to do ---- */

.setup-fault { padding: var(--s3) 0; border-top: 1px solid var(--border); }
.setup-fault:first-of-type { border-top: 0; padding-top: 0; }
.setup-fault h3 { margin: 0 0 var(--s2); font-size: .95rem; }
.setup-remaining { margin: var(--s2) 0 0; padding-left: 1.3rem; display: grid; gap: var(--s2); }
.setup-remaining li { font-size: .86rem; line-height: 1.6; color: var(--text-dim); max-width: 70ch; }

/* The toolbar's way out, which only exists when nothing can run. */
.ws-setup { white-space: nowrap; }
.ws-setup[hidden] { display: none; }

/* ---------------- the verdict, and which attempt it belongs to ----------------

   A verdict that stays on screen while the reader changes their answer stops
   describing anything. Rather than wiping it — the feedback is the useful
   part and they may still be reading it — it is labelled and dimmed, and the
   marks on the controls are cleared so that a freshly ticked box never looks
   as though it has already been judged. */
.verdict-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3);
}
.verdict-head h4 { margin: 0; }
.verdict-when { white-space: nowrap; }
.verdict[data-stale="true"] { opacity: .62; border-left-style: dashed; }
.verdict[data-stale="true"] .verdict-when { color: var(--warn); }

.verdict-history { margin-top: var(--s2); }
.verdict-history > summary { cursor: pointer; padding: .15rem 0; }
.verdict-history > summary:hover { color: var(--text-dim); }
.verdict-history ol {
  margin: .35rem 0 0; padding-left: 1.2rem;
  display: grid; gap: .3rem; color: var(--text-dim);
}

/* Said next to the control, because a toast is easy to miss. */
.needs-answer {
  margin: var(--s2) 0 0; padding: .4rem .7rem;
  border: 1px solid var(--warn); border-left-width: 3px;
  border-radius: var(--radius-s);
  background: var(--bg-elev); color: var(--text);
}

/* ---------------- fetching the in-browser C++ compiler ----------------

   90 MB with no feedback is indistinguishable from a page that has hung, so
   this is a real progress bar with real byte counts, and the Stop button
   beside Run is live for the whole download. */
.cxx-dl { display: grid; gap: var(--s2); padding: var(--s3) 0; }
.cxx-dl .bar { height: 6px; background: var(--bg-inset); border-radius: 999px; overflow: hidden; }
.cxx-dl .bar i {
  display: block; height: 100%; background: var(--accent);
  transition: width .2s linear;
}
.ws-stop[hidden] { display: none; }
.ws-stop { color: var(--warn); }

/* ---------------- the subject switch ----------------

   Two pills in the top bar. It is a lens rather than an account: everything
   it hides is still solved, still bookmarked, and still found by search. */
/* ---------------- you, in the top right ---------------- */

.me { position: relative; margin-left: auto; flex: none; }

.me-btn {
  position: relative; display: grid; place-items: center;
  width: 38px; height: 38px; padding: 0;
  background: none; border: 0; cursor: pointer; border-radius: 50%;
}
.me-btn:hover .me-glyph { color: var(--text); }
.me-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The ring is progress to the next rank. pathLength="100" on the circle
   means the dash array is a percentage and nobody has to think about
   2*pi*r when the radius changes. */
.me-ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.me-ring circle { fill: none; stroke-width: 2.5; }
.me-ring-track { stroke: var(--border); }
.me-ring-fill {
  stroke: var(--accent); stroke-linecap: round;
  stroke-dasharray: 0 100;
  transition: stroke-dasharray .4s ease;
}
.me-glyph {
  font-size: 1rem; line-height: 1; color: var(--text-dim);
  transition: color .13s;
}

.me-menu {
  position: absolute; right: 0; top: calc(100% + .5rem);
  z-index: 60; min-width: 13rem;
  display: flex; flex-direction: column;
  padding: var(--s2);
  background: var(--bg-elev-2);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-3);
}
.me-head {
  display: flex; flex-direction: column; gap: .1rem;
  padding: .3rem .5rem .45rem;
}
.me-name { font-weight: 650; font-size: .9rem; }
.me-menu .purse {
  padding: .35rem .5rem .5rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: .3rem;
}
.me-menu a {
  padding: .42rem .5rem; border-radius: var(--radius-s);
  color: var(--text-dim); font-size: .88rem; text-decoration: none;
}
.me-menu a:hover, .me-menu a:focus-visible {
  background: var(--bg-elev); color: var(--text); text-decoration: none;
}

/* ---------------- the front page ----------------

   Wider than the rest of the site and quieter: nothing on it is a
   control, so nothing on it competes with the one button that is. */

#view-home { padding-bottom: var(--s6); }

.hero { padding: clamp(2rem, 7vw, 5rem) 0 clamp(1.6rem, 4vw, 3rem); }
.hero-copy { max-width: 46rem; }
.hero-eyebrow {
  margin: 0 0 1rem; font-size: .7rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .18em; color: var(--text-faint);
}
.hero-title {
  margin: 0 0 1rem;
  font-size: clamp(2rem, 5.2vw, 3.6rem); line-height: 1.07;
  letter-spacing: -.03em; font-weight: 600;
}
.hero-title-dim { color: var(--text-faint); }
.hero-sub {
  margin: 0 0 1.6rem; max-width: 46ch;
  font-size: clamp(.95rem, 1.5vw, 1.1rem); line-height: 1.6; color: var(--text-dim);
}

/* The four strands, as a row of quiet labels rather than four more
   headings. They say what the subject matter is; they are not links,
   because every one of them is a filter the menu above already offers. */
.hero-strands {
  display: flex; flex-wrap: wrap; gap: .5rem 1.6rem;
  list-style: none; margin: 0 0 2rem; padding: 0;
}
.hero-strands li {
  display: inline-flex; align-items: center; gap: .45rem;
  font-size: .84rem; color: var(--text-dim);
}
.hero-strands svg {
  width: 16px; height: 16px; flex: none; color: var(--text-faint);
  fill: none; stroke: currentColor; stroke-width: 1.3;
  stroke-linecap: round; stroke-linejoin: round;
}

.hero-cta { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; }
.hero-cta-note { margin-left: .3rem; }
.btn-lg { font-size: .95rem; padding: .62em 1.3em; border-radius: 999px; }
.hero-note { margin: 1.1rem 0 0; max-width: 48ch; line-height: 1.5; }

/* ---------------- the four drawings ---------------- */

.hero-tiles {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: 0;
  border: 1px solid var(--border); border-radius: var(--radius-l);
  background: var(--bg-elev);
  overflow: hidden;
}
.htile {
  display: flex; flex-direction: column;
  padding: var(--s4); color: inherit; text-decoration: none;
  border-left: 1px solid var(--border);
  transition: background .14s;
}
.htile:first-child { border-left: 0; }
.htile:hover { background: var(--bg-elev-2); text-decoration: none; }
.htile:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.htile h3 { margin: 0 0 .3rem; font-size: .95rem; }
.htile p { margin: 0; font-size: .78rem; line-height: 1.5; color: var(--text-dim); }

.htile-art { margin-bottom: var(--s4); }
.htile-art svg { display: block; width: 100%; height: auto; }
/* One palette for all four, so the strip reads as one picture. They are
   illustrations of what is inside, not screenshots of it. */
.htile-axis  { fill: none; stroke: var(--border-strong); stroke-width: 1.2; stroke-linecap: round; }
.htile-line  { fill: none; stroke: var(--text-faint); stroke-width: 1.6; stroke-linecap: round; }
.htile-fill  { fill: color-mix(in srgb, var(--accent) 18%, transparent); stroke: none; }
.htile-pane  { fill: var(--bg-inset); stroke: var(--border-strong); stroke-width: 1.2; }
.htile-chip  { fill: none; stroke: var(--text-faint); stroke-width: 1.3; }
.htile-dot   { fill: var(--border-strong); }
.htile-bar   { fill: var(--border-strong); }
.htile-bar-on { fill: color-mix(in srgb, var(--accent) 55%, transparent); }
.htile-node  { fill: var(--bg-inset); stroke: var(--border-strong); stroke-width: 1.2; }
.htile-node-on {
  fill: color-mix(in srgb, var(--accent) 16%, transparent);
  stroke: color-mix(in srgb, var(--accent) 55%, var(--border-strong));
}
.htile-tick  { fill: var(--text-faint); font-family: var(--mono); font-size: 9px; }
.htile:hover .htile-line { stroke: var(--accent); }

/* ---------------- how it goes, and what you get ---------------- */

.home-head {
  margin: var(--s7) 0 var(--s4);
  font-size: 1.3rem; letter-spacing: -.02em;
}

.home-steps {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--s4); list-style: none; margin: 0; padding: 0;
  counter-reset: none;
}
.home-steps li { min-width: 0; }
.step-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.7rem; height: 1.7rem; margin-bottom: .6rem;
  border-radius: 999px; border: 1px solid var(--border-strong);
  font-family: var(--mono); font-size: .78rem; color: var(--text-dim);
}
.home-steps h3 { margin: 0 0 .3rem; font-size: 1rem; }
.home-steps p { margin: 0; font-size: .85rem; line-height: 1.6; color: var(--text-dim); }

.home-feats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--s3);
}
.feat {
  padding: var(--s4);
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius-l);
}
.feat h3 { margin: 0 0 .4rem; font-size: .95rem; }
.feat p { margin: 0; font-size: .82rem; line-height: 1.6; color: var(--text-dim); }

/* The numbers, counted rather than written. Deliberately not a stat row
   about you - this page is about the site. */
.home-facts {
  display: flex; flex-wrap: wrap; gap: var(--s4) var(--s6);
  margin: var(--s7) 0 0; padding: var(--s5) 0;
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.fact { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.fact-n {
  font-family: var(--mono); font-size: 1.5rem; font-weight: 600;
  letter-spacing: -.02em;
}
.fact-k { font-size: .76rem; color: var(--text-faint); }
.home-empty { margin: 0; font-size: .85rem; color: var(--warn); }

.home-foot { padding: var(--s7) 0 var(--s4); }
.home-foot h2 { margin: 0 0 .3rem; font-size: 1.6rem; letter-spacing: -.02em; }
.home-foot p { margin: 0 0 1.2rem; }

@media (max-width: 760px) {
  .htile { border-left: 0; border-top: 1px solid var(--border); }
  .htile:first-child { border-top: 0; }
}

/* ---------------- the catalogue and its rail ---------------- */

.plist-wrap {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 19rem);
  gap: var(--s4); align-items: start;
}
.plist-main { min-width: 0; }
.plist-rail { display: flex; flex-direction: column; gap: var(--s2); min-width: 0; }

/* renderHeat finds the caption by class rather than by the id of the one
   page the heatmap used to live on. */
.heat-caption { margin: 0; }

.rail-head {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--text-faint); margin: 0;
}
.rail-head + * { margin-top: 0; }
.rail-head:not(:first-child) { margin-top: var(--s3); }

/* The action card was drawn for a wide two-column grid and is the wrong
   shape in a 19rem rail: the title wraps four times to leave room for a
   button beside it. In here it stacks, and the button goes under. */
.plist-rail .action { grid-template-columns: minmax(0, 1fr); gap: var(--s3); padding: var(--s3); }
.plist-rail .action h2 { font-size: 1rem; }
.plist-rail .action-side {
  flex-direction: row; align-items: center; justify-content: space-between;
  gap: var(--s2);
}
/* The heading above it already says This week. */
.plist-rail .action .eyebrow { display: none; }

.rail-list {
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden;
}
.rail-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2);
  padding: .5rem .7rem; font-size: .84rem;
  color: var(--text-dim); text-decoration: none;
  background: var(--bg-elev);
}
.rail-row + .rail-row { border-top: 1px solid var(--border); }
.rail-row:hover { background: var(--bg-elev-2); color: var(--text); text-decoration: none; }
.rail-row-title { min-width: 0; overflow-wrap: anywhere; }

/* The rail goes under the list rather than beside it once there is no
   room for two columns. It is secondary, so it goes second. */
@media (max-width: 1100px) {
  .plist-wrap { grid-template-columns: minmax(0, 1fr); }
  .plist-rail { flex-direction: row; flex-wrap: wrap; }
  .plist-rail > * { flex: 1 1 18rem; }
  .rail-head { flex-basis: 100%; }
  .rail-head:not(:first-child) { margin-top: var(--s2); }
}

/* ---------------- a checkbox you can read ---------------- */

.check {
  display: flex; align-items: center; gap: .55rem;
  cursor: pointer; font-size: .9rem; color: var(--text);
}
.check input { margin: 0; flex: none; accent-color: var(--accent); }
.check input:disabled + span { color: var(--text-faint); }
.check:has(input:disabled) { cursor: default; }

/* The per-language set under a master switch. The fieldset is real, so a
   screen reader reads the legend as the group's name rather than leaving
   four unexplained checkboxes in a row. */
.lang-checks {
  display: flex; flex-wrap: wrap; gap: .3rem 1.1rem;
  border: 0; margin: 0; padding: 0;
}
.lang-checks legend {
  padding: 0; margin-bottom: .35rem;
  text-transform: uppercase; letter-spacing: .09em;
}
.lang-checks .check { font-size: .84rem; font-family: var(--mono); }

/* ---------------- learn ----------------

   The tree on the left, the selected node on the right. Below the
   workspace breakpoint they stack, tree first, because picking a node is
   what you came to do. */


.crumbs {
  display: flex; gap: .45rem; align-items: center;
  font-size: .76rem; color: var(--text-faint); margin-bottom: .5rem;
}
.crumbs a { color: var(--text-faint); }
.crumbs a:hover { color: var(--accent); }

.learn-group + .learn-group { margin-top: var(--s5); }
.learn-group-head {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--text-faint); margin: 0 0 var(--s3);
}


/* The nesting is the prerequisite order, so it is a real list and a
   screen reader gets it for free. The rail is the only decoration. */
/* Tight on purpose: the indent is six levels deep in places, and every
   millimetre of it is width the node's name and counts do not get. */


/* A dot for the node's own state, so the shape of what is done is
   readable without counting. Colour is never the only signal: the
   counts beside it say the same thing in words. */

.chip-link {
  padding: .1em .55em; border-radius: 999px;
  background: var(--bg-inset); border: 1px solid var(--border);
  color: var(--text-dim); text-decoration: none;
}
.chip-link:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }

/* Inside a node panel the rows are denser than the catalogue: there are
   rarely more than a handful and they sit under a heading already. */

/* The counters on a track card: what it is made of, each as a fraction
   so the card says how far in you are without being read twice. */
.track-counts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(5.5rem, 100%), 1fr));
  gap: .3rem; margin-top: .55rem;
}
.track-count {
  display: flex; flex-direction: column; gap: .1rem;
  padding: .35rem .45rem; border-radius: var(--radius-s);
  background: var(--bg-inset); border: 1px solid var(--border);
}
.track-count-label {
  font-size: .6rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-faint);
}
.track-count-value { font-family: var(--mono); font-size: .76rem; color: var(--text-dim); }
/* Only the required halves go green when the track is finished. Related
   practice and readings are not part of finishing it, and colouring them
   as though they were is the same claim the shared "Problems" label used
   to make. They are set back a step for the same reason. */
.track-card[data-state="completed"] .track-count[data-kind="questions"] .track-count-value,
.track-card[data-state="completed"] .track-count[data-kind="coding"] .track-count-value {
  color: var(--ok);
}
.track-count[data-kind="related"],
.track-count[data-kind="resources"] { background: none; border-style: dashed; }
.track-count[data-kind="related"] .track-count-value,
.track-count[data-kind="resources"] .track-count-value { color: var(--text-faint); }

/* ---------------- the Learn canvas ----------------

   The tree used to be a nested list. It is a canvas now: boxes placed by
   js/learn/layout.js, edges drawn in SVG behind them, the whole thing
   panned and zoomed by one transform so text never reflows at a new
   scale.

   There is no list view to fall back to, which is the reason the boxes
   are real buttons inside a role="tree" rather than divs: this picture
   has to be the accessible one. */

/* ---- the graph as a workspace ----

   The same bargain the code workspace makes, and the same mechanism:
   body[data-graph="true"] gives .main the window below the header, and
   the router takes it off on every route that is not an open track. The
   Learn index is NOT a graph — it is an ordinary card page at the
   ordinary width — so this is set by the renderer that draws a track and
   cleared by the one that draws the list, as well as by the router.

   A graph in a 1180px column with 3rem of padding either side was a
   picture of a hundred and fifteen boxes in a sixth of the screen. */
body[data-graph="true"] .main {
  max-width: none;
  padding: 0;
  height: calc(100vh - var(--header-h));
  overflow: hidden;
}
body[data-graph="true"] #view-learn { height: 100%; min-height: 0; }

/* The bar, the chapter rail, and the canvas. The rail is `hidden`
   unless the track follows a course, and a hidden element is not a grid
   item at all — so the rows are named rather than inferred, or
   auto-placement drops the canvas into the empty middle row and gives
   it no height. (The same trap as .ws-body and .mf.) */
.graph {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  height: 100%;
  min-height: 0;
}
.graph > .graph-bar       { grid-row: 1; }
.graph > .learn-chapters  { grid-row: 2; }
.graph > .learn-canvas    { grid-row: 3; }

.graph-bar {
  display: flex; align-items: center; gap: var(--s3);
  flex-wrap: wrap; flex: none;
  padding: .45rem var(--s4);
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
}
.graph-bar h1 {
  margin: 0; font-size: 1rem; font-weight: 600;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.graph-acts { display: flex; gap: .25rem; margin-left: auto; }
.graph-acts .btn[disabled] { opacity: .4; cursor: default; }

/* ---- find a concept ---- */

.graph-find { position: relative; flex: 0 1 15rem; min-width: 8rem; }
.graph-find input {
  width: 100%; padding: .3rem .6rem; font-size: .8rem;
  background: var(--bg-inset); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-s);
}
.graph-find-list {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 40;
  max-height: 16rem; overflow-y: auto; padding: 4px;
  display: flex; flex-direction: column; gap: 1px;
  background: var(--bg-elev); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-2);
}
.graph-find-list[hidden] { display: none; }
.graph-find-opt {
  display: flex; flex-direction: column; gap: .05rem; text-align: left;
  padding: .35rem .5rem; border: 0; border-radius: var(--radius-s);
  background: none; color: var(--text-dim); font: inherit; font-size: .82rem;
  cursor: pointer;
}
.graph-find-opt:hover,
.graph-find-opt[aria-selected="true"] { background: var(--bg-elev-2); color: var(--text); }
.graph-find-why { font-size: .68rem; color: var(--text-faint); }
.graph-find-none { padding: .4rem .5rem; font-size: .8rem; color: var(--text-faint); }

.learn-progress {
  font-family: var(--mono); font-size: .78rem; color: var(--text-dim);
  padding: .2em .7em; border-radius: 999px;
  background: var(--bg-inset); border: 1px solid var(--border);
  white-space: nowrap;
}


.learn-canvas {
  /* The details panel's width, written here as the default and
     overwritten on this element by the resize drag. One number, in one
     place, that both the panel and the code working out whether the
     selected box is hidden underneath it can read. */
  --lpanel-w: 26rem;
  position: relative; overflow: hidden;
  min-height: 0;
  background: var(--bg-inset);
  cursor: grab;
  /* A drag across the canvas is a pan, not a text selection. */
  user-select: none;
  touch-action: none;
}
.learn-canvas[data-dragging="true"] { cursor: grabbing; }

.learn-pan {
  position: absolute; top: 0; left: 0;
  transform-origin: 0 0;
  will-change: transform;
}
.learn-edges { position: absolute; top: 0; left: 0; overflow: visible; pointer-events: none; }
.learn-edges path {
  fill: none; stroke: var(--border-strong); stroke-width: 1.5;
}
.learn-edges path[data-extra="true"] {
  /* The prerequisites that make it a DAG rather than a tree, dashed so
     the difference is visible without being louder than the spine. */
  stroke-dasharray: 4 4; opacity: .75;
}
/* Lit: this edge touches the selected box. Path: this edge is somewhere
   on the way down to it from a root. Two weights, because "rests
   directly on" and "is somewhere beneath" are different claims and one
   colour for both would say they are the same. */
.learn-edges path[data-lit="true"] { stroke: var(--accent); stroke-width: 2.5; opacity: 1; }
.learn-edges path[data-path="true"] {
  stroke: var(--accent); stroke-width: 1.8; opacity: .55; stroke-dasharray: none;
}
.lnode[data-ancestor="true"] { border-color: var(--accent); color: var(--text); }

.learn-nodes { position: absolute; top: 0; left: 0; }

.lnode {
  position: absolute;
  display: flex; flex-direction: column; justify-content: center; gap: .3rem;
  padding: .45rem .6rem; text-align: left;
  background: var(--bg-elev); color: var(--text-dim);
  border: 1px solid var(--border); border-radius: var(--radius-s);
  font: inherit; font-size: .8rem; line-height: 1.25; cursor: pointer;
  transition: border-color .13s, background .13s, color .13s;
  overflow: hidden;
}
.lnode:hover { border-color: var(--border-strong); color: var(--text); }
.lnode:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.lnode[aria-selected="true"] {
  border-color: var(--accent); background: var(--accent-soft); color: var(--text);
  font-weight: 600;
}
.lnode-name {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
/* The thin bar under the name, as on the reference. Colour is never the
   only signal: the panel and the aria-label both say the numbers. */
.lnode .bar { height: 3px; }
.lnode[data-state="done"] .bar i { background: var(--ok); }
/* A concept with readings and no problems can be read and nothing more.
   It gets a quiet left edge rather than the green a solve earns, because
   reading a thing is not knowing it. */
.lnode[data-state="read"] { border-left: 2px solid var(--text-faint); }
.lnode[data-state="done"] { border-left: 2px solid var(--ok); }

/* ---- a concept that belongs to a course ----

   The chapter number sits in the corner of the box, and material the
   notes rule out of the course is dimmed and labelled. Neither is
   colour alone: the number is a numeral and the label is a word, and
   both are in the aria-label as well. */
.lnode-ch {
  position: absolute; top: .2rem; right: .35rem;
  font-size: .62rem; font-variant-numeric: tabular-nums;
  color: var(--text-faint); opacity: .85;
}
.lnode[data-optional="true"] { border-style: dashed; }
.lnode-opt {
  align-self: flex-start;
  font-size: .58rem; text-transform: uppercase; letter-spacing: .05em;
  padding: 0 .25rem; border-radius: var(--radius-s);
  border: 1px solid var(--border-strong); color: var(--text-faint);
}
/* A folded chapter is one box standing for several, so it is drawn
   heavier than the sections it replaced. */
.lnode[data-folded] {
  border-width: 2px; border-color: var(--border-strong);
  background: var(--bg-elev-2);
  font-weight: 600;
}
.lnode[data-folded] .lnode-ch { display: none; }

/* ---- the chapter rail ---- */

/* `display: grid` beats the `hidden` attribute's own `display: none`,
   so the attribute has to be honoured explicitly — otherwise every
   non-course track grows an empty bar. */
.learn-chapters[hidden] { display: none; }
.learn-chapters {
  display: grid; gap: .3rem;
  padding: .4rem var(--s4) .5rem;
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
}
.lchap-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.lchap-acts { display: flex; gap: .3rem; flex: none; }
.lchap-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .3rem;
}
.lchap {
  display: flex; align-items: stretch;
  border: 1px solid var(--border); border-radius: var(--radius-s);
  background: var(--bg);
  overflow: hidden;
}
.lchap-fold, .lchap-name {
  appearance: none; border: 0; background: none; cursor: pointer;
  color: var(--text-dim); font: inherit; font-size: .74rem;
  padding: .25rem .4rem;
  display: flex; align-items: center; gap: .35rem;
}
.lchap-fold { border-right: 1px solid var(--border); color: var(--text-faint); }
.lchap-fold:hover, .lchap-name:hover { background: var(--bg-elev-2); color: var(--text); }
.lchap-fold:focus-visible, .lchap-name:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
.lchap-fold[aria-expanded="false"] { background: var(--accent-soft); color: var(--text); }
.lchap-n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.1rem; height: 1.1rem; border-radius: var(--radius-s);
  background: var(--bg-elev-2); color: var(--text-faint);
  font-size: .64rem; font-variant-numeric: tabular-nums;
}
.lchap-title { white-space: nowrap; }
.lchap-count { font-variant-numeric: tabular-nums; }
.lchap-opt { color: var(--text-faint); font-size: .62rem; }

/* ---- where a concept comes from, in the panel ---- */

.lpanel-where { display: flex; flex-wrap: wrap; gap: .35rem; align-items: baseline; margin: .2rem 0; }
.lpanel-src { color: var(--text-dim); }
.lpanel-opt {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .05em;
  padding: 0 .3rem; border-radius: var(--radius-s);
  border: 1px solid var(--border-strong); color: var(--text-faint);
}
.lpanel-folded { margin: .5rem 0; }
.lpanel-sections { list-style: none; margin: .2rem 0; padding: 0; display: grid; gap: .2rem; }
.lpanel-sections li {
  display: flex; flex-wrap: wrap; gap: .35rem; align-items: baseline;
  padding: .15rem .35rem; border-left: 2px solid var(--border);
}
.lpanel-sections li[data-optional="true"] { border-left-style: dashed; opacity: .8; }

@media (max-width: 860px) {
  .lchap-title { display: none; }
  .lchap-count { display: none; }
}

.learn-hint {
  position: absolute; left: var(--s3); bottom: var(--s2);
  margin: 0; pointer-events: none;
}
/* Out from under the panel when it is open. The zoom buttons used to
   be in the bottom-right corner of the canvas, which is exactly where
   the panel is. */
.learn-canvas[data-panel="open"] .learn-zoom {
  right: calc(var(--lpanel-w) + var(--s3));
}
@media (max-width: 760px) {
  .learn-canvas[data-panel="open"] .learn-zoom { right: var(--s3); bottom: auto; top: var(--s3); }
  .learn-canvas[data-panel="open"] .learn-hint { display: none; }
}
.learn-zoom {
  position: absolute; right: var(--s3); bottom: var(--s3);
  display: flex; align-items: center; gap: .3rem;
  padding: .2rem .25rem; border-radius: var(--radius);
  background: var(--bg-elev); border: 1px solid var(--border);
}
.learn-zoom .btn { width: 2rem; padding: 0; font-size: 1rem; line-height: 1.8; }
.learn-zoom-at {
  min-width: 3rem; text-align: center; font-family: var(--mono);
}

/* ---- the node panel ---- */

/* Docked to the right of the canvas and resizable by its left edge. The
   width is a custom property so the drag only has to write one number,
   and so the canvas can read it when it works out whether the selected
   box is hidden underneath. */
.learn-panel {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 5;
  width: min(var(--lpanel-w), 100%);
  display: flex; flex-direction: column;
  background: var(--bg-elev);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-3);
  min-height: 0;
}
/* `overscroll-behavior: contain` is the second half of the wheel fix.
   Without it, wheeling to the bottom of the panel hands the rest of the
   gesture to the scroll container behind it — the graph — so reading to
   the end of a concept's resources would start moving the graph under
   the panel. The guard belongs here rather than in JavaScript, because
   only the browser knows which element it decided to chain to. */
.lpanel-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--s4);
  overscroll-behavior: contain;
}
/* The grip is eight pixels wide with a two-pixel line in it: a one-pixel
   border is a one-pixel target. */
.lpanel-grip {
  position: absolute; top: 0; left: -4px; bottom: 0; width: 9px; z-index: 6;
  cursor: col-resize; background: none; border: 0;
}
.lpanel-grip::after {
  content: ''; position: absolute; top: 0; bottom: 0; left: 3px; width: 2px;
  background: transparent; transition: background .12s;
}
.lpanel-grip:hover::after,
.lpanel-grip:focus-visible::after,
.learn-canvas[data-resizing="true"] .lpanel-grip::after { background: var(--accent); }
/* The outline is suppressed and replaced, not dropped: a 2px ring around
   a 9px strip reads as a smudge, where the full-height accent line above
   is unmistakable and taller than the panel is. Declared in
   tests/browser/states.test.mjs beside the other two. */
.lpanel-grip:focus-visible { outline: none; }
.lpanel-grip:focus-visible::after { width: 3px; left: 3px; }
.lpanel-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s3); margin-bottom: .3rem;
}
.lpanel-head h2 { margin: 0; font-size: 1.1rem; }
.lpanel-close {
  flex: none; background: none; border: 0; cursor: pointer;
  color: var(--text-faint); font-size: 1.1rem; line-height: 1; padding: .2rem;
}
.lpanel-close:hover { color: var(--text); }
.lpanel-sub {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--text-faint);
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s2); margin: var(--s4) 0 var(--s2);
}
/* In a flex column an inline-level link stretches to the full width and
   stops looking like a chip. */
.lpanel { display: flex; flex-direction: column; }
.lpanel > .chip-link { align-self: flex-start; }

/* The one thing to do next at this concept, where the eye lands. Only
   drawn when there is one. */
.lpanel-next {
  display: flex; align-items: center; gap: var(--s3);
  margin: var(--s3) 0 0; padding: .5rem .6rem;
  background: var(--bg-inset); border: 1px solid var(--border);
  border-radius: var(--radius-s);
}
.lpanel-next .tiny { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.lpanel-note { margin: -.3rem 0 .4rem; }

.lpanel table { width: 100%; border-collapse: collapse; font-size: .82rem; }
.lpanel th {
  text-align: left; font-weight: 500; font-size: .68rem;
  text-transform: uppercase; letter-spacing: .07em; color: var(--text-faint);
  padding: 0 .5rem .3rem 0; border-bottom: 1px solid var(--border);
}
.lpanel td { padding: .4rem .5rem .4rem 0; border-bottom: 1px solid var(--border); vertical-align: top; }
.lpanel tr:last-child td { border-bottom: 0; }
.lpanel td input { margin: 0; accent-color: var(--ok); }
.lpanel .kind {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint); border: 1px solid var(--border); border-radius: 3px;
  padding: 0 .3em; white-space: nowrap;
}

/* Below the workspace breakpoint the panel is a drawer across the bottom
   rather than a column: a 26rem column on a 390px phone is the whole
   screen, and the graph it is about disappears behind it. Two thirds of
   the height, so the selected box is still on screen above it — which is
   the one thing the panel must not hide. */
@media (max-width: 760px) {
  .learn-panel {
    top: auto; left: 0; right: 0; bottom: 0;
    width: 100%; height: min(58%, 26rem);
    border-left: 0; border-top: 1px solid var(--border);
    border-radius: var(--radius-l) var(--radius-l) 0 0;
  }
  /* Resizing a drawer by its left edge makes no sense. */
  .lpanel-grip { display: none; }
  .graph-bar { padding: .4rem var(--s3); gap: var(--s2); }
  .graph-bar h1 { flex: 1 1 100%; order: -1; }
  .graph-find { flex: 1 1 100%; }
  .graph-acts { margin-left: 0; flex-wrap: wrap; }
}

/* ---------------- the review queue ----------------

   Every row carries the reason it is here, and the reason is a chip
   rather than a colour: five reasons is more than colour can carry, and
   two of them would have to be the same hue anyway. */

.rq-tallies { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 var(--s4); }
.rq-tally {
  padding: .15em .6em; border-radius: 999px;
  background: var(--bg-inset); border: 1px solid var(--border);
  color: var(--text-dim);
}
.rq-tally b { color: var(--text); font-family: var(--mono); }

.rq-list { display: flex; flex-direction: column; gap: var(--s3); }
.rq-row {
  display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s3);
  padding: var(--s3) var(--s4); border-radius: var(--radius);
  background: var(--bg-elev); border: 1px solid var(--border);
  border-left: 3px solid var(--border);
}
.rq-row[data-reason="due"] { border-left-color: var(--warn); }
.rq-row[data-reason="missed"] { border-left-color: var(--accent); }
.rq-row[data-reason="read"] { border-left-color: var(--text-faint); }
.rq-main { flex: 1 1 22rem; min-width: 0; }
.rq-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; }
.rq-title a { font-weight: 600; }
.rq-why {
  padding: .1em .5em; border-radius: 999px; white-space: nowrap;
  background: var(--bg-inset); border: 1px solid var(--border);
  color: var(--text-dim);
}

/* In your words, so it reads as a quotation rather than as a label. */
.rq-missed {
  margin: .45rem 0 .2rem; padding: .35rem 0 .35rem .7rem;
  border-left: 2px solid var(--border-strong);
  font-size: .86rem; color: var(--text);
}
.rq-concepts { margin: .3rem 0 0; }
/* .meta is only styled where it is scoped, so a bare one here had no
   flex and no gap and ran its spans together: "C++•Beginner1 attempts". */
.rq-main .meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  margin-top: .18rem; font-size: .74rem; color: var(--text-faint);
}

.rq-side {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  margin-left: auto;
}
/* Your own rating. Visually apart from everything a grader said,
   because they are different kinds of claim. */
.rq-felt {
  padding: .1em .55em; border-radius: var(--radius-s);
  border: 1px dashed var(--border); color: var(--text-dim);
  white-space: nowrap;
}

/* ---- what I missed, on the verdict ---- */

.missed {
  margin: .7rem 0 0; padding: .6rem .7rem;
  border-radius: var(--radius-s);
  background: var(--bg-inset); border: 1px dashed var(--border);
}
.missed p { margin: 0 0 .35rem; }
.missed-box {
  width: 100%; resize: vertical; font: inherit; font-size: .86rem;
  padding: .4rem .55rem; border-radius: var(--radius-s);
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
}
.missed-box:focus-visible { border-color: var(--accent); }
.missed-saved { margin: .35rem 0 0; color: var(--text-dim); }

@media (max-width: 760px) {
  .rq-side { margin-left: 0; }
}

/* ---------------- online assessments ----------------

   A separate mode and it looks like one: nothing here borrows the
   catalogue's row, because a timed attempt is not a problem you are
   browsing and the two should not be confusable at a glance. */

/* A thing the reader has to know before they act, rather than a thing
   that went wrong. Used where an assessment cannot be started, or
   where part of a result is ungraded. */
.oa-note {
  margin: var(--s3) 0; padding: .6rem .8rem;
  border-radius: var(--radius-s);
  background: var(--bg-inset);
  border: 1px solid var(--border); border-left: 3px solid var(--warn);
}
.oa-note p { margin: 0; font-size: .85rem; }

.oa-cards {
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(min(22rem, 100%), 1fr));
}
.oa-card {
  display: flex; flex-direction: column; gap: .4rem;
  padding: var(--s4); border-radius: var(--radius);
  background: var(--bg-elev); border: 1px solid var(--border);
  color: inherit; text-decoration: none;
  transition: border-color .13s, background .13s;
}
.oa-card:hover { border-color: var(--border-strong); background: var(--bg-elev-2); }
.oa-card[data-state="running"] { border-color: var(--warn); }
.oa-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); }
.oa-card-top h3 { margin: 0; font-size: 1rem; }
.oa-card-blurb { margin: 0; font-size: .85rem; color: var(--text-dim); }
.oa-facts { display: flex; flex-wrap: wrap; gap: .25rem .7rem; color: var(--text-dim); }
.oa-card-foot {
  display: flex; justify-content: space-between; gap: var(--s3);
  margin-top: auto; padding-top: .4rem; border-top: 1px solid var(--border);
}

.oa-head { font-size: 1rem; margin: var(--s5) 0 var(--s3); }

.oa-history { display: flex; flex-direction: column; gap: 1px; }
.oa-row {
  display: grid; align-items: center; gap: var(--s3);
  grid-template-columns: minmax(0, 1fr) auto 7rem 4rem;
  padding: .5rem .7rem; border-radius: var(--radius-s);
  background: var(--bg-elev); color: inherit; text-decoration: none;
  border: 1px solid var(--border);
}
.oa-row:hover { border-color: var(--border-strong); }
.oa-row-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oa-row-state { color: var(--text-faint); }
.oa-row[data-state="running"] .oa-row-state { color: var(--warn); }
.oa-row-score { font-family: var(--mono); text-align: right; }

/* ---- before the clock ---- */

.oa-brief {
  display: grid; gap: 1px; margin: var(--s4) 0;
  background: var(--border); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
}
.oa-fact {
  display: grid; align-items: baseline; gap: .1rem var(--s4);
  grid-template-columns: 9rem minmax(0, 1fr);
  padding: .6rem .8rem; background: var(--bg-elev);
}
.oa-fact-label {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-faint);
}
.oa-fact-value { font-weight: 600; }
.oa-fact-note { grid-column: 2; }

.oa-exec {
  padding: var(--s3) var(--s4); border-radius: var(--radius);
  background: var(--bg-inset); border: 1px solid var(--border);
}
.oa-exec-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem var(--s3);
  padding: .3rem 0; border-top: 1px solid var(--border);
}
.oa-exec-name { font-size: .85rem; }
.oa-exec-row[data-ready="false"] { color: var(--warn); }
.oa-warm { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); margin-top: .6rem; }

/* The limits, said in a box that does not look like a warning — it is
   not a problem, it is what the thing is. */
.oa-honest {
  margin: var(--s4) 0; padding: var(--s3) var(--s4);
  border-radius: var(--radius); border: 1px dashed var(--border);
}
.oa-honest h3 { margin: 0 0 .4rem; font-size: .8rem; color: var(--text-dim); }
.oa-honest ul { margin: 0; padding-left: 1.1rem; }
.oa-honest li { font-size: .82rem; color: var(--text-dim); margin: .25rem 0; }

.oa-start { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); margin: var(--s4) 0; }
.oa-start .small { max-width: 44ch; color: var(--text-dim); }

/* An attempt that is still open, said at the top of the page. The
   clock is a deadline, so one left behind is running down whether or
   not anybody is looking at it — which makes this the most useful
   thing this page can say, not a decoration. */
.oa-resume {
  display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap;
  margin: 0 0 var(--s4); padding: var(--s4);
  border-radius: var(--radius); background: var(--bg-elev);
  border: 1px solid var(--accent); border-left-width: 3px;
}
.oa-resume[data-urgent="true"] { border-color: var(--warn); }
.oa-resume-text { flex: 1 1 22rem; min-width: 0; }
.oa-resume-text p { margin: .2rem 0 0; color: var(--text-dim); }

/* Reporting a bad exercise. Prepared and copied; never sent. */
.report { margin-top: .2rem; }
.report summary { cursor: pointer; color: var(--text-dim); }
.report-body { padding-top: .3rem; display: flex; flex-direction: column; gap: .3rem; }
.report-what {
  width: 100%; font: inherit; font-size: .82rem; padding: .4rem;
  background: var(--bg-inset); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-s);
  resize: vertical;
}
.report-out {
  margin: .4rem 0 0; padding: .5rem; max-height: 14rem; overflow: auto;
  background: var(--bg-inset); border-radius: var(--radius-s);
  font-size: .72rem; white-space: pre-wrap; overflow-wrap: anywhere;
}

/* ---- the attempt ----

   The window, like the code workspace and the graph: the clock and the
   navigator have to stay put while a problem fills the rest. */
/* Immersive by default, with no button to press.

   An assessment used to run inside the ordinary page: the site's
   header above it, with the Content menu, the avatar, the XP and the
   coin purse, and a Focus button in the problem offering to hide
   them. That is three things wrong at once. The navigation goes
   somewhere the reader must not go mid-attempt; the purse and the XP
   are the one part of the site an attempt deliberately does not touch,
   sitting in the corner implying otherwise; and the fix was a control
   the reader had to find.

   So the attempt takes the window, and the only header on screen is
   its own. Nothing is hidden behind a preference, and nothing depends
   on the Fullscreen API being granted — fullscreen is asked for on the
   Start click and is a bonus, not a requirement. */
body[data-attempt="true"] .topbar { display: none; }
body[data-attempt="true"] .main {
  max-width: none; padding: 0;
  height: 100vh;
  overflow: hidden;
}
body[data-attempt="true"] #view-assess-run { height: 100%; min-height: 0; }
body[data-attempt="true"] #assessRun {
  display: grid; grid-template-rows: auto minmax(0, 1fr);
  height: 100%; min-height: 0;
}
.oa-host { min-height: 0; overflow: hidden; }
/* The problem inside it gets the row, not the window: it is nested one
   level deeper than it is in practice. */
body[data-attempt="true"] .ws { height: 100%; }

/* The reported bug, and it made every non-coding question in an
   assessment unanswerable rather than merely awkward.

   A code problem fills `.oa-host` with `.ws`, which is a fixed-height
   grid that scrolls inside its own panes. Everything else — a multiple
   choice, a predicted output, an ordering — fills it with
   `.pwrap.reading`, which is an ordinary block that expects the *page*
   to scroll. Under `body[data-attempt]` the page does not scroll: the
   row is `overflow: hidden`, by design, so the clock cannot be scrolled
   away from.

   Measured before the fix, at 1440x900 on question 1 of the C++
   assessment: 1,064px of content in an 803px row, no scrollbar
   anywhere, the answer box cut off mid-listing and the Submit button at
   y=1076 — 176px below the window, with `elementFromPoint` returning
   null on it. So the question rendered, and could not be answered.

   The scroll therefore belongs to the question, one level in. */
body[data-attempt="true"] .pwrap.reading {
  height: 100%; overflow-y: auto; overscroll-behavior: contain;
}
/* The bottom padding is what stops the Submit button sitting flush
   against the edge of the row, which reads as "there is more below". */
body[data-attempt="true"] .pwrap.reading .pmain { padding-bottom: var(--s6); }

.oa-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s3);
  padding: .45rem var(--s4); flex: none;
  background: var(--bg-elev); border-bottom: 1px solid var(--border);
}
.oa-bar-title { font-weight: 600; font-size: .9rem; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oa-bar .btn { margin-left: auto; }

.oa-clock {
  font-family: var(--mono); font-size: 1rem; font-weight: 600;
  padding: .15em .6em; border-radius: var(--radius-s);
  background: var(--bg-inset); border: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}
.oa-clock[data-state="soon"] { color: var(--warn); border-color: var(--warn); }
.oa-clock[data-state="urgent"],
.oa-clock[data-state="over"] { color: var(--danger); border-color: var(--danger); }

/* The bar's own items. `.oa-bar .btn { margin-left: auto }` used to
   push the single Finish button to the right end; with two buttons and
   a save status that rule would push the first of them instead, so the
   actions are one group and the group is what floats right. */
/* An assessment question read on the practice page after the attempt.
   It is not a practice status, so it does not borrow one of their
   colours. */
.status-word.status-bank {
  color: var(--text-dim); border: 1px solid var(--border);
  border-radius: var(--radius-s); padding: 0 .4em; cursor: help;
}

.oa-bar-acts { display: flex; gap: var(--s2); margin-left: auto; }
.oa-bar .oa-bar-acts .btn { margin-left: 0; }
.oa-progress { color: var(--text-dim); font-variant-numeric: tabular-nums; }
.oa-save[data-state="failed"] { color: var(--danger); font-weight: 600; }
.oa-save[data-state="saved"] { color: var(--ok); }

.oa-nav { display: flex; flex-wrap: wrap; gap: .25rem; }
.oa-dot {
  width: 1.9rem; height: 1.9rem; padding: 0;
  border-radius: var(--radius-s); cursor: pointer;
  font: inherit; font-size: .78rem; font-family: var(--mono);
  background: var(--bg-inset); color: var(--text-dim);
  border: 1px solid var(--border);
}
.oa-dot:hover { border-color: var(--border-strong); color: var(--text); }
/* Three states, three appearances, and none of them is colour alone:
   the title and the aria-label both say which. */
.oa-dot[data-state="attempted"] { border-style: dashed; color: var(--text); }
.oa-dot[data-state="submitted"] { background: var(--accent-soft); border-color: var(--accent); color: var(--text); }
.oa-dot[aria-current="true"] { outline: 2px solid var(--accent); outline-offset: 1px; }

/* What a question inside an attempt is told, which is deliberately not a
   verdict: see problem.js ▸ the feedback policy. */
.oa-state {
  margin: var(--s4) 0; padding: var(--s3) var(--s4); max-width: 70ch;
  border-radius: var(--radius); background: var(--bg-elev);
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
}
.oa-state[data-state="errored"] { border-left-color: var(--warn); }
.oa-state[data-state="review"] { border-left-color: var(--text-dim); }
.oa-state .verdict-head h4 { margin: 0; font-size: .95rem; }
.oa-state p { margin: .3rem 0 0; }

/* Saving / Saved / Save failed. Three states, and the failed one is the
   only one that is allowed to be loud — a page that cannot save and does
   not say so is a page quietly losing work. */
#answerSaved[data-state="failed"] { color: var(--danger); font-weight: 600; }
#answerSaved[data-state="saved"] { color: var(--ok); }

/* ---- afterwards ---- */

.oa-score {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s4);
  margin: var(--s4) 0; padding: var(--s4);
  border-radius: var(--radius); background: var(--bg-elev);
  border: 1px solid var(--border);
}
.oa-score[data-passed="true"] { border-color: var(--ok); }
.oa-score[data-passed="false"] { border-color: var(--danger); }
.oa-score-big { font-size: 2.6rem; font-weight: 700; font-family: var(--mono); line-height: 1; }
.oa-score-side p { margin: 0 0 .15rem; }
.oa-verdict { font-weight: 600; }

.oa-results { display: flex; flex-direction: column; gap: var(--s3); }
.oa-result {
  padding: var(--s3) var(--s4); border-radius: var(--radius);
  background: var(--bg-elev); border: 1px solid var(--border);
  border-left: 3px solid var(--border);
}
.oa-result[data-outcome="right"] { border-left-color: var(--ok); }
.oa-result[data-outcome="partial"] { border-left-color: var(--warn); }
.oa-result[data-outcome="wrong"] { border-left-color: var(--danger); }
.oa-result[data-outcome="errored"] { border-left-color: var(--warn); border-style: dashed; }
.oa-result-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); }
.oa-result-top h3 { margin: 0 0 .2rem; font-size: .95rem; }
.oa-outcome { font-size: .78rem; color: var(--text-dim); white-space: nowrap; }
.oa-source summary { cursor: pointer; color: var(--text-dim); }
.oa-source pre {
  margin: .4rem 0 0; padding: .6rem; max-height: 22rem; overflow: auto;
  background: var(--bg-inset); border-radius: var(--radius-s);
  font-size: .78rem;
}
.oa-result-acts { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); margin-top: var(--s3); flex-wrap: wrap; }
.oa-proof { margin-top: var(--s3); padding-top: var(--s3); border-top: 1px dashed var(--border); }
.oa-proof .btn[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.oa-rubric { margin: .3rem 0 0; padding-left: 1.1rem; }
.oa-rubric li { font-size: .85rem; color: var(--text-dim); margin: .2rem 0; }

.oa-again { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); margin: var(--s5) 0; }
.oa-again .tiny { max-width: 48ch; }

@media (max-width: 760px) {
  .oa-row { grid-template-columns: minmax(0, 1fr) auto; }
  .oa-row-state, .oa-row-score { grid-column: 2; text-align: right; }
  .oa-fact { grid-template-columns: minmax(0, 1fr); }
  .oa-fact-note { grid-column: 1; }
  .oa-bar { padding: .4rem var(--s3); }
  .oa-bar-title { flex: 1 1 100%; }
  .oa-bar-acts { flex: 1 1 100%; margin-left: 0; justify-content: flex-end; }
  .oa-progress { flex: 1 1 auto; }
}

/* A run that never happened, as opposed to one that went wrong.

   Deliberately not styled as a verdict: no score, no case list, and
   none of the red a wrong answer gets. It is information about the
   site, and the reader did nothing to cause it. */
.platform {
  margin: var(--s3) 0; padding: var(--s4);
  border-radius: var(--radius); background: var(--bg-elev);
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
}
.platform[data-kind="service"] { border-left-color: var(--warn); }
.platform-head { margin: 0 0 .3rem; font-size: 1rem; }
.platform-say { margin: 0 0 .4rem; color: var(--text-dim); font-size: .88rem; }
.platform-why { margin: 0 0 .6rem; }
.platform-acts { display: flex; flex-wrap: wrap; gap: var(--s2); }
.platform-detail { margin-top: var(--s3); }
.platform-detail summary { cursor: pointer; color: var(--text-faint); }
.platform-detail p { margin: .3rem 0 0; color: var(--text-dim); }

/* ---------------- hardware exercises ----------------

   The result pane of an hdl problem: a list of named checks, each with
   what was expected and what happened, and the console under it. The
   waveform is its own tab and its own set of rules below.

   Three states per check and they are three different things: passed,
   failed, and never ran. The third is why the colour is not enough on
   its own — "missing" is a hole in the run rather than a wrong value,
   and it says so in words. */
.hdl-checks { display: flex; flex-direction: column; gap: 2px; margin: var(--s3) 0; }
.hdl-check {
  display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap;
  padding: .3rem var(--s3); border-radius: var(--radius-s);
  background: var(--bg-inset); border-left: 3px solid var(--border);
}
.hdl-check[data-state="pass"]    { border-left-color: var(--ok); }
.hdl-check[data-state="fail"]    { border-left-color: var(--danger); }
.hdl-check[data-state="missing"] { border-left-color: var(--warn); border-style: dashed;
  border-left-style: solid; }
.hdl-check-name { font-weight: 600; font-size: .82rem; min-width: 11ch; }
/* A check with a waveform behind it is a button: pressing it opens the
   instant the check is about. It has to look like one without becoming
   a different shape from the rows that are not. */
button.hdl-check {
  font: inherit; text-align: left; width: 100%; cursor: pointer;
  border-top: 0; border-right: 0; border-bottom: 0;
}
button.hdl-check:hover { background: var(--bg-elev-2); }
button.hdl-check:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.hdl-check-go { margin-left: auto; color: var(--accent); white-space: nowrap; }
button.hdl-check:hover .hdl-check-go { text-decoration: underline; }

/* The one-time explanation of the four tabs. */
.hdl-walk {
  margin: 0 0 var(--s4); padding: var(--s3) var(--s4);
  border-radius: var(--radius); background: var(--bg-elev);
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
}
.hdl-walk summary { cursor: pointer; font-weight: 600; }
.hdl-walk-body { padding-top: .4rem; }
.hdl-walk-body ol { margin: 0 0 .5rem; padding-left: 1.2rem; }
.hdl-walk-body li { margin: .3rem 0; font-size: .88rem; color: var(--text-dim); }
.hdl-walk-body li b { color: var(--text); }
.hdl-walk-body p { margin: .3rem 0; }

/* What x and z mean, beside the colours they are drawn in. */
.wave-legend { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); margin: .3rem 0; }
.wave-key { display: flex; align-items: baseline; gap: .35rem; color: var(--text-dim);
  flex: 1 1 22rem; }
.wave-key i { width: .7rem; height: .7rem; border-radius: 2px; flex: none;
  position: relative; top: .1rem; }
.wave-key[data-v="x"] i { background: var(--danger); opacity: .45; }
.wave-key[data-v="z"] i { background: var(--warn); opacity: .45; }
.wave-key b { font-family: var(--mono); color: var(--text); }
.hdl-head { margin: var(--s4) 0 .3rem; font-size: .8rem; color: var(--text-dim); }
/* Everything the design and the testbench printed, verbatim. Bounded,
   because a $monitor on a wide bus produces megabytes in a few
   simulated microseconds and the runner caps what it keeps. */
.run-out-pre {
  margin: 0; padding: var(--s3); max-height: 22rem; overflow: auto;
  background: var(--bg-inset); border-radius: var(--radius-s);
  font-size: .78rem; white-space: pre-wrap; overflow-wrap: anywhere;
}

/* Compiler messages, with the file and the line in front of each. */
.diags { display: flex; flex-direction: column; gap: 2px; }
.diag {
  display: grid; grid-template-columns: auto auto minmax(0, 1fr); gap: var(--s3);
  align-items: baseline; padding: .3rem var(--s3);
  border-radius: var(--radius-s); background: var(--bg-inset);
  border-left: 3px solid var(--border);
}
.diag[data-level="error"] { border-left-color: var(--danger); }
.diag[data-level="warning"] { border-left-color: var(--warn); }
.diag[data-level="sorry"] { border-left-color: var(--accent); }
.diag-where { color: var(--text-dim); white-space: nowrap; }
.diag-level { color: var(--text-faint); text-transform: lowercase; }
.diag-msg { font-size: .85rem; overflow-wrap: anywhere; }
@media (max-width: 700px) {
  .diag { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------- the waveform viewer ----------------

   A canvas for the traces, DOM for everything else. The names column
   is a fixed width so the lanes line up with their labels, and the
   canvas takes the rest — the lane height is in wave.js and the two
   have to agree, which is why the row height is written here as a
   variable rather than twice. */
.wave { display: flex; flex-direction: column; gap: var(--s2); }
.wave-bar {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
}
.wave-bar .btn { padding: .15rem .5rem; }
.wave-baselab { display: inline-flex; align-items: center; gap: .3rem; }
.wave-base {
  font: inherit; font-size: .78rem; padding: .1rem .2rem;
  background: var(--bg-inset); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-s);
}
.wave-at { margin-left: auto; color: var(--accent); }

.wave-pick summary { cursor: pointer; color: var(--text-dim); }
.wave-pick-body {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: 0 var(--s3); max-height: 9rem; overflow-y: auto;
  padding: var(--s2) 0; overscroll-behavior: contain;
}
.wave-pick-row { display: flex; align-items: center; gap: .35rem; min-width: 0; }
.wave-pick-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.wave-plot {
  display: grid; grid-template-columns: 11rem minmax(0, 1fr);
  background: var(--bg-inset); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
}
.wave-names { display: flex; flex-direction: column; border-right: 1px solid var(--border); }
/* Matches AXIS_H in wave.js: the first row of the names column is the
   space the time axis takes, so the names line up with their lanes. */
.wave-name-axis { height: 22px; flex: none; }
.wave-name {
  height: 26px; margin-bottom: 4px; flex: none;
  display: flex; align-items: center; gap: .3rem;
  padding: 0 var(--s3); font-size: .76rem;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.wave-canvas { display: block; width: 100%; }
.wave-canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.wave-cursor-info {
  display: flex; flex-wrap: wrap; gap: var(--s3) var(--s4);
  padding: var(--s2) 0; min-height: 1.4rem;
}
.wave-val b { color: var(--text); }
/* A value with an x or a z in it is not a number and is not shown as
   one. Marked so it cannot be skimmed past. */
.wave-val b[data-unknown="true"] { color: var(--danger); }

.wave-host { min-width: 0; }

/* ---------------- mathematics ----------------

   KaTeX sets its own type; what is needed here is the space around it and
   the behaviour of a long expression on a narrow screen. */
.katex { font-size: 1.04em; }
.katex-display {
  margin: var(--s3) 0;
  /* A long expression scrolls inside itself rather than pushing the page
     sideways — but silently clipping it would be worse than either, so the
     scrollbar is left visible rather than hidden until hover. */
  overflow-x: auto; overflow-y: hidden;
  padding: .2rem 0 .35rem;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.katex-display::-webkit-scrollbar { height: 6px; }
.katex-display::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: 3px;
}
.katex-display > .katex { text-align: left; }
/* A long expression scrolls inside itself rather than pushing the page
   sideways, which is the same rule the code blocks follow. */
.prose .katex-display { max-width: 100%; }

/* When KaTeX did not load. The source is better than nothing. */
.math-raw { color: var(--text-dim); font-family: var(--mono); font-size: .9em; }
.math-bad { color: var(--warn); }

/* ---------------- maths answer fields ---------------- */

.maths-answer { display: grid; gap: .35rem; margin: var(--s3) 0; max-width: 42rem; }
.maths-input {
  font-family: var(--mono); font-size: .95rem;
  padding: .55rem .75rem; width: 100%;
  background: var(--bg-inset); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius-s);
}
.maths-input:focus { border-color: var(--accent); }
.maths-input[data-mark="wrong"] { border-color: var(--danger); }
.maths-input:disabled { opacity: .6; }
.maths-syntax { margin: 0; }

/* The live reading. Most "wrong" answers in a maths tool are the tool
   reading something other than what was meant, and showing the reading
   makes that visible before submitting rather than after. */
.maths-preview { margin: 0; min-height: 1.2em; font-size: .8rem; }
.maths-preview[data-state="empty"] { visibility: hidden; }
.maths-preview[data-state="ok"] { color: var(--text-dim); }
.maths-preview[data-state="bad"] { color: var(--warn); }
.maths-read { font-family: var(--mono); color: var(--text); }
.maths-truth { margin: .4rem 0 0; color: var(--ok); }

/* ---------------- proofs ---------------- */

.proof-claim {
  border-left: 3px solid var(--accent); padding: .1rem 0 .1rem var(--s3);
  margin-bottom: var(--s4);
}
.proof-steps { list-style: decimal; margin: 0; padding-left: 1.6rem; display: grid; gap: var(--s3); }
.proof-step { padding: .4rem .6rem; border-radius: var(--radius-s); }
.proof-step[data-mark="wrong"] { background: color-mix(in srgb, var(--danger) 12%, transparent); }
.proof-step[data-mark="right"] { background: color-mix(in srgb, var(--ok) 12%, transparent); }
.proof-text > :first-child { margin-top: 0; }
.proof-text > :last-child { margin-bottom: 0; }
.proof-why {
  margin-top: .35rem; width: min(100%, 36rem);
  font: inherit; font-size: .84rem; padding: .3rem .5rem;
  background: var(--bg-elev); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius-s);
}

.proof-box {
  width: 100%; margin-top: var(--s2);
  font-family: var(--mono); font-size: .88rem; line-height: 1.6;
  padding: .7rem .85rem; resize: vertical;
  background: var(--bg-inset); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius-s);
}
.proof-box:focus { border-color: var(--accent); }

.cx-claim {
  border: 1px solid var(--border-strong); border-left-width: 3px;
  border-radius: var(--radius-s); padding: var(--s3); margin-bottom: var(--s3);
  background: var(--bg-elev);
}

/* ---------------- the self-review pane ---------------- */

.verdict[data-kind="review"] { border-left-color: var(--accent); }
.review-pane {
  margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--border);
}
.review-pane h4 { margin: 0 0 .3rem; font-size: .9rem; }
.rubric { list-style: none; margin: var(--s2) 0 0; padding: 0; display: grid; gap: .45rem; }
.rubric li { display: flex; gap: .55rem; align-items: flex-start; font-size: .86rem; line-height: 1.5; }
.rubric input { margin: .25rem 0 0; flex: none; accent-color: var(--ok); }

/* ---------------- figures ----------------

   Inline SVG drawn from the theme's own tokens, so a figure repaints with
   the theme instead of being a dark-mode picture on a light page. */
.mfig { margin: var(--s4) 0; }
.mfig-svg { width: 100%; max-width: 32rem; height: auto; display: block; }
.mfig figcaption {
  margin-top: var(--s2); font-size: .8rem; line-height: 1.55;
  color: var(--text-dim); max-width: 60ch;
}
.mfig-axis { stroke: var(--border-strong); stroke-width: 1; }
.mfig-seg  { stroke: var(--accent); stroke-width: 3; }
.mfig-tick, .mfig-label { fill: var(--text-dim); font-size: 11px; font-family: var(--mono); }
.mfig-shape { fill: none; stroke-width: 2; }
.mfig-one { stroke: var(--diff-beginner); }
.mfig-two { stroke: var(--accent); }
.mfig-inf { stroke: var(--diff-advanced); }
.mfig-dot { fill: var(--bg); stroke: var(--accent); stroke-width: 2; }
.mfig-dot.mfig-filled { fill: var(--accent); }
.mfig-node rect { fill: var(--bg-elev); stroke: var(--border-strong); stroke-width: 1; }
.mfig-node.mfig-hit rect  { stroke: var(--ok); }
.mfig-node.mfig-miss rect { stroke: var(--danger); }
.mfig-edge { stroke: var(--border-strong); stroke-width: 1; }
/* The legend. Set a size here rather than inheriting, so it stays legible
   when the figure is scaled down on a phone. */
.mfig-key .mfig-label { font-size: 12px; }

/* ---- a workspace of several files ----

   Left: the explorer, which collapses to nothing and leaves the tabs.
   Right: one row of tabs and a stack of editors, exactly one of which
   is visible. The hidden ones are `hidden`, not removed: each is a live
   CodeMirror holding a cursor and an undo history. */
.mf {
  --files-w: 190px;
  min-height: 0; display: grid;
  grid-template-columns: var(--files-w) auto minmax(0, 1fr);
}
.mf[data-files="false"] { grid-template-columns: 0 0 minmax(0, 1fr); }
.mf[data-files="false"] .mf-side,
.mf[data-files="false"] .mf-gutter { display: none; }
/* Same reason as .ws-body: a hidden item leaves no track behind it, so
   the column each child sits in is stated rather than inferred. */
.mf > .mf-side   { grid-column: 1; }
.mf > .mf-gutter { grid-column: 2; }
.mf > .mf-main   { grid-column: 3; }

/* The bar between the explorer and the editor. Narrower than the one
   between the statement and the workspace, because this one lives
   inside a panel rather than between two of them, but the same idea:
   the element is the hit area and the line you can see is drawn inside
   it. */
.mf-gutter {
  position: relative; width: 10px; cursor: col-resize;
  border: 0; padding: 0; background: none; touch-action: none;
}
.mf-gutter::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 4px;
  width: 2px; border-radius: 1px; background: var(--border);
  transition: background .14s;
}
.mf-gutter:hover::before,
[data-dragging] > .mf-gutter::before { background: var(--accent); }
.mf-gutter:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.mf-side {
  min-width: 0; min-height: 0; display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--radius-s);
  background: var(--bg-elev); overflow: auto;
}
.mf-side-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: .35rem .5rem; border-bottom: 1px solid var(--border);
  text-transform: uppercase; letter-spacing: .06em;
}
.mf-tree, .mf-group { list-style: none; margin: 0; padding: 0; display: grid; gap: .05rem; }
.mf-tree { padding: .25rem; }
.mf-group[hidden] { display: none; }

/* One row shape for a directory and for a file, indented by how deep
   it is. The depth is a custom property rather than a class per level
   so three levels — which is all a workspace path may have — need no
   three rules. */
.mf-file, .mf-dir {
  --mf-depth: 0;
  width: 100%; display: flex; align-items: center; gap: .35rem;
  appearance: none; border: 0; background: none; cursor: pointer;
  padding: .3rem .4rem;
  padding-left: calc(.4rem + var(--mf-depth) * .8rem);
  border-radius: var(--radius-s);
  font-family: var(--mono); font-size: .78rem; color: var(--text-dim);
  text-align: left;
}
.mf-file:hover, .mf-dir:hover { background: var(--bg-elev-2); color: var(--text); }
.mf-file[aria-current="true"] { background: var(--accent-soft); color: var(--text); }
.mf-file:focus-visible, .mf-dir:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
.mf-dir { color: var(--text-faint); }
.mf-caret {
  flex: 0 0 auto; width: .7rem; font-size: .6rem;
  transition: transform .12s;
}
.mf-dir[aria-expanded="true"] .mf-caret { transform: rotate(90deg); }
.mf-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mf-dot {
  width: .4rem; height: .4rem; border-radius: 50%; flex: 0 0 auto;
  background: transparent;
}
/* A file you have changed. The only way to see at a glance which of
   three you have actually touched. */
.mf-file[data-edited="true"] .mf-dot,
.mf-tab[data-edited="true"]::before { background: var(--accent); }
.mf-tag {
  font-size: .6rem; text-transform: uppercase; letter-spacing: .05em;
  padding: 0 .25rem; border-radius: var(--radius-s);
  border: 1px solid var(--border-strong); color: var(--text-faint);
  font-family: var(--font);
}
.mf-tag-ro { color: var(--text-faint); }
.mf-side-note { padding: .4rem .5rem; margin: auto 0 0; }

.mf-main { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); }
.mf-tabs {
  display: flex; gap: .15rem; align-items: center;
  border-bottom: 1px solid var(--border); overflow-x: auto;
}
.mf-toggle {
  appearance: none; border: 0; background: none; cursor: pointer;
  color: var(--text-faint); padding: .25rem .4rem; font-size: .9rem;
  border-radius: var(--radius-s); flex: 0 0 auto;
}
.mf-toggle:hover { color: var(--text); background: var(--bg-elev-2); }
.mf-tab {
  appearance: none; border: 0; background: none; cursor: pointer;
  padding: .3rem .55rem; font-family: var(--mono); font-size: .76rem;
  color: var(--text-faint); border-bottom: 2px solid transparent;
  white-space: nowrap; display: flex; align-items: center; gap: .3rem;
}
.mf-tab::before {
  content: ''; width: .35rem; height: .35rem; border-radius: 50%;
  background: transparent;
}
.mf-tab:hover { color: var(--text-dim); }
.mf-tab[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); }

.mf-stack { min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr); }
.mf-slot { min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr); }
.mf-slot[hidden] { display: none; }
.mf-slot .ed { min-height: 0; height: auto; }

/* The workspace takes the whole editor row; the host grid above it is
   written for one editor and its status line. */
.ws-editor .ed-host-multi { grid-template-rows: minmax(0, 1fr); }

/* Narrow: the explorer stops being a column and becomes a short band
   above the tabs, because 190px of file names beside a 320px editor is
   not a trade anybody wants. The gutter has nothing to resize then, so
   it goes. */
@media (max-width: 860px) {
  .mf { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
  .mf[data-files="false"] { grid-template-columns: minmax(0, 1fr); }
  .mf > .mf-side, .mf > .mf-main { grid-column: auto; }
  .mf-gutter { display: none; }
  .mf[data-files="true"] .mf-side { max-height: 8rem; margin-bottom: var(--s2); }
}

/* The file a diagnostic is in, in front of its line and column. */
.diag-file { color: var(--text-faint); margin-right: .25rem; }

/* A submitted workspace, in the Submissions tab: one block per file. */
.sub-files { display: grid; gap: .5rem; }
.sub-file-name {
  display: flex; align-items: center; gap: .4rem;
  margin: 0 0 .2rem; font-size: .76rem; color: var(--text-dim);
}

/* ---- a project, and where you are in it ---- */
.proj {
  border: 1px solid var(--border); border-radius: var(--radius-s);
  background: var(--bg-elev); padding: .5rem .6rem; margin: 0 0 var(--s3);
}
.proj-head { display: flex; align-items: baseline; gap: .45rem; flex-wrap: wrap; }
.proj-kind {
  text-transform: uppercase; letter-spacing: .06em; color: var(--accent);
}
.proj-title { font-weight: 600; }
.proj-steps {
  list-style: none; margin: .45rem 0 0; padding: 0;
  display: flex; gap: .3rem; flex-wrap: wrap;
}
.proj-step { min-width: 0; }
.proj-link {
  display: flex; align-items: center; gap: .35rem;
  padding: .25rem .5rem; border-radius: var(--radius-s);
  border: 1px solid var(--border); color: var(--text-dim);
  font-size: .78rem; text-decoration: none;
}
a.proj-link:hover { border-color: var(--border-strong); color: var(--text); }
.proj-step[data-here="true"] .proj-link {
  border-color: var(--accent); background: var(--accent-soft); color: var(--text);
}
.proj-step[data-state="solved"] .proj-n { color: var(--ok); }
.proj-n { font-family: var(--mono); font-size: .72rem; color: var(--text-faint); }
.proj-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.proj-note { margin: .45rem 0 0; }

/* Reset is a menu on a workspace: one file, or all of them. It is the
   ⋯ menu's body with the summary styled as the button it replaces. */
.ws-reset > summary { list-style: none; }
.ws-reset .ws-menu-body { min-width: 15rem; }
.ws-reset .ws-menu-body > button { justify-content: space-between; width: 100%; }

/* The row badge in the catalogue. */
.badge-project { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
