/* ==========================================================================
   codeschool.ing — Student Portal

   A layer on top of `base.css`, which is the vitrine's CSS copied with no
   changes. Everything that gives identity — colours, typography, the terminal
   frame, the light theme, the graph — comes from there and is not repeated
   here. This file only has what the vitrine never needed: a three-zone shell
   and the study screens.

   WHAT DID NOT CROSS OVER: `.screen{height:100vh}`, the fullpage. It presumes
   a linear narrative — seven screens, you scroll once and you have reached the
   end. In a portal the scrolling is continuous and the content has
   unpredictable height.
   ========================================================================== */

/* ==========================================================
   1 · SHELL
   ========================================================== */

/* THE COLOUR SCHEME HAS TO BE DECLARED, NOT JUST PAINTED.

   The portal is dark because the CSS paints everything dark — but none of that
   counts for the BROWSER, which still believes it is on a light page. What it
   draws on its own comes out in the wrong theme: the light-grey scrollbar, the
   `<select>`, the little clear button in the search field.

   It does not look the same everywhere, and that is how the defect arrived: in
   Firefox the thin bar is discreet enough to go unnoticed; in Chromium (and
   therefore in Brave, Chrome and Edge) it is a light strip in the middle of a
   dark screen.

   `color-scheme` is the declaration that was missing. It follows the theme, and
   with that the browser draws what is its own in the right tone — including
   before the CSS loads, which also kills the white flash. */
:root{color-scheme:dark}
html[data-theme="light"]{color-scheme:light}

/* `hidden` has to beat the component classes. The browser rule for `[hidden]`
   has zero specificity, so `.btn{display:inline-flex}` runs it over — and the
   "Try again" button showed up before there was anything to redo. It holds for
   any element hidden by attribute, which is how the portal hides what should
   not be seen yet (the justification of a choice, for example). */
[hidden]{display:none !important}

/* THE CONTENT IS NOT SELECTABLE, and the code button is the one way out.
   See app/copy.js for what this buys and what it does not: the shipped
   single-file bundle carries every lesson inline, so this is friction, not
   protection.

   The exemption is everything the student writes into — name, e-mail, password,
   search, notes and the answer fields, the code editor included. That work is
   theirs, and a field you cannot select is a field you cannot fix a typo in.
   The rule lives on the two properties on purpose: Safari still wants the
   prefixed one, and losing it there would silently undo the whole thing. */
.portal-body{overflow-y:auto;-webkit-user-select:none;user-select:none}
input,textarea,[contenteditable="true"]{-webkit-user-select:text;user-select:text}

/* `base.css` styles the `nav` ELEMENT — the vitrine has exactly one, the fixed
   top bar, and it never needed a class. The portal has others: the side rail
   and the lesson breadcrumbs are real navigation and deserve the right markup.
   Without this rule both inherit `position:fixed` and become a second bar on
   top of the content — which is what happened, and it is the kind of collision
   that only shows up on screen.

   The neutralisation lives here, and not there, because `base.css` is a copy of
   the vitrine and has to stay syncable: diverging from it costs more than this
   rule. */
.portal nav{
  position:static;height:auto;padding:0;z-index:auto;
  background:none;-webkit-backdrop-filter:none;backdrop-filter:none;border-bottom:0;align-items:stretch;
}

/* THE LESSON HAS ONE WIDTH, and that width has two values.

     --rail    the menu column
     --reading   820px: the narrow column, where the lesson lives while the
                 annotated code block still fits stacked. Prose above ~68
                 characters per line tires the eye, and this is the number that
                 gives 68.
     --wide     the wide column, where the lesson lives when the code block
                 opens in two columns — it has one column that does not wrap for
                 comfort, and that column is what dictates the size.
     --screen      which of the two is in force right now. EVERYTHING in the lesson
                 uses this one: the title, the breadcrumbs, the steps, the prose,
                 the player, the example, the material, the footer.

   This was the third attempt, and the first two got it wrong the same way. The
   first let the player bleed edge to edge while the text stayed at 820. The
   second released the example (and later the player) to `--wide` and left the
   rest at 820. Both produced the SAME thing: different left margins inside one
   lesson, the eye hunting for where the line starts on every scroll.

   A single variable settles it by construction — there is no way for one
   element to disagree with another, not even mid-resize. And the test stopped
   asking "who went past the ceiling?" and started asking "do they all start in
   the same column?", which is the real rule.

   The price is declared: at `--wide` the prose line reaches 121ch (measured),
   against the comfortable 68. That is what a single width costs, and it only
   happens on a large screen, where the alternative was the misalignment.

   The navigation arrows live in the gap that is left over, and they derive from
   `--screen` too: when the lesson widens, the gap shrinks and they follow. */
.portal{
  --rail:380px;
  --reading:820px;
  /* The annotated code block has two columns, and the right one does NOT wrap
     for comfort: either the program fits, or a scrollbar appears. So its width
     comes from a MEASUREMENT of the content, not from a guess.

       IBM Plex Mono at .79rem advances 7.6px per character (measured in the
       browser); the longest line in the examples has 74 characters → 562px
       plus the block's 36px of padding                             → 598px

     `--code` is 604: a little more than that, and the slack is the six pixels
     that keep the bar from appearing over a single character. `--note` is the
     comfort of the text column; `--note-min`, the minimum below which it stops
     being readable and the two columns stop being worth it. */
  --code:604px;
  --note:440px;
  --note-min:300px;
  /* THE CEILING OF THE WIDE COLUMN COMES FROM THE ARROW, not from a chosen
     number. The arrows sit in the middle of the gap and are 44px wide; to leave
     16px between the arrow and the content, the gap on each side needs
     4 × 38 = 152px in total:

       --wide <= 100vw - rail - 152

     Until that is the tighter bound, the one in charge is `.content`'s
     padding. And the floor is the block's content: `--code + --note + 30`, above
     which widening would only stretch the text column. */
  --wide:min(
    calc(var(--code) + var(--note) + 30px),
    calc(100vw - var(--rail) - 2 * var(--pad-x))
  );
  --screen:var(--reading);
  display:grid;
  /* 380 and not 272: at 272 nearly every lesson name wrapped onto two or three
     lines, and a list of thirteen wrapped items becomes a wall. The 50px that
     came later buy the difference between "Front-end with React and TypeScript"
     on one line or on two. */
  grid-template-columns:var(--rail) minmax(0,1fr);
  min-height:100vh;
  padding-top:64px;               /* the bar is fixed and has a fixed height */
}
body.no-rail .portal{grid-template-columns:minmax(0,1fr)}

/* The "confirm your e-mail" nudge. It lives BETWEEN the fixed bar and the grid,
   so it clears the bar itself (margin-top) and the grid drops its own 64px while
   the bar is shown — otherwise the two stack and leave a gap. Amber is the one
   attention colour the portal already uses; this is a nudge, not an error, so it
   is a tint and a single bold word, nothing louder. */
.verify-banner{
  margin-top:64px;
  align-items:center;gap:12px;
  padding:10px clamp(16px,4vw,28px);
  background:rgba(255,77,94,.1);
  border-bottom:1px solid var(--wire);
  font-size:.9rem;line-height:1.45;color:var(--paper);
}
.verify-banner:not([hidden]){display:flex}
body.banner-on .portal{padding-top:0}
.vb-text{flex:1 1 auto;min-width:0}
.vb-text strong{color:var(--amber)}
.vb-addr{font-family:'IBM Plex Mono',monospace;word-break:break-all}
.vb-close{
  flex:0 0 auto;border:0;background:none;cursor:pointer;
  color:var(--paper-dim);font-size:1.25rem;line-height:1;padding:2px 8px;border-radius:4px;
}
.vb-close:hover{color:var(--paper);background:var(--panel)}
body.no-rail .rail{display:none}

/* The padding becomes a VARIABLE because it is not only padding: the edge-to-
   edge video cancels it with a negative margin, and a number repeated in two
   places diverges the day one of them changes. Which is what happened — the
   narrow padding became 16px here and stayed 18px there, and 2px of horizontal
   scrolling were left over on the phone. */
.content{
  --pad-x:clamp(18px,3.2vw,44px);
  --pad-y:26px;
  min-width:0;
  padding:var(--pad-y) var(--pad-x) 60px;
}
.content:focus{outline:none}

/* ---------- side rail ---------- */
/* SPACING IS LEGIBILITY. The first version was dense — 2px of breathing room
   between 8px-tall lines — and a list of thirteen lessons with their sections
   open became a wall that is read line by line instead of being swept with the
   eye. The air between the items is what lets the menu be CONSULTED rather than
   read. */
.rail{
  position:sticky;top:64px;align-self:start;
  height:calc(100vh - 64px);overflow-y:auto;
  border-right:1px solid var(--wire);
  padding:26px 18px 48px;
  /* `scrollbar-width` alone is not enough: without the COLOUR, Chromium uses
     the system one. The vitrine always declares the two together (see
     `.courses-scroll` in base.css), and the rail had been left with only the first
     half. The `::-webkit-` rule is the same patch from there, for the versions
     that still ignore `scrollbar-color`. */
  scrollbar-width:thin;scrollbar-color:var(--wire) transparent;
}
.rail::-webkit-scrollbar{width:7px}
.rail::-webkit-scrollbar-track{background:transparent}
.rail::-webkit-scrollbar-thumb{background:var(--wire);border-radius:4px}
.rail:hover::-webkit-scrollbar-thumb{background:var(--phosphor-dim)}
.rail-nav{display:flex;flex-direction:column;gap:6px;margin-bottom:30px}
.rail-link{
  padding:11px 14px;border-radius:4px;font-size:.94rem;color:var(--paper-dim);
  border-left:2px solid transparent;transition:color .15s,background .15s;
}
.rail-link:hover{color:var(--paper);background:var(--panel)}
.rail-link.on{color:var(--phosphor);border-left-color:var(--phosphor);background:var(--panel)}

.rail-sec{display:flex;flex-direction:column;gap:12px}
.rail-tit{
  font-family:'IBM Plex Mono',monospace;font-size:.68rem;letter-spacing:.12em;
  text-transform:uppercase;color:var(--phosphor-dim);padding:0 11px;
}
.rail-count{font-family:'IBM Plex Mono',monospace;font-size:.68rem;color:var(--paper-dim);padding:0 11px}
.rail-back{
  display:inline-block;margin-bottom:16px;padding:0 11px;
  font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--phosphor);
}

.rail-courses{display:flex;flex-direction:column;gap:4px}
.rail-course{
  display:grid;grid-template-columns:8px 1fr auto;align-items:center;gap:10px;
  padding:11px 14px;border-radius:4px;font-size:.88rem;color:var(--paper-dim);line-height:1.35;
}
.rail-course:hover{background:var(--panel);color:var(--paper)}
.tc-count{font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--phosphor-dim)}
.tc-mark{width:8px;height:8px;border-radius:50%;border:1px solid var(--wire)}
.tc-mark[data-state="done"]{background:var(--phosphor);border-color:var(--phosphor)}
.tc-mark[data-state="current"]{background:var(--amber);border-color:var(--amber)}
.tc-mark[data-state="available"]{border-color:var(--phosphor)}

/* A LESSON IS A HEADING, A SECTION IS A LINE. Before, both were similar lines
   with different indents, and there was no telling which was which without
   reading. The lesson gets monospaced uppercase and a counter; the section
   stays in normal text with an icon. Two different natures, two different
   shapes. */
.rail-lessons{display:flex;flex-direction:column;gap:6px}
.rail-lesson{
  display:grid;grid-template-columns:22px 1fr auto;align-items:center;gap:8px;
  border-radius:4px;padding:3px 8px 3px 2px;
}
.rail-lesson:hover{background:var(--panel)}
.rail-lesson.on{background:var(--panel);box-shadow:inset 2px 0 0 var(--phosphor)}
.ta-open{
  display:grid;place-items:center;width:20px;height:26px;
  background:none;border:0;color:var(--phosphor-dim);cursor:pointer;
}
.ta-open svg{width:13px;height:13px;transition:transform .18s}
.rail-lesson.is-open .ta-open svg{transform:rotate(-180deg)}
.ta-title{display:flex;flex-direction:column;gap:3px;padding:10px 0;min-width:0}
.ta-num{
  font-family:'IBM Plex Mono',monospace;font-size:.58rem;letter-spacing:.12em;
  text-transform:uppercase;color:var(--phosphor-dim);
}
.rail-lesson.on .ta-num{color:var(--phosphor)}
.ta-tit{
  font-family:'Space Grotesk',sans-serif;font-weight:400;font-size:.88rem;
  line-height:1.35;color:var(--paper);
}
.rail-lesson.done .ta-tit{color:var(--paper-dim)}
.ta-count{font-family:'IBM Plex Mono',monospace;font-size:.62rem;color:var(--phosphor-dim)}

.rail-sections{display:flex;flex-direction:column;gap:3px;margin:4px 0 10px}
.rail-section,.rail-exam{
  display:grid;grid-template-columns:16px 1fr;align-items:center;gap:11px;
  padding:9px 12px 9px 30px;border-radius:4px;
  font-size:.85rem;line-height:1.45;color:var(--paper-dim);
}
.ts-middle{display:flex;flex-direction:column;gap:2px;min-width:0}
.ts-dur{font-size:.62rem;color:var(--phosphor-dim);letter-spacing:.06em}
.rail-section:hover,.rail-exam:hover{background:var(--panel);color:var(--paper)}
.rail-section.on,.rail-exam.on{color:var(--phosphor);background:var(--panel)}
/* play for what is left, check for what is done: the icon states the status
   without leaning on colour alone, and it also says that this is something to
   go through */
.ts-mark{display:grid;place-items:center;color:var(--phosphor-dim)}
.ts-mark svg{width:14px;height:14px}
.rail-section.done .ts-mark,.rail-exam.done .ts-mark{color:var(--phosphor)}
.rail-section.on .ts-mark,.rail-exam.on .ts-mark{color:var(--phosphor)}
.rail-section.done .ts-title,.rail-exam.done .ts-title{color:var(--paper-dim)}
.rail-section.assessment .ts-title{font-style:italic}

/* ---------- top bar: only what the vitrine did not have ---------- */
.nav-context{margin-left:26px;min-width:0;flex:1 1 auto}
.ctx-box{position:relative}
.ctx{
  display:flex;align-items:center;gap:10px;max-width:460px;
  background:none;border:1px solid transparent;border-radius:4px;
  padding:5px 9px;color:var(--paper-dim);cursor:pointer;font:inherit;
}
.ctx:hover{color:var(--paper);border-color:var(--wire)}
.ctx-arrow{font-size:.7rem;color:var(--phosphor-dim)}
.ctx-menu{
  display:none;position:absolute;top:calc(100% + 8px);left:0;min-width:280px;max-height:70vh;overflow-y:auto;
  scrollbar-width:thin;scrollbar-color:var(--wire) transparent;
  background:var(--panel);border:1px solid var(--wire);border-radius:5px;padding:5px;z-index:120;
}
.ctx-box.is-open .ctx-menu{display:block}
.ctx-group{
  display:block;padding:9px 11px 4px;font-family:'IBM Plex Mono',monospace;
  font-size:.62rem;letter-spacing:.12em;text-transform:uppercase;color:var(--phosphor-dim);
}
.ctx-op{
  display:block;width:100%;text-align:left;padding:7px 11px;border-radius:4px;
  background:none;border:0;color:var(--paper-dim);font:inherit;font-size:.86rem;cursor:pointer;
}
.ctx-op:hover{background:var(--ink);color:var(--paper)}
.ctx-op.on{color:var(--phosphor)}
.ctx-map{font-family:'IBM Plex Mono',monospace;font-size:.74rem;color:var(--phosphor);border-bottom:1px solid var(--wire);border-radius:0;margin-bottom:4px}
.ctx-name{font-size:.84rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ctx-bar{flex:0 0 84px;height:4px;border-radius:2px;background:var(--wire);overflow:hidden}
.ctx-bar span{display:block;height:100%;background:var(--phosphor)}
.ctx-pct{font-family:'IBM Plex Mono',monospace;font-size:.7rem;color:var(--phosphor)}

/* `base.css` does NOT reset the `button` element: every vitrine component
   declares its own background (`.theme`, `.burger`, `.nav-cta`…). Every new
   button has to declare its own, otherwise it inherits the browser's light grey
   — which is what left the magnifier, the avatar and the ordering arrows white
   on the dark theme. */
.nav-icon{
  display:flex;align-items:center;justify-content:center;width:32px;height:32px;
  background:none;border:0;color:var(--paper-dim);border-radius:4px;cursor:pointer;
}
.nav-icon:hover{color:var(--phosphor);background:var(--panel)}
.nav-icon svg{width:17px;height:17px}

.account{position:relative}
.account-btn{display:flex;align-items:center;background:none;border:0;padding:0;cursor:pointer}
.account-avatar{
  display:grid;place-items:center;width:30px;height:30px;border-radius:50%;
  background:var(--panel);border:1px solid var(--wire);color:var(--phosphor);
  font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:.82rem;
}
.account-btn:hover .account-avatar{border-color:var(--phosphor)}
.account-menu{
  display:none;position:absolute;top:calc(100% + 8px);right:0;min-width:186px;
  background:var(--panel);border:1px solid var(--wire);border-radius:5px;
  padding:5px;flex-direction:column;z-index:120;
}
.account.is-open .account-menu{display:flex}
.account-op{padding:8px 11px;border-radius:4px;font-size:.86rem;color:var(--paper-dim)}
.account-op:hover{background:var(--ink);color:var(--paper)}

.rail-btn{display:none;background:none;border:0;font-size:1.3rem;color:var(--paper-dim);padding:0 10px;cursor:pointer}
.rail-veil{display:none}

/* ==========================================================
   2 · SCREENS — generic blocks
   ========================================================== */

.view{max-width:1180px;margin:0 auto;display:flex;flex-direction:column;gap:22px}
.view-head{display:flex;flex-direction:column;gap:6px}
.view-head h1{font-family:'Space Grotesk',sans-serif;font-size:clamp(1.4rem,3vw,2rem);line-height:1.15}
.view-head p{color:var(--paper-dim);font-size:.92rem}

.block{
  background:var(--panel);border:1px solid var(--wire);border-radius:6px;
  padding:18px 20px;display:flex;flex-direction:column;gap:14px;
}
.block-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.block-top h2{font-family:'Space Grotesk',sans-serif;font-size:1.02rem}
.block-link{font-family:'IBM Plex Mono',monospace;font-size:.74rem;color:var(--phosphor)}
.block-risk{border-color:rgba(255,77,94,.3)}
.btn-risk{border-color:var(--amber);color:var(--amber)}
.btn-risk:hover{background:rgba(255,77,94,.1)}

.bar{display:block;height:5px;border-radius:3px;background:var(--wire);overflow:hidden}
.bar-fill{display:block;height:100%;background:var(--phosphor);transition:width .3s ease}

.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(226px,1fr));gap:12px}
.card{
  display:flex;flex-direction:column;gap:6px;padding:14px;
  background:var(--ink);border:1px solid var(--wire);border-radius:5px;
  transition:border-color .2s,transform .2s;
}
.card:hover{border-color:var(--phosphor);transform:translateY(-2px)}
.card-name{font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:.94rem;line-height:1.2}
.card-summary{font-size:.82rem;color:var(--paper-dim);line-height:1.45}
.card-cat,.card-meta,.card-count{font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--paper-dim)}
.card-cat{color:var(--phosphor-dim);letter-spacing:.08em;text-transform:uppercase}

/* ---------- pick up where you left off ---------- */
.resume{
  display:flex;flex-direction:column;gap:8px;padding:20px 22px;
  background:var(--panel);border:1px solid var(--phosphor-dim);border-radius:6px;
  transition:border-color .2s;
}
.resume:hover{border-color:var(--phosphor)}
.resume-label{font-family:'IBM Plex Mono',monospace;font-size:.68rem;letter-spacing:.12em;text-transform:uppercase;color:var(--phosphor)}
.resume-lesson{font-family:'Space Grotesk',sans-serif;font-weight:600;font-size:clamp(1.05rem,2.2vw,1.35rem);line-height:1.2}
.resume-course{font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--paper-dim)}
.resume-btn{align-self:flex-start;margin-top:6px}

.track-numbers{display:flex;flex-wrap:wrap;gap:20px}
.track-numbers span{display:flex;flex-direction:column;font-family:'IBM Plex Mono',monospace;font-size:.7rem;color:var(--paper-dim)}
.track-numbers b{font-family:'Space Grotesk',sans-serif;font-size:1.25rem;color:var(--paper)}

/* ==========================================================
   3 · THE GRAPH AS A PROGRESS MAP
   ========================================================== */

/* MORE AIR BETWEEN THE LEVELS. The vitrine uses 48px, and there that is enough:
   the graph is one of the seven screens and disappears after a single scroll.
   Here it is the screen the student comes back to in order to get their
   bearings, and at 48px the edges shave past the cards — they do not collide,
   but the reading gets cramped and the eye does not separate one column from
   the next.

   The trade is horizontal scrolling, and it is cheap: the graph already
   scrolls, already has the arrows and already has the fade at the borders. The
   edge routing is measured over the REAL positions, so it follows along without
   a line of JavaScript — and the smoke test's collision detector confirms that
   it did.

   Here and not in `base.css`, which is a copy of the vitrine and has to stay
   syncable. */
.portal .graph-levels{gap:88px}
.portal .level{gap:26px}
.portal .subcol{gap:20px}

/* `.graph-levels` and `.level` have height:100%, so the box needs a definite
   height — in the vitrine it came from the fullpage section, which does not
   exist here. */
.view-track .graph-box{height:clamp(360px,58vh,640px)}
.view-track .track-top{display:flex;flex-wrap:wrap;gap:18px;justify-content:space-between;align-items:flex-start}
.view-track .track-top p{max-width:58ch;color:var(--paper-dim);font-size:.9rem}
.view-track .track-summary{display:flex;gap:18px;flex-wrap:wrap}
.view-track .track-summary span{display:flex;flex-direction:column;font-family:'IBM Plex Mono',monospace;font-size:.68rem;color:var(--paper-dim)}
.view-track .track-summary b{font-family:'Space Grotesk',sans-serif;font-size:1.15rem;color:var(--paper)}
.view-track .track-summary i{font-style:normal;font-size:.64rem}

/* The four states. `ahead` is the most restrictive and is clickable ANYWAY:
   the vitrine's FAQ promises that the track is a recommended order, not a
   lock. */
.course-node.node-done{border-color:var(--phosphor-dim);background:rgba(91,140,255,.06)}
.course-node.node-current{border-color:var(--amber)}
.course-node.node-ahead{opacity:.62}
.course-node.node-ahead:hover{opacity:1}

.node-state{
  font-family:'IBM Plex Mono',monospace;font-size:.6rem;letter-spacing:.1em;
  text-transform:uppercase;color:var(--paper-dim);
}
.node-state[data-state="done"]{color:var(--phosphor)}
.node-state[data-state="current"]{color:var(--amber)}
.node-state[data-state="available"]{color:var(--phosphor-dim)}

.node-bar{display:block;height:3px;border-radius:2px;background:var(--wire);overflow:hidden;margin-top:5px}
.node-bar-fill{display:block;height:100%;background:var(--phosphor)}
.node-count{font-family:'IBM Plex Mono',monospace;font-size:.6rem;color:var(--paper-dim)}

/* an edge whose prerequisite is already met lights up; the rest stays quiet */
.edge-done .row{stroke:var(--phosphor);opacity:.55}

.graph-legend{display:flex;flex-wrap:wrap;gap:16px}
.leg{display:flex;align-items:center;gap:6px;font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--paper-dim)}
.leg-color{width:11px;height:11px;border-radius:2px;border:1px solid var(--wire);background:var(--ink)}
.leg-color.node-done{border-color:var(--phosphor-dim);background:rgba(91,140,255,.25)}
.leg-color.node-current{border-color:var(--amber)}
.leg-color.node-ahead{opacity:.5}

/* ==========================================================
   4 · COURSE AND LESSON
   ========================================================== */

.course-head{display:flex;flex-direction:column;gap:8px}
.course-head h1{font-family:'Space Grotesk',sans-serif;font-size:clamp(1.4rem,3vw,2rem);line-height:1.15}
.course-summary{color:var(--paper-dim);max-width:70ch}
.course-meta{display:flex;flex-wrap:wrap;gap:14px;font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--phosphor-dim)}
.course-count{font-family:'IBM Plex Mono',monospace;font-size:.7rem;color:var(--paper-dim)}

.course-cols{display:grid;grid-template-columns:minmax(0,1fr);gap:18px}
.course-main,.course-side{display:flex;flex-direction:column;gap:18px}
.course-main{min-width:0}
.syllabus{display:flex;flex-direction:column;gap:7px;font-size:.88rem;color:var(--paper-dim);padding-left:16px}
.syllabus li{list-style:'› ';padding-left:4px}
.prerequisites{font-size:.88rem;color:var(--paper-dim)}
.related{display:flex;flex-wrap:wrap;gap:7px}
.link-chip{
  font-family:'IBM Plex Mono',monospace;font-size:.72rem;padding:5px 9px;border-radius:4px;
  border:1px solid var(--wire);color:var(--paper-dim);
}
.link-before{border-color:rgba(255,77,94,.35);color:var(--amber)}
.link-after{border-color:var(--phosphor-dim);color:var(--phosphor)}
.link-chip:hover{background:var(--ink)}

/* `list-style:none` because the numbering belongs to `.lesson-num`, monospaced
   and zero-padded: letting the <ol> marker show up as well numbered the same
   line twice ("1. 01 Client, server and host"). */
.lessons{display:flex;flex-direction:column;gap:1px;list-style:none;padding:0}
.lesson-row{
  display:grid;grid-template-columns:18px 26px 1fr auto;align-items:center;gap:4px 10px;
  padding:10px 12px;border-radius:4px;color:var(--paper);font-size:.9rem;
}
.lesson-sections{
  grid-column:3;font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--phosphor-dim);
}
.lesson-prog{
  grid-column:4;grid-row:1/3;font-family:'IBM Plex Mono',monospace;
  font-size:.7rem;color:var(--paper-dim);
}
.lesson-row:hover{background:var(--ink)}
.lesson-mark{color:var(--phosphor);font-size:.8rem}
.lesson-num{font-family:'IBM Plex Mono',monospace;font-size:.7rem;color:var(--phosphor-dim)}
.lesson-row.done .lesson-tit{color:var(--paper-dim)}

/* ---------- the lesson, now in sections ---------- */
/* one width for the whole screen, and every child inherits it by being a child
   — none of them declares a width of its own */
.view-lesson{max-width:var(--screen)}
.lesson-head{display:flex;flex-direction:column;gap:12px}

/* The steps are the lesson's spine: where I am among the sections and how much
   is left. They WRAP onto several lines instead of scrolling horizontally —
   unlike the vitrine's rows, which scroll because there height is the scarce
   resource and there are arrows to navigate with. Here there are no arrows, and
   a scrollable row with no affordance hides the last section without warning:
   which is what happened to "How to choose", the fifth in the hosting topic,
   that simply did not show up. */
.steps{display:flex;flex-wrap:wrap;gap:6px}
.step{
  flex:0 0 auto;display:flex;align-items:center;gap:7px;max-width:230px;
  padding:7px 11px;border:1px solid var(--wire);border-radius:4px;
  color:var(--paper-dim);font-size:.8rem;transition:border-color .15s,color .15s;
}
.step:hover{border-color:var(--phosphor-dim);color:var(--paper)}
.step.on{border-color:var(--phosphor);color:var(--phosphor);background:var(--panel)}
.step.done .step-n{color:var(--phosphor)}
.step.done:not(.on) .step-title{color:var(--paper-dim)}
.step-n{font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--phosphor-dim)}
.step-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.step-assessment{border-style:dashed}

.section-title{font-family:'Space Grotesk',sans-serif;font-size:clamp(1.1rem,2.2vw,1.4rem);line-height:1.2}

/* PROSE IS NOT A CARD.

   It used to be: `--panel` background, border, rounded corner — a box inside
   the page. That draws a border between "the content area" and "the rest", and
   the rest is nothing: it is the margin left over from centring. The border
   separates text from emptiness, which is not a distinction worth a line on
   screen.

   A card is for what gets COMPARED side by side — courses, materials, the
   blocks on the course page. Running text meant to be read for half an hour
   wants the opposite: nothing around it. The figures, the code and the video
   still have a frame, and now that frame means something — "this here is
   something other than prose" — which it could not say while it lived inside
   another frame.

   PROSE NO LONGER HAS A CEILING OF ITS OWN — it goes to `--screen`, like
   everything else.

   It used to have `max-width:68ch`, the classic line limit for long reading.
   The effect was that, at `--wide`, the paragraph stopped at 605px while the
   player and the example beside it went to 1074 — the same left margin and
   three right margins, which is the misalignment from the other end.

   The price is real and measured: at 1074px the line reaches 121ch, against the
   comfortable 68. It was a choice between two defects, and alignment won; if
   the long line turns out to hurt reading, the ceiling comes back in `ch` and
   what is lost is the alignment on the right, not the one on the left. */
.lesson-text{
  gap:0;background:none;border:0;border-radius:0;padding:0;
}
.lesson-text p{line-height:1.72;margin-bottom:14px}
.lesson-text p:last-child{margin-bottom:0}
.lesson-text strong{color:var(--paper)}
.lesson-text code{
  font-family:'IBM Plex Mono',monospace;font-size:.86em;
  background:var(--ink);padding:1px 5px;border-radius:3px;color:var(--phosphor);
}
/* A `code` INSIDE A `pre` is not inline code: there it takes no box, no
   padding, no rounded corner and no colour of its own — the block is in charge.

   The rule above had been catching both, and painted an `--ink` rectangle
   behind every line of the example. It stayed invisible for a long time because
   it is almost the colour of the panel; it turned up by accident, in a
   background drawing that was tried and discarded — the rectangle ate half of
   it. The drawing went away, the fix stays: a box behind a line of code was a
   mistake either way. */
.lesson-text pre code{
  background:none;padding:0;border-radius:0;color:inherit;font-size:inherit;
}
/* The prose's code block is the same component the exercises use: a long line
   scrolls inside the box instead of being folded — code does not wrap for
   reading comfort. */
.prose-code{margin:0 0 16px;max-width:100%}
.prose-code .code{font-size:.8rem}
.prose-list{display:flex;flex-direction:column;gap:8px;margin:0 0 14px;padding-left:18px}
.prose-list li{list-style:'› ';padding-left:5px;line-height:1.6;color:var(--paper-dim)}
.prose-list li strong{color:var(--paper)}

.assessment-intro{font-family:'IBM Plex Mono',monospace;font-size:.74rem;color:var(--paper-dim)}
/* The assessment exists in every lesson, but not every one has exercises yet.
   The dashed border says "this is reserved" without pretending there is already
   something to do. */
.step-assessment{border-style:dashed}
.step-pending{opacity:.55}
.step-pending:hover{opacity:1}
.assessment-pending{border-style:dashed}
.foot-note{font-size:.72rem}
.rail-section.pending{opacity:.6}
.rail-section.pending:hover{opacity:1}
.resume-where{font-size:.68rem}
.crumbs{display:flex;align-items:center;gap:8px;font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--paper-dim)}
.crumbs a{color:var(--phosphor)}
.lesson-title{font-family:'Space Grotesk',sans-serif;font-size:clamp(1.3rem,2.8vw,1.8rem);line-height:1.18}

/* TWO SHAPES OF SECTION, AND THE LAYOUT FOLLOWS THE SHAPE.

   A section that HAS a video shows the player right after the header, at the
   lesson's width. One that does NOT have a frame at all — not grey, not
   reserved: an empty rectangle promises a video that never comes, and the
   promise does not expire. See the comment in screens/lesson.js.

   The player takes the whole width because that is what EVERY child of the
   screen does now; what it has of its own is a height ceiling, and that becomes
   a WIDTH ceiling. A player cannot push the rest of the section below the fold
   — but capping the height of an `aspect-ratio` box does not shrink the box: it
   flattens it, and the video inside stretches. By limiting the width, the 16/9
   ratio derives the height, and in a short window the player ends up smaller
   than the rest of the lesson instead of deformed. */
.lesson-with-video .modal-video{
  --video-alt:min(64vh, calc(100vh - 300px));
  width:min(100%, calc(var(--video-alt) * 16 / 9));
}
/* The duration in the corner of the player, as everywhere else that has video:
   it answers "is there time right now?", which is the question one asks before
   hitting play. It is the lesson's own — a course introduction has no duration
   to show — so it stays here rather than moving into base.css with the frame. */
.video-duration{
  position:absolute;right:12px;bottom:12px;z-index:1;
  font-size:.68rem;letter-spacing:.06em;
  background:rgba(0,0,0,.62);color:#fff;border-radius:3px;padding:3px 7px;
}
/* The portal's play button is a little larger than the vitrine's, and this
   override is older than the shared frame. It stays because it is what the
   course screen already looked like — deleting it while unifying the two
   components would have restyled the screen that was being copied FROM, which
   is the one change nobody asked for. It measured 60px before and after. */
.video-play{
  width:60px;height:60px;border-radius:50%;background:var(--phosphor);color:var(--ink);
  font-size:1.3rem;display:grid;place-items:center;
}

.lesson-text{min-height:0}
/* Without the box, the text needs breathing room of its own on top — that space
   used to be the card's `padding`. */
.lesson-text>*:first-child{margin-top:6px}
.account-ex{font-family:'IBM Plex Mono',monospace;font-size:.74rem;color:var(--phosphor)}
.ex-list{display:flex;flex-direction:column;gap:16px}

.lesson-foot{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between;align-items:center;padding-top:8px;border-top:1px solid var(--wire)}
/* THE NAVIGATION ARROWS MOVE TO THE SIDES on a wide screen. The lesson is
   pinned to one column, so there is space to spare on the sides and space
   missing vertically — and every pixel spent below is text pushed past the
   fold. */
.side-arrow{display:none}

/* THE ARROWS LIVE IN THE SCREEN'S LEFTOVER, not glued to the text.

   The lesson is at most `--screen` wide and is centred in the area the rail
   leaves. What remains on both sides — the "gap" — is space no content uses,
   and it is exactly where a navigation control belongs: close enough to be
   reached, far enough not to fight for the line with the word being read.

     gap on each side = (100vw - rail - view) / 2
     left arrow   → centred in the gap between the rail and the column
     right arrow  → centred in the gap between the column and the screen edge

   It is `--screen`, and not `--reading`: when the lesson widens the gap shrinks,
   and an arrow positioned by the narrow column would sit on top of the wide
   one's content.

   THE CUT IS 1400px, and the number comes out of the arithmetic: with the 380
   rail and the 820 narrow column, the gap on each side is (1400-380-820)/2 =
   100px — enough for a 44px arrow with slack on both sides. Below that there is
   no gap, and the footer is the right place again. It never stopped existing. */
@media(min-width:1400px){
  .side-arrow{
    display:grid;place-items:center;position:fixed;top:50%;
    width:44px;height:44px;border-radius:50%;z-index:60;
    background:var(--panel);border:1px solid var(--wire);color:var(--paper-dim);
    transition:color .15s,border-color .15s,background .15s;
  }
  .side-arrow:hover{color:var(--phosphor);border-color:var(--phosphor);background:var(--ink)}
  .side-arrow svg{width:20px;height:20px}
  .side-left{
    left:calc(var(--rail) + (100vw - var(--rail) - var(--screen)) / 4);
    transform:translate(-50%,-50%);
  }
  .side-right{
    right:calc((100vw - var(--rail) - var(--screen)) / 4);
    transform:translate(50%,-50%);
  }
  /* with the arrows at the sides the whole footer goes away: it only repeated
     the same two routes, and repeating costs the height the arrows went there
     to save */
  .lesson-foot{display:none}

}

/* ---------- the assessment wizard ---------- */
.wizard{display:flex;flex-direction:column;gap:14px}
.wz-top{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px}
.wz-count{
  font-family:'IBM Plex Mono',monospace;font-size:.68rem;letter-spacing:.1em;
  text-transform:uppercase;color:var(--phosphor-dim);
}
.wz-dots{display:flex;flex-wrap:wrap;gap:5px}
.wz-dot{
  width:26px;height:26px;border-radius:50%;cursor:pointer;
  background:none;border:1px solid var(--wire);color:var(--paper-dim);
  font-family:'IBM Plex Mono',monospace;font-size:.68rem;
  transition:border-color .15s,color .15s;
}
.wz-dot:hover{border-color:var(--phosphor-dim);color:var(--paper)}
.wz-dot.on{border-color:var(--phosphor);color:var(--phosphor)}
.wz-dot.right{background:rgba(91,140,255,.16);border-color:var(--phosphor-dim);color:var(--phosphor)}
.wz-dot.wrong{background:rgba(255,77,94,.14);border-color:rgba(255,77,94,.5);color:var(--amber)}
.wz-dot.pending{border-style:dashed}
.wz-foot{display:flex;gap:8px;justify-content:space-between}
.wz-foot .btn:disabled{opacity:.4;cursor:default}

.wz-result{
  display:flex;flex-direction:column;gap:10px;align-items:flex-start;
  padding:22px;border:1px solid var(--wire);border-radius:6px;background:var(--ink);
}
.wz-res-label{
  font-family:'IBM Plex Mono',monospace;font-size:.66rem;letter-spacing:.12em;
  text-transform:uppercase;color:var(--phosphor);
}
.wz-res-score{font-family:'Space Grotesk',sans-serif;font-size:1.6rem}
.wz-res-score strong{color:var(--phosphor)}
.wz-res-note{font-family:'IBM Plex Mono',monospace;font-size:.74rem;color:var(--paper-dim)}

/* ==========================================================
   5 · EXERCISES — the seven types
   ========================================================== */

.ex{
  background:var(--ink);border:1px solid var(--wire);border-radius:6px;
  padding:16px 18px;display:flex;flex-direction:column;gap:12px;
}
.ex-top{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.ex-type,.ex-difficulty{
  font-family:'IBM Plex Mono',monospace;font-size:.62rem;letter-spacing:.08em;
  text-transform:uppercase;padding:3px 7px;border-radius:3px;border:1px solid var(--wire);
}
.ex-type{color:var(--phosphor);border-color:var(--phosphor-dim)}
.ex-difficulty{color:var(--paper-dim)}

.ex-prompt{font-size:.95rem;line-height:1.6}
.ex-instruction{font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--paper-dim)}
.ex-label{font-family:'IBM Plex Mono',monospace;font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;color:var(--phosphor-dim)}
.ex-note{font-family:'IBM Plex Mono',monospace;font-size:.68rem;color:var(--paper-dim)}
.ex-note-domain{color:var(--phosphor-dim)}
.ex-note-warn{color:var(--amber)}
.ex-error{color:var(--amber);font-family:'IBM Plex Mono',monospace;font-size:.8rem}
.ex code{font-family:'IBM Plex Mono',monospace;font-size:.86em;background:var(--panel);padding:1px 5px;border-radius:3px}

.ex-body{display:flex;flex-direction:column;gap:10px}
.ex-field{
  width:100%;background:var(--panel);border:1px solid var(--wire);border-radius:4px;
  padding:10px 12px;color:var(--paper);font:inherit;resize:vertical;
}
.ex-field.mono{font-family:'IBM Plex Mono',monospace;font-size:.84rem;line-height:1.5}
.ex-field:focus{outline:none;border-color:var(--phosphor)}
.ex-field.field-right{border-color:var(--phosphor)}
.ex-field.field-wrong{border-color:var(--amber)}

.ex-hint{border-top:1px dashed var(--wire);padding-top:10px}
.ex-hint summary{cursor:pointer;font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--phosphor)}
.ex-hint p{margin-top:8px;font-size:.88rem;color:var(--paper-dim);line-height:1.55}

.ex-actions{display:flex;gap:8px;flex-wrap:wrap}
.ex-verdict:empty{display:none}
.ex-verdict{font-family:'IBM Plex Mono',monospace;font-size:.78rem;padding:9px 11px;border-radius:4px;border:1px solid var(--wire)}
.v-right{color:var(--phosphor);border-color:var(--phosphor-dim);background:rgba(91,140,255,.07)}
.v-wrong{color:var(--amber);border-color:rgba(255,77,94,.35)}
.v-pending,.v-empty,.v-waiting,.v-old{color:var(--paper-dim)}
.v-trap{display:block;margin-top:8px;color:var(--paper-dim);font-family:'IBM Plex Sans',sans-serif;font-size:.85rem;line-height:1.5}

/* ---------- choices (quiz and multiple choice) ---------- */
.choices{display:flex;flex-direction:column;gap:7px}
.choice{
  display:grid;grid-template-columns:16px 1fr;align-items:start;gap:10px;
  padding:11px 13px;border:1px solid var(--wire);border-radius:5px;cursor:pointer;
  transition:border-color .15s,background .15s;
}
.choice:hover{border-color:var(--phosphor-dim)}
.choice input{grid-row:1;-webkit-appearance:none;appearance:none;width:0;height:0;position:absolute;opacity:0}
.choice-mark{grid-row:1;width:15px;height:15px;margin-top:3px;border:1px solid var(--wire);border-radius:50%;position:relative}
.ex-multiple-choice .choice-mark{border-radius:3px}
.choice input:checked ~ .choice-mark{border-color:var(--phosphor)}
.choice input:checked ~ .choice-mark::after{
  content:'';position:absolute;inset:3px;border-radius:inherit;background:var(--phosphor);
}
.choice-text{grid-column:2;font-size:.9rem;line-height:1.5}
.choice-why{
  grid-column:2;font-size:.82rem;color:var(--paper-dim);line-height:1.5;
  padding-top:7px;margin-top:5px;border-top:1px dashed var(--wire);
}
.choice-right{border-color:var(--phosphor);background:rgba(91,140,255,.07)}
.choice-wrong{border-color:var(--amber);background:rgba(255,77,94,.06)}
.choice-missed{border-style:dashed}

/* ---------- ordering ---------- */
.ord{display:flex;flex-direction:column;gap:6px;counter-reset:step;list-style:none}
.ord-item{
  display:grid;grid-template-columns:16px 1fr auto;align-items:center;gap:10px;
  padding:10px 12px;border:1px solid var(--wire);border-radius:5px;background:var(--panel);
  cursor:grab;
}
.ord-item.dragging{opacity:.45;border-style:dashed}
.ord-item::before{counter-increment:step}
.ord-grip{color:var(--phosphor-dim);font-size:.9rem}
.ord-text{font-size:.88rem;line-height:1.45}
.ord-arrows{display:flex;flex-direction:column;gap:2px}
.ord-arrow{
  width:24px;height:18px;display:grid;place-items:center;border-radius:3px;
  background:none;border:1px solid var(--wire);color:var(--paper-dim);cursor:pointer;
}
.ord-arrow svg{width:13px;height:13px}
.ord-grip svg{width:14px;height:14px;display:block}
.ord-arrow:hover:not(:disabled){border-color:var(--phosphor);color:var(--phosphor)}
.ord-arrow:disabled{opacity:.35}
.ord-right{border-color:var(--phosphor)}
.ord-wrong{border-color:var(--amber)}

/* ---------- matching: tiles, in place of the selects ---------- */
.matching-cols{display:grid;grid-template-columns:1fr 1fr;gap:10px;align-items:start}
.matching-col{display:flex;flex-direction:column;gap:7px}
.tile{
  text-align:left;padding:10px 12px;border-radius:5px;cursor:pointer;
  background:var(--panel);border:1px solid var(--wire);color:var(--paper);
  font:inherit;font-size:.86rem;line-height:1.35;
  transition:border-color .15s,background .15s,transform .12s;
}
.tile:hover:not(:disabled){border-color:var(--phosphor-dim)}
.tile.sel{border-color:var(--phosphor);background:rgba(91,140,255,.1)}
.tile-right{border-color:var(--phosphor);background:rgba(91,140,255,.08);opacity:.65;cursor:default}
.tile-wrong{border-color:var(--amber);background:rgba(255,77,94,.1)}
.tile.shaking{animation:shake .3s}
@keyframes shake{25%{transform:translateX(-3px)}75%{transform:translateX(3px)}}
.matching-count{font-family:'IBM Plex Mono',monospace;font-size:.7rem;color:var(--paper-dim)}
.matching-errors{font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--amber);margin-top:8px}

/* ---------- matching, in an EXAM ----------

   A pair here is CHOSEN, not confirmed: nothing has been checked and nothing
   will be until the exam closes. So the styling has to say "this is where you
   put it" without saying "this is right" — a paired tile keeps the neutral
   border and gains the chosen text under it, which is also the only way to see
   your own answer without breaking the pair to look. */
.tile-paired{border-color:var(--phosphor-dim);background:rgba(91,140,255,.05)}
.tile-taken{opacity:.4;cursor:default}
.tile-chosen{
  display:block;margin-top:5px;font-family:'IBM Plex Mono',monospace;
  font-size:.72rem;color:var(--paper-dim);
}
/* After the exam: the pairing arrives with the verdict and the wrong ones say
   what the right answer was, since this is the first time anybody sees it. */
.tile-chosen.tile-answer{color:var(--phosphor)}

/* ---------- code and expected output ---------- */
.code-block{border:1px solid var(--wire);border-radius:5px;overflow:hidden;background:var(--panel)}
/* The bar became a row: the language on the left, the copy button on the right.
   It is `min-height` and not a fixed height because the language may be absent —
   the bar still has to hold the button at the same size either way. */
.code-bar{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:3px 5px 3px 11px;min-height:28px;
  border-bottom:1px solid var(--wire);background:var(--ink);
}
.code-lang{font-family:'IBM Plex Mono',monospace;font-size:.64rem;letter-spacing:.1em;text-transform:uppercase;color:var(--phosphor-dim)}

/* THE COPY BUTTON IS QUIET UNTIL IT IS WANTED. It sits at ~55% opacity and
   comes up to full on hover or focus anywhere in the block — present enough to
   be found, faint enough not to compete with the first line of code, which is
   what the eye goes to. It never hides completely: a control that appears only
   on hover does not exist on a touch screen, and the phone is where copying a
   snippet is most annoying to do by hand. */
.code-copy{
  flex:0 0 auto;display:grid;place-items:center;width:26px;height:26px;
  background:none;border:0;border-radius:4px;padding:0;cursor:pointer;
  color:var(--paper-dim);opacity:.55;
  transition:opacity .15s,color .15s,background .15s;
}
.code-copy svg{width:15px;height:15px}
.code-block:hover .code-copy,.example:hover .code-copy,.code-copy:focus-visible{opacity:1}
.code-copy:hover{color:var(--phosphor);background:var(--panel)}
.code-copy:focus-visible{outline:2px solid var(--phosphor);outline-offset:1px}
/* Copied is the brand's green; failed is the amber the portal already uses for
   "this did not go well". Both hold for a moment and then let go — see copy.js */
.code-copy.copied{opacity:1;color:var(--phosphor)}
.code-copy.failed{opacity:1;color:var(--amber)}
.code{
  padding:12px;overflow-x:auto;font-family:'IBM Plex Mono',monospace;font-size:.82rem;line-height:1.55;
  scrollbar-width:thin;scrollbar-color:var(--wire) transparent;
}
.code-editor .ex-field{border:0;border-radius:0;background:var(--panel)}
.code-area{
  min-height:180px;white-space:pre;overflow-wrap:normal;overflow-x:auto;
  scrollbar-width:thin;scrollbar-color:var(--wire) transparent;
}

.cases{display:flex;flex-direction:column;gap:10px;padding-top:4px}
.cases-title{font-family:'IBM Plex Mono',monospace;font-size:.66rem;letter-spacing:.1em;text-transform:uppercase;color:var(--phosphor-dim)}
.case{border:1px solid var(--wire);border-radius:5px;padding:10px 12px;display:flex;flex-direction:column;gap:7px}
.case-desc{font-size:.82rem;color:var(--paper-dim)}
.case-io{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px}
.case-label{font-family:'IBM Plex Mono',monospace;font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;color:var(--phosphor-dim)}
.case .code{padding:8px;background:var(--panel);border-radius:4px;margin-top:4px}

/* ---------- expression answer ---------- */
.expr-origin{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.expr-label{font-family:'IBM Plex Mono',monospace;font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;color:var(--phosphor-dim)}
.expr-code{font-family:'IBM Plex Mono',monospace;font-size:.95rem;color:var(--phosphor);background:var(--panel);padding:5px 10px;border-radius:4px}

/* ==========================================================
   6 · SEARCH, PERFORMANCE AND NOTES
   ========================================================== */

/* ---------- the ⌘K panel ---------- */
.search-veil{
  position:fixed;inset:0;z-index:200;
  background:rgba(10,14,20,.72);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
  display:flex;justify-content:center;align-items:flex-start;
  /* high, but not flush: the box appears where the eye already is after the
     shortcut */
  padding:12vh 20px 20px;
}
body.with-search{overflow:hidden}
.search-box{
  width:min(680px,100%);max-height:76vh;display:flex;flex-direction:column;
  background:var(--panel);border:1px solid var(--wire);border-radius:8px;overflow:hidden;
  box-shadow:0 24px 60px -20px rgba(0,0,0,.6);
}
.search-top{display:flex;align-items:center;gap:11px;padding:14px 16px;border-bottom:1px solid var(--wire)}
.search-glass{display:grid;place-items:center;color:var(--phosphor-dim)}
.search-glass svg{width:18px;height:18px}
.search-field{
  flex:1 1 auto;min-width:0;background:none;border:0;color:var(--paper);
  font:inherit;font-size:1rem;outline:none;
  /* Safari draws a native rounded box for type=search otherwise */
  -webkit-appearance:none;appearance:none;
}
.search-field::-webkit-search-cancel-button{display:none}
.search-res{
  overflow-y:auto;padding:6px;
  scrollbar-width:thin;scrollbar-color:var(--wire) transparent;
}
/* The group label in RED, and not in the blue of everything else: it is the
   only thing in the list that is not a result — it is the divider between them.
   In blue it blended with each item's context, which is also blue and also
   monospaced, and the list became a single column. */
.search-group{
  padding:12px 12px 5px;font-family:'IBM Plex Mono',monospace;font-size:.62rem;
  letter-spacing:.14em;text-transform:uppercase;color:var(--amber);
}
.search-item{
  display:flex;flex-direction:column;gap:3px;width:100%;text-align:left;
  padding:9px 12px;border-radius:5px;background:none;border:0;cursor:pointer;font:inherit;
}
.search-item.on{background:var(--ink);box-shadow:inset 2px 0 0 var(--phosphor)}
.bi-tit{
  color:var(--paper);font-size:.9rem;line-height:1.3;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
}
.bi-sub{font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--phosphor-dim)}
.bi-ctx{
  font-size:.78rem;color:var(--paper-dim);line-height:1.4;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.search-hint{padding:26px 14px;text-align:center;font-family:'IBM Plex Mono',monospace;font-size:.8rem;color:var(--paper-dim)}
.search-foot{
  display:flex;gap:16px;padding:9px 16px;border-top:1px solid var(--wire);
  font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--paper-dim);
}
.search-foot kbd{
  display:inline-block;padding:1px 5px;margin-right:3px;border-radius:3px;
  border:1px solid var(--wire);background:var(--ink);font-family:inherit;font-size:.9em;
}

/* ---------- performance ---------- */
/* The label has a label's width; what grows is the bar, which is the data. It
   used to be the other way round, and the result was a name adrift on the left
   and a short dash half a metre away from it, on the other side of the
   screen. */
.perf-row{display:grid;grid-template-columns:minmax(0,210px) minmax(0,1fr) auto;align-items:center;gap:14px}
.perf-label{font-size:.86rem;color:var(--paper);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.perf-num{font-family:'IBM Plex Mono',monospace;font-size:.72rem;color:var(--paper-dim)}
.perf-wrong{display:flex;flex-direction:column;gap:1px;list-style:none;padding:0}
.perf-wrong a{display:flex;flex-direction:column;gap:3px;padding:10px 12px;border-radius:4px}
.perf-wrong a:hover{background:var(--ink)}
.de-type{
  font-family:'IBM Plex Mono',monospace;font-size:.6rem;letter-spacing:.1em;
  text-transform:uppercase;color:var(--amber);
}
.de-prompt{font-size:.88rem;line-height:1.4;color:var(--paper)}
.de-where{font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--phosphor-dim)}

/* ---------- notes ---------- */
.note{border-top:1px dashed var(--wire);padding-top:12px;margin-top:2px}
.note summary{
  cursor:pointer;font-family:'IBM Plex Mono',monospace;font-size:.74rem;color:var(--phosphor);
  list-style:none;
}
.note summary::-webkit-details-marker{display:none}
.note-field{
  width:100%;margin-top:10px;padding:11px 13px;border-radius:5px;resize:vertical;
  background:var(--panel);border:1px solid var(--wire);color:var(--paper);
  font:inherit;font-size:.9rem;line-height:1.6;
}
.note-field:focus{outline:none;border-color:var(--phosphor)}
.note-status{display:block;margin-top:6px;font-size:.68rem;min-height:1em}

.note-item{display:flex;flex-direction:column;gap:5px;padding:12px 0;border-top:1px solid var(--wire)}
.note-item:first-of-type{border-top:0}
.note-where{font-family:'IBM Plex Mono',monospace;font-size:.68rem;color:var(--phosphor)}
.note-text{font-size:.9rem;line-height:1.6;color:var(--paper-dim);white-space:pre-wrap}

/* ==========================================================
   7 · SIGN IN, CERTIFICATES, ACCOUNT
   ========================================================== */

.view-signin{min-height:calc(100vh - 130px);display:grid;place-items:center;max-width:none}
.signin-box{width:min(420px,100%);border:1px solid var(--wire);border-radius:6px;background:var(--panel);overflow:hidden}
.signin-body{padding:22px 24px 26px;display:flex;flex-direction:column;gap:14px}
.signin-body h1{font-family:'Space Grotesk',sans-serif;font-size:1.3rem}
.signin-sub{font-size:.88rem;color:var(--paper-dim)}
.signin-body form{display:flex;flex-direction:column;gap:14px}
.signin-notice{font-size:.68rem}

/* ---------- the certificate ----------

   A document, not a window. See the comment in screens/certificates.js: it is
   the only artefact of the portal that leaves here, and it has to look like a
   document wherever it turns up.

   The frame is double — the card's border and an inner rule, which is what
   gives the "printed" reading. `--phosphor` stays only on the rule and on the
   brand LED: a document with too much colour looks like an advert. */
.certs{display:grid;grid-template-columns:repeat(auto-fill,minmax(430px,1fr));gap:18px}
.cert{
  border:1px solid var(--wire);border-radius:4px;background:var(--panel);
  padding:9px;transition:border-color .2s;
}
.cert:hover{border-color:var(--phosphor-dim)}
.cert-sheet{
  border:1px solid var(--wire);border-radius:2px;
  padding:26px 30px 16px;
  display:flex;flex-direction:column;gap:22px;min-height:250px;
  --cert-grain:none;
  /* The paper's own grain, over the corner glow that suggests paper under
     light. A touch of texture so the document does not read as a flat white
     sheet, in the light theme only — on the dark one a black band is invisible
     and the glow already does the work. The PNG export draws the same band, so
     the file matches the card.

     A RASTER TILE, not a repeating-gradient. As a gradient the grain
     re-rasterised a frame out of phase on any partial repaint of this sheet —
     a button clicked, focus moving, the modal's backdrop-filter recompositing —
     and the phase slip flashed a horizontal line that vanished. A 1×4 tile
     (three clear rows, one at ~.02 black) re-blits at a fixed origin instead,
     so a repaint cannot shift it. One knob: --cert-grain. */
  background:
    var(--cert-grain),
    radial-gradient(120% 90% at 100% 0%, rgba(126,231,135,.05), transparent 55%),
    radial-gradient(90% 80% at 0% 100%, rgba(126,231,135,.035), transparent 60%);
}
html[data-theme="light"] .cert-sheet{
  --cert-grain:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAECAYAAABP2FU6AAAADElEQVR42mNgwACsAAAZAAYNVTSFAAAAAElFTkSuQmCC");
}
.cert-top{display:flex;align-items:center;justify-content:space-between;gap:14px}
.cert-brand{
  display:flex;align-items:center;gap:8px;
  font-family:'Space Grotesk',sans-serif;font-size:.82rem;letter-spacing:.06em;
  text-transform:uppercase;color:var(--paper);
}
/* the negative indent eats the `letter-spacing` left over after the "l" and
   glues the ".ing" to the brand — without it one reads "CODESCHOOL .ING" */
.cert-brand b{font-weight:400;color:var(--phosphor);margin-left:-.06em}
.cert-led{
  width:7px;height:7px;border-radius:50%;background:var(--phosphor);
  box-shadow:0 0 8px var(--phosphor);
}
.cert-type,.cert-seal{
  font-family:'IBM Plex Mono',monospace;font-size:.58rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--paper-dim);
  border:1px solid var(--wire);border-radius:3px;padding:3px 8px;
}

.cert-body{display:flex;flex-direction:column;gap:6px;flex:1;justify-content:center}
.cert-line{
  font-family:'IBM Plex Mono',monospace;font-size:.68rem;letter-spacing:.1em;
  color:var(--paper-dim);
}
/* The name of whoever receives it is the largest element of the document. It is
   what that person looks for first on opening it, and what the other person
   reads on receiving it. */
.cert-student{
  font-family:'Space Grotesk',sans-serif;font-size:clamp(1.4rem,2.6vw,1.85rem);
  line-height:1.15;color:var(--paper);margin:2px 0 12px;
}
.cert-course{font-family:'Space Grotesk',sans-serif;font-size:1.06rem;line-height:1.3;color:var(--paper)}
.cert-meta{font-family:'IBM Plex Mono',monospace;font-size:.7rem;color:var(--phosphor-dim);margin-top:4px}

.cert-foot{
  display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:10px;
  padding-top:12px;border-top:1px solid var(--wire);
  font-family:'IBM Plex Mono',monospace;font-size:.66rem;color:var(--paper-dim);
}
.cert-code{letter-spacing:.1em;color:var(--phosphor-dim)}

/* The sample has to be recognisable as a sample WITHOUT being read: dashed
   frame, muted colour and a seal in place of the type. A fake certificate that
   looks like a real one is a problem — not a preview. */
.cert-sample{opacity:.66}
.cert-sample .cert-sheet{border-style:dashed;background:none}
.cert-sample .cert-led{box-shadow:none;background:var(--wire)}
.cert-sample .cert-brand b{color:var(--paper-dim)}

/* A REVOKED certificate is shown and not hidden: the public page already
   answers "revoked" to whoever opens the link, and dropping the card would
   leave the holder the last to know. Marked in the same three ways the sample
   is — frame, colour and seal — for the same reason: it must not be mistakable
   for a document that stands, without being read. */
.cert-void{opacity:.7}
.cert-void .cert-sheet{border-color:var(--amber)}
.cert-void .cert-led{box-shadow:none;background:var(--amber)}
.cert-void .cert-code{color:var(--paper-dim);text-decoration:line-through}
.cert-seal-void{color:var(--amber);border-color:var(--amber)}

/* The screen knows nothing rather than knowing there is nothing. Said out loud,
   because an empty list with no explanation reads as "you have none". */
.cert-offline{
  font-family:'IBM Plex Mono',monospace;font-size:.78rem;color:var(--amber);
  border:1px solid var(--amber);border-radius:3px;padding:10px 12px;margin:0 0 18px;
}

/* ---------- the enlarged certificate ----------
   It is the only artefact of the portal a person wants to LOOK AT rather than
   use. That is why the click opens it big, with the rest of the screen dimmed
   and frozen — the vitrine's own modal, with a strip of actions above the
   box. */
.cert{cursor:zoom-in}
.cert:focus-visible{outline:2px solid var(--phosphor);outline-offset:3px}

/* THE MODAL'S GRID NEEDS A DEFINITE COLUMN.

   base.css's `.modal` is `display:grid;place-items:center`, and an `auto` track
   sizes itself by the max-content of what is inside. The child's `100%` then
   resolves against THAT width — the one it asked for itself — and limits
   nothing: on the phone the certificate came out 408px wide inside a 390px
   screen and was clipped on both sides.

   `minmax(0,1fr)` makes the track definite, and `100%` goes back to meaning
   "the cell". The vitrine never saw this because its content is text, which
   shrinks; the certificate has a minimum width of its own. */
.modal{grid-template-columns:minmax(0,1fr);align-items:start;overflow-y:auto}
.modal-stack{
  display:flex;flex-direction:column;gap:14px;
  width:min(1040px,100%);margin:auto 0;
}
.modal-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:10px}
.modal-cert .modal-box{width:100%;background:none;border:0;box-shadow:none;overflow:visible}
/* inside the modal the certificate is the whole content: no zoom cursor, no
   hover, and with a larger body — that is what it was enlarged for */
.modal-cert .cert{cursor:default;padding:14px}
/* "no hover" for real, not only in the cursor. The class-level .cert:hover
   still animated the border here, and on this enlarged sheet that 200ms
   border-colour transition repainted the paper on every pass of the cursor —
   during which the grain's repeating-gradient re-rasterised a frame out of
   phase, the horizontal line that flickered and vanished. The open document is
   not a target; its border stays put. */
.modal-cert .cert:hover{border-color:var(--wire)}
.modal-cert .cert-sheet{min-height:min(62vh,480px);padding:46px 56px 30px;gap:30px}
.modal-cert .cert-brand{font-size:1rem}
/* `5vw` on the font + `min-height` in `vh` make the document grow along with
   the screen; on the phone the two pushed the certificate out of it. Here the
   whole enlargement goes back to the size of the list card — enlarging on a
   device where it already takes the full width has nothing to enlarge. */
.modal-cert .cert-student{font-size:clamp(2.1rem,5vw,3.4rem)}
.modal-cert .cert-course{font-size:1.6rem}
.modal-cert .cert-line{font-size:.78rem}
.modal-cert .cert-meta,.modal-cert .cert-foot{font-size:.8rem}

@media(max-width:640px){
  .modal{padding:12px}
  .modal-cert .cert{padding:8px}
  .modal-cert .cert-sheet{min-height:0;padding:20px 18px 14px;gap:16px}
  .modal-cert .cert-brand{font-size:.78rem}
  .modal-cert .cert-student{font-size:1.5rem}
  .modal-cert .cert-course{font-size:1rem}
  .modal-cert .cert-line{font-size:.66rem}
  .modal-cert .cert-meta,.modal-cert .cert-foot{font-size:.66rem}
  /* the footer line stacks instead of squeezing date and code onto one line */
  .cert-foot{flex-direction:column;gap:4px}
}
/* THE LINKEDIN BUTTONS ARE ONLY THE ICON, in their brand colour.

   The official blue is #0A66C2, and that is what the light theme uses. On dark
   it gives 2.5:1 of contrast against the panel — below the 3:1 an icon needs to
   be distinguishable — so the dark theme lightens it. Lightening a brand's
   colour is not a liberty: it is what makes it visible, and invisible it
   represents no brand at all. */
.cert-in{
  --linkedin:#3b8fd9;
  display:grid;place-items:center;width:36px;height:36px;border-radius:5px;
  color:var(--linkedin);border:1px solid transparent;
  /* No native chrome: the download control is a <button> and the two LinkedIn
     ones are <a>, so without this the button alone wore the browser's grey box
     while the anchors did not. All three are just an icon now, boxed only on
     hover. */
  background:none;padding:0;font:inherit;-webkit-appearance:none;appearance:none;cursor:pointer;
  transition:background .15s,border-color .15s;
}
html[data-theme="light"] .cert-in{--linkedin:#0a66c2}
.cert-in svg{width:21px;height:21px}
/* The download is not a LinkedIn action, so it keeps a neutral ink rather than
   the brand blue the class carries for the two beside it. The action bar sits on
   the modal's dark veil in BOTH themes, though, so the light theme cannot use
   its own (dark) --paper here — it would vanish against the veil, visible only
   on hover. Dark theme's --paper is already a light ink, so only light overrides
   to one that reads on a near-black ground. */
.cert-png{color:var(--paper)}
html[data-theme="light"] .cert-png{color:#b3bccb}
.cert-in:hover{background:var(--panel);border-color:var(--linkedin)}
/* disabled, and looking it: the button exists to say what will be there */
.cert-in-off{opacity:.35;cursor:not-allowed}
.cert-in-off:hover{background:none;border-color:transparent}

.cert-preview{margin-top:26px;display:flex;flex-direction:column;gap:12px}
/* the label travels next to the title, and not at the other end of the screen:
   it explains the title, and an explanation a metre away explains nothing */
.cert-preview .block-top{align-items:baseline;justify-content:flex-start;gap:14px}

.cert-missing{list-style:none;padding:0;display:flex;flex-direction:column;gap:10px}
.cert-missing li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;font-size:.9rem}

.account-note{font-size:.84rem;color:var(--paper-dim);line-height:1.5}
/* The build, at the bottom of the last screen. It is not for the student, so
   it is the quietest thing on the page and centred rather than in the flow of
   the sections above. */
.account-version{margin-top:6px;text-align:center;font-size:.66rem;letter-spacing:.06em}
.account-version a{color:inherit;transition:color .2s}
.account-version a:hover{color:var(--phosphor)}
/* `.block` is a flex column, and a flex column stretches its children: a loose
   button inside it became an edge-to-edge strip. A button has the width of its
   label. */
.view-account .block>.btn{align-self:flex-start}
.account-action{display:flex;flex-wrap:wrap;align-items:center;gap:12px}
.account-notice{font-size:.72rem}
.account-notice.good{color:var(--phosphor)}
.account-notice.bad{color:var(--amber)}
.view-account form{display:flex;flex-direction:column;gap:14px}

/* Two-factor: the setup key and the recovery codes are both things to read off
   the screen and type or copy, so both select whole on a click. */
.mfa-key{letter-spacing:.12em;-webkit-user-select:all;user-select:all}
.mfa-codes{list-style:none;padding:0;margin:8px 0 16px;display:grid;
  grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px}
.mfa-codes li{background:var(--panel);border:1px solid var(--wire);border-radius:4px;
  padding:7px 11px;letter-spacing:.08em;-webkit-user-select:all;user-select:all}
/* The password meter sits FLUSH with the field, not at the foot of the form: it
   comments on that field, and a comment far from what it comments on becomes
   decoration. */
.password-meter{display:flex;align-items:center;gap:10px;margin-top:7px}
.password-meter .bar{flex:1;max-width:220px}
.password-label{font-size:.66rem;min-width:8ch}

/* ---------- My plan ---------- */
.pl-current-top{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:18px}
.pl-label{font-size:.6rem;letter-spacing:.16em;text-transform:uppercase;color:var(--phosphor)}
.pl-current-top h2{font-family:'Space Grotesk',sans-serif;font-size:1.5rem;margin-top:4px}
.pl-summary{font-size:.9rem;color:var(--paper-dim);margin-top:5px;max-width:52ch}
.pl-price{
  font-family:'Space Grotesk',sans-serif;font-size:1.7rem;color:var(--phosphor);
  display:flex;flex-direction:column;align-items:flex-end;line-height:1.1;
}
.pl-cycle{font-family:'IBM Plex Mono',monospace;font-size:.64rem;color:var(--paper-dim);margin-top:4px}
.pl-facts{display:flex;flex-wrap:wrap;gap:6px 20px;font-size:.7rem}

/* The table scrolls inside its own box on narrow screens — the page never
   scrolls horizontally, which is the rule for the whole portal. */
.pl-table-scroll{
  overflow-x:auto;margin:0 -4px;padding:0 4px;
  scrollbar-width:thin;scrollbar-color:var(--wire) transparent;
}
.pl-table{width:100%;border-collapse:collapse;min-width:520px}
.pl-table th,.pl-table td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--wire)}
.pl-table thead th{
  font-family:'Space Grotesk',sans-serif;font-size:.92rem;font-weight:400;color:var(--paper);
  text-align:center;white-space:nowrap;
}
.pl-table thead th:first-child{text-align:left}
.pl-table tbody th{font-weight:400;font-size:.86rem;color:var(--paper-dim);line-height:1.4}
.pl-table td{text-align:center;width:152px}
.pl-table td svg{width:16px;height:16px}
.pl-table td.yes{color:var(--phosphor)}
.pl-table td.no{color:var(--wire)}
/* the subscribed plan's column gets a continuous background: it is what lets
   the "what do I have" reading be vertical, without hunting for the header on
   every row */
.pl-table th.on,.pl-table td.on{background:var(--ink)}
.pl-yours{
  display:block;font-size:.54rem;letter-spacing:.16em;text-transform:uppercase;
  color:var(--phosphor);margin-top:3px;
}
.pl-action-row td{padding-top:16px;border-bottom:0}
.pl-action-row .btn{width:100%;justify-content:center;font-size:.78rem;padding:8px 10px}
.pl-already{font-size:.62rem;letter-spacing:.12em;text-transform:uppercase;color:var(--paper-dim)}
/* the text only the screen reader uses: the icon alone does not say
   "included" */
.pl-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.account-confirm{display:flex;flex-wrap:wrap;align-items:center;gap:10px;font-size:.85rem}
.view-account .field{max-width:420px}

/* ==========================================================
   8 · FIGURES, ANNOTATED CODE AND MATERIAL
   ========================================================== */

/* ---------- figure ---------- */
.fig{margin:18px 0;display:flex;flex-direction:column;gap:8px}
.fig img,.fig-svg{
  width:100%;max-width:100%;border:1px solid var(--wire);border-radius:6px;
  background:var(--panel);display:block;
}
.fig-svg{padding:14px}
.fig-svg svg{width:100%;height:auto;display:block}
.fig figcaption{
  font-family:'IBM Plex Mono',monospace;font-size:.7rem;line-height:1.5;
  color:var(--paper-dim);text-align:center;
}

/* ---------- code as an example, Go By Example style ----------

   Two columns: the EXPLANATION on the left, the PROGRAM on the right, each note
   at the height of the snippet it comments on. See the comment in text.js.

   What makes the right column look like a file, and not a pile of fragments:
   `row-gap:0`, no border between the lines, and the same background on every
   code cell. Without that the block becomes a table — which is exactly what it
   exists in order not to be.

   `align-items:start` on the note is what holds it at the top of the snippet it
   refers to; centred, it would float in the middle of ten lines of code and stop
   pointing at anything. The `<pre>` does NOT take start: it stretches to the end
   of the grid row, and it is that stretching that seams the background. */
.example{margin:24px 0}
/* The file name was alone on the right; now the copy button joins it. They stay
   together at the right end, over the code column, because that is the column
   the button acts on — put it at the far left and it would sit over the notes,
   which are not what gets copied. */
.example-bar{display:flex;align-items:center;justify-content:flex-end;gap:6px;margin-bottom:5px}
.example-file{font-size:.64rem;letter-spacing:.06em}
/* In the example the bar is outside the block's frame, so the button gives back
   the 5px the `.code-bar` padding provides there — otherwise it would hang
   past the right edge of the code column. */
.example-bar .code-copy{margin-right:-4px}

/* ONE COLUMN IS THE DEFAULT, and two are the exception.

   That is how the rule got inverted: the two columns only pay off when BOTH
   fit — the code without scrolling and the note still readable. Where they do
   not fit, what is left is chopped-up code next to a squeezed note, which is
   worse than the stacked form on any screen.

   Stacked, the note comes BEFORE the snippet: read the explanation and then the
   code is the order that works without the side-by-side alignment tying the two
   together. It is the shape the phone always had. */
.example-grid{display:grid;grid-template-columns:minmax(0,1fr);row-gap:0}
.example-note{
  padding:12px 0 8px;
  font-size:.86rem;line-height:1.62;color:var(--paper-dim);
}
.example-note:empty{padding:0}
.example-note code{
  font-family:'IBM Plex Mono',monospace;font-size:.86em;
  background:var(--panel);padding:1px 5px;border-radius:3px;color:var(--paper);
}
.example-note strong{color:var(--paper)}

.example-code{
  margin:0;padding:10px 18px;overflow-x:auto;
  scrollbar-width:thin;scrollbar-color:var(--wire) transparent;
  background:var(--panel);border-left:2px solid var(--wire);
  font-family:'IBM Plex Mono',monospace;font-size:.79rem;line-height:1.7;
  color:var(--paper);white-space:pre;
}
.example-output{
  margin-top:10px;padding:9px 16px;
  background:var(--ink);border:1px solid var(--wire);border-radius:4px;
}
.example-output-label{
  display:block;font-size:.58rem;letter-spacing:.14em;text-transform:uppercase;margin-bottom:5px;
}
.example-output .code{margin:0;padding:0;background:none;border:0;font-size:.76rem;color:var(--paper-dim)}
.example-empty{display:none}

/* THE LESSON'S SINGLE CUT, and it comes out of the arithmetic, not of taste.

   Two things happen here at the same time, because they are the same thing: the
   code block opens into two columns AND the whole lesson moves to the wide
   width. Two columns need `--code` + `--note-min` + the gap between them:

     604 + 300 + 30 = 934px

   And `--wide` cannot go past what the arrows allow — `100vw - rail - 152`.
   Equating the two:

     100vw - 380 - 152 >= 934   →   100vw >= 1466

   Below 1466px the lesson goes back to 820 and the block stays stacked, where
   the code's 598px have room to spare. Between 1466 and 1606 the lesson grows
   along with the window, from 934 up to the content's 1074; from there on it
   stops growing, because widening further would only stretch the text
   column. */
@media(min-width:1466px){
  .portal{
    /* from here up what squeezes is the arrow, not `.content`'s padding: 152
       is more than the padding's 88 at any width in this range */
    --wide:min(
      calc(var(--code) + var(--note) + 30px),
      calc(100vw - var(--rail) - 152px)
    );
    --screen:var(--wide);
  }
  .example-grid{
    /* the code is FIXED and the note absorbs whatever is left: shrinking the
       column that does not wrap is what produces the scrollbar */
    grid-template-columns:minmax(var(--note-min),1fr) var(--code);
    column-gap:30px;
  }
  .example-note{align-self:start;padding:11px 0}
  /* the file's breathing room goes at the block's ends, not on every snippet:
     the middle has to join without a seam */
  .example-code{padding:0 18px}
  .example-code:first-of-type{padding-top:12px}
  .example-code:last-of-type{padding-bottom:12px}
  .example-output{grid-column:2}
}

/* ---------- the three colours of the highlighting ----------
   Red for the language's structure, blue for the literals, white for the rest.
   Comments in the muted grey — they are the only thing read as prose, and prose
   inside code should not compete with the code. */
.t-kw{color:var(--amber)}
.t-str,.t-num{color:var(--phosphor)}
.t-com{color:var(--paper-dim);font-style:italic}
.t-fun,.t-prop{color:var(--paper)}

/* Below 900px the two columns do not fit without chopping up the code. The note
   goes ABOVE the snippet: read the explanation and then the code is the order
   that works when there is no side-by-side alignment tying the two together. */
@media(max-width:900px){
  .example-grid{grid-template-columns:1fr}
  .example-note{padding:12px 0 8px}
  .example-code{padding:10px 14px}
  .example-code:first-of-type{padding-top:10px}
  .example-output{grid-column:1}
  .example-empty{display:none}
}

/* ---------- supporting material ---------- */
.materials{margin:22px 0 0;display:flex;flex-direction:column;gap:2px}
.materials-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px}
.materials-top h3{font-family:'Space Grotesk',sans-serif;font-size:.95rem;font-weight:400}
.materials-top .mono{font-size:.66rem}
.mat{
  display:flex;align-items:center;gap:12px;padding:10px 12px;
  border:1px solid var(--wire);border-radius:5px;background:var(--panel);
  transition:border-color .15s,background .15s;
}
.mat:hover{border-color:var(--phosphor);background:var(--ink)}
.mat-icon,.mat-download{display:grid;place-items:center;flex:0 0 auto;color:var(--paper-dim)}
.mat-icon svg{width:22px;height:22px}
.mat-download{margin-left:auto}
.mat-download svg{width:17px;height:17px}
.mat:hover .mat-download{color:var(--phosphor)}
.mat-middle{display:flex;flex-direction:column;gap:2px;min-width:0}
.mat-title{font-size:.9rem;color:var(--paper)}
.mat-meta{font-size:.64rem;letter-spacing:.04em}
.course-side .materials{margin:0}

/* ==========================================================
   9 · EXAMS
   ========================================================== */

/* The card that announces the exam. It is DELIBERATELY different from an
   ordinary block: the exam is the only thing in the portal that has a
   consequence — failing costs a recorded grade — and a piece that looks like
   all the others does not warn of that. */
.exam-card{
  /* explicit `flex-direction`: `.block` stacks in a column, and without this
     line the card inherited the stacking and the button fell underneath,
     centred. */
  display:flex;flex-direction:row;flex-wrap:wrap;
  align-items:center;justify-content:space-between;gap:18px;
  border-color:var(--phosphor-dim);
}
.exam-card.passed{border-color:var(--phosphor)}
.exam-card-text{display:flex;flex-direction:column;gap:5px;min-width:220px;flex:1}
.exam-card-label{
  font-size:.6rem;letter-spacing:.16em;text-transform:uppercase;color:var(--phosphor);
}
.exam-card-text h2{font-family:'Space Grotesk',sans-serif;font-size:1.15rem}
.exam-card-text p{font-family:'IBM Plex Mono',monospace;font-size:.7rem;color:var(--paper-dim)}
.exam-card-score{color:var(--paper) !important}
.exam-card-score.passed{color:var(--phosphor) !important}
.exam-card-action{display:flex;flex-direction:column;gap:10px;min-width:190px}
.exam-card-action .bar{width:100%}

.view-exam .exam-rules .prose-list{margin:0}
.exam-notice,.exam-before{
  margin-top:12px;padding:10px 12px;border-radius:4px;font-size:.85rem;line-height:1.5;
  background:var(--ink);border:1px solid var(--wire);color:var(--paper-dim);
}
.exam-before.passed{border-color:var(--phosphor);color:var(--paper)}
.exam-score{font-size:2.4rem !important}
.exam-score.passed strong{color:var(--phosphor)}
.exam-score.missed strong{color:var(--paper-dim)}

/* Held back: while the exam is not closed, the verdict only says that the
   answer went in. The colour is neutral on purpose — any green or red here
   would give away the result the exam is holding. */
.v-recorded{color:var(--paper-dim) !important;border-color:var(--wire) !important}
.wz-dot.done{border-color:var(--paper-dim);color:var(--paper)}
.wz-warn{background:var(--panel) !important;color:var(--paper) !important;border-color:var(--phosphor) !important}

/* ==========================================================
   10 · WIDTHS
   ========================================================== */

/* Two columns on the course when each still gets ~440px — the same criterion
   the vitrine uses in its modal, and for the same reason: any narrower than
   that, and two columns read worse than one. */
@media(min-width:1024px){
  .course-cols{grid-template-columns:minmax(0,1fr) 320px}
}

/* The rail becomes a drawer at 1180px — the same cut at which the vitrine's bar
   stops fitting on one line, and for the same reason: below that there is no
   width for persistent chrome and content at the same time. */
@media(max-width:1180px){
  .portal{--rail:0px;grid-template-columns:minmax(0,1fr)}
  .rail{
    position:fixed;top:64px;left:0;bottom:0;width:min(300px,84vw);z-index:110;
    background:var(--ink);transform:translateX(-102%);transition:transform .22s ease;
    /* Full height from the nav to the floor. `height:auto` here collapsed the
       drawer to its content — a short menu left a gap of bare veil below it —
       because with `auto` the browser sizes to content and ignores `bottom`. A
       definite height fills it, and the base rule's `overflow-y:auto` still lets
       a long menu scroll inside. */
    height:calc(100vh - 64px);
  }
  body.rail-open .rail{transform:none}
  .rail{width:min(340px,88vw)}
  body.no-rail .rail-btn{display:none}
  .rail-btn{display:block}
  .rail-veil{display:block;position:fixed;inset:64px 0 0;background:rgba(10,14,20,.6);z-index:105}
  .nav-context{margin-left:14px}
}

@media(max-width:860px){
  /* base.css already turns the graph into a list; here we only release the
     fixed height */
  .view-track .graph-box{height:auto}
  .ctx-name{display:none}
}

@media(max-width:700px){
  .content{--pad-x:16px;--pad-y:20px;padding:var(--pad-y) var(--pad-x) 48px}
  .search-veil{padding:0}
  .search-box{width:100%;max-height:100%;height:100%;border-radius:0;border:0}
  .perf-row{grid-template-columns:minmax(0,1fr) auto;gap:8px}
  .perf-row .bar{grid-column:1/-1}
  .lesson-foot{flex-direction:column;align-items:stretch}
  .lesson-nav{justify-content:space-between}
  .wz-foot{flex-wrap:wrap}
  /* the two tile columns stay side by side down to quite narrow: splitting into
     two columns IS the exercise, and stacking them would erase the metaphor */
  .matching-cols{gap:7px}
  .tile{padding:9px 10px;font-size:.8rem}
}

@media(max-width:620px){
  .nav-context{display:none}
  .view-head h1{font-size:1.25rem}
  /* Search goes before everything else — it is the most dispensable item on the
     bar, because the Catalogue is one tap away in the rail and it already has
     the field. Without this the account avatar was clipped by the right border
     at 390px. It is the vitrine's own order of sacrifice, where "Student area"
     gives way first. */
  #search-btn{display:none}
  .account-avatar{width:28px;height:28px}
}

@media(prefers-reduced-motion:reduce){
  .rail{transition:none}
  .card:hover,.course-node:hover{transform:none}
}
