/* =============================================================================
   kodo.css — THE SITE SHEET. One <link>, and a page is in the system.

       <link rel="stylesheet" href="css/kodo.css">
       <link rel="stylesheet" href="<page>.css">        <!-- page chrome -->

   It is bench.css and editorial.css welded together: bench.css's three token
   tiers and its components, carrying the editorial values rather than the
   sketchbook ones. Nothing here is a retheme of anything — there is no second
   sheet left to disagree with, which is what the split cost every time a page
   forgot to load the second one, or loaded it in the wrong place.

   Three tiers, and a rule for choosing between them:
     PRIMITIVE  a raw value with no opinion about use (--cream-300)
     SEMANTIC   a role in the UI (--surface-page, --text-muted)
     DOMAIN     a fact about the science (--atom-O, --energy-atp)
   Pages and components reference the second two. Reach for a primitive only
   when the thing genuinely has no role, which is rarer than it looks.

   §6.5 is the part that is neither: the SITE SHELL — the wordmark, the gutter,
   the masthead. A page that draws its own is a page whose logo is a different
   size from the one next door, which is the state this section ended.

   bench.css is what the benches still load. It is frozen: the sketchbook look,
   kept because forty test pages are drawn in it and none of them is public.
   New work links this file.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Jost:wght@300;400;500;600&family=Literata:ital,opsz,wght@0,7..72,400;0,7..72,600;1,7..72,400&display=swap');

/* THE CALLOUT, PULLED IN RATHER THAN COPIED. Almost every lesson annotates its
   model, and components are growing their own notes for a generated page to
   switch on, so a <link> a page has to remember is a <link> a page will forget
   — and an unstyled callout on a generated page has no fallback to land on.
   Importing keeps ONE copy of the rules and one file that owns them, which is
   what the pages still on bench.css link directly. It resolves against this
   file's own URL, so every path a page reaches kodo.css by works. */
@import url('annotate.css');
/* The values, shared with the front door, which cannot load this sheet. Every
   colour below is a ROLE pointed at one of brand.css's names. brand.css */
@import url('brand.css');
/* -----------------------------------------------------------------------------
   0. BASE
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box }

/* The components below set `display`, which is an author rule and therefore
   beats the UA's `[hidden] { display: none }`. Without this a button a page
   has honestly hidden stays on screen, and nothing in the JS looks wrong.
   membrane-lab hit this three times and carried its own copy for each. */
.pill[hidden],
.btn[hidden],
.iconbtn[hidden],
.segmented[hidden],
.textbtn[hidden] { display: none }

html,
body {
  margin: 0;
  height: 100%;
  color: var(--text-strong);
  font-family: var(--font-ui);
}

/* A lesson in someone else's frame. lib/embed.js puts `bare` on <html> when
   the page is framed; each lesson marks its OWN title, because only the page
   knows which element is its name and which is a control that has to survive.
   Here rather than in a page: every lesson loads this file, and a lesson that
   has not been restyled still gets embedded.
   `!important` because the mark lands on whatever element IS the title, and
   that is often an id with its own `display` — molecule-lab's #brand is a flex
   row, and a class cannot outrank an id. A one-purpose hiding utility is the
   place this is the honest fix rather than a shortcut. */
html.bare .chrome-title { display: none !important }

/* -----------------------------------------------------------------------------
   1. PRIMITIVES
   -------------------------------------------------------------------------- */
:root {
  /* Warm neutrals. One ramp, cream through to ink, lifted off the values the
     lessons had already converged on by hand. Every surface and every piece of
     text on the paper comes from here. */
  --cream-50:  #fffdf7;
  --cream-100: #fbf7ee;
  --cream-200: #f1ece3;
  --cream-300: #efe6d6;
  --cream-400: #e2d8c6;
  --cream-500: #d8cfbf;
  --cream-600: #b9ae9d;
  --clay-500:  #8a8073;
  --clay-600:  #5a5346;
  --clay-700:  #4a4232;
  --clay-800:  #3a352f;
  --clay-900:  #2b2723;

  --slate-700: #353f44;

  /* Temperature, and the two ends of any hot/cold readout. */
  --ember-500: #e5533a;
  --azure-500: #4a90d9;
}

/* -----------------------------------------------------------------------------
   2. SEMANTIC
   -------------------------------------------------------------------------- */
:root {
  --surface-page:    var(--cream-200);   /* the stage's paper */
  --surface-rule:    var(--cream-400);   /* the dots printed on it */
  --surface-raised:  var(--cream-200);   /* a card or panel lying on the paper */
  --surface-card:    var(--cream-50);    /* a sticky note, brighter than the page */
  /* THE GROUND A MOLECULE IS DRAWN ON. A step warmer than the paper, so a
     framed viewer reads as a window cut into the page rather than as a border
     drawn on it. The 3D canvas is alpha:true — this is the colour behind it,
     and no renderer knows it exists. */
  --surface-stage:   var(--cream-200);
  --border-hair:     var(--cream-500);
  --border-strong:   var(--clay-900);    /* the drawn 2px line round a button */

  --text-strong:     var(--clay-900);
  --text-body:       var(--clay-800);
  --text-dim:        var(--clay-600);
  --text-muted:      var(--clay-500);
  --text-inverse:    var(--cream-300);   /* on an ink fill */

  /* Button fills. Brighter and more saturated than the readout accents: a
     primary pill has to carry white type and win the stage. The ion-dipole
     amber is a BOND and stays palette.js's --bond-iondipole. */
  --action-blue:     #2f7ee0;
  --action-amber:    #ef8b17;

  --accent-hot:      var(--ember-500);
  --accent-cold:     var(--azure-500);
  --sketch:          var(--slate-700);

  /* Focus is a system-level promise, not a per-page one. Half the featured
     pages ship a focus ring today and half do not; components below all use
     this so the answer stops depending on which lesson you are in. */
  --focus-ring:      2px solid var(--clay-900);
  --focus-offset:    3px;
}

/* -----------------------------------------------------------------------------
   3. DOMAIN
   Atoms and bonds are NOT written out here. They are generated from palette.js
   at load (see tokens-from-palette.js) so a caption's coloured O and the sphere
   it points at cannot drift, which is exactly what they had done: the old CSS
   said #e6362f and the renderer drew #d6362e. Available as --atom-O, --atom-H,
   --atom-Na ... and --bond-hbond, --bond-covalent, --bond-iondipole,
   --bond-condense, --bond-lonepair.

   Below are the domain colours a LESSON reads, and the bar for being here is
   both of these:

     1. NOTHING ELSE ALREADY PUBLISHES IT. A token that restates a generated one
        is the oxygen bug rebuilt with new names. If the colour you want is an
        element's or a bond's, say --atom-P or --bond-hbond and stop.
     2. MORE THAN ONE PAGE READS IT. One page's colour lives on that page.
        hemoglobin-lab's core and quaternary greens were here and went back:
        a design system that carries a colour with one caller is a place to
        lose it, not a place to share it.

   Four duplicates and one single-page pair have already left by those two
   rules. The block is meant to be small.
   -------------------------------------------------------------------------- */
:root {
  /* The two carriers, read by glycolysis-lab, krebs-lab and fermentation-lab.
     The phosphate and the carbon are NOT here: they are --atom-P and --atom-C,
     and naming them twice is how a re-tuned palette leaves a caption behind. */
  --energy-nad: #2f7d5c;
  --energy-atp: #c8452e;

  /* The membrane's channel green. ONE PAGE READS IT, which is rule 2 above
     unmet; it stays only until membrane-lab and the membrane component stop
     being two copies of one physics, because the component will want it too. */
  --membrane-channel: #2a7a6a;
}

/* -----------------------------------------------------------------------------
   4. TYPE
   Four families, named by the job they do rather than by what they look like,
   because the job is what decides which one a new page should reach for.
   -------------------------------------------------------------------------- */
:root {
  /* UI: labels, buttons, controls, anything the student operates. */
  --font-ui: 'Proxima Soft', 'Proxima Nova', 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* Editorial: headings, readouts, the claim a step is making. The workhorse.
     glycolysis reaches for it more than everything else combined. */
  --font-editorial: 'Zilla Slab', Georgia, serif;
  /* Shout: a title, a sticky note, the one word a page raises its voice for.
     Used four times on a busy page, never for a run of text. */
  --font-shout: 'Permanent Marker', cursive;
  /* Aside: annotation in the margin, in the student's own hand. */
  --font-aside: 'Caveat', cursive;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Scale. Twenty-six distinct sizes are in the featured pages today, nine of
     them fractional. These ten steps cover every one of them, and every step is
     an even number except --text-sm, which is the size the buttons wanted and
     is worth an odd box for.

     Nothing outside the buttons below is re-pointed at the scale by this file.
     Migrating a page is its own pass, with the browser open, because rounding
     12.5 up or down is a look and not a find-and-replace. */
  --text-2xs:  10px;    /* letterspaced uppercase micro-labels, axis ticks */
  --text-xs:   12px;    /* quiet chrome: dock buttons, tabs, undo */
  --text-sm:   13px;    /* buttons, dense readouts */
  --text-base: 16px;
  --text-md:   18px;    /* the step's claim */
  --text-lg:   20px;
  --text-xl:   22px;    /* page title */
  --text-2xl:  26px;
  --text-3xl:  34px;
  --text-4xl:  46px;

  --leading-tight: 1.15;
  --leading-snug:  1.35;
  --leading-body:  1.5;

  --weight-regular:  400;
  --weight-medium:   500;
  --weight-semibold: 600;
  --weight-bold:     700;

  --tracking-tight: -.01em;
  --tracking-normal: 0;
  --tracking-wide:   .2px;
  --tracking-caps:   .06em;   /* uppercase micro-labels need the air */
}

/* -----------------------------------------------------------------------------
   5. SPACE, SHAPE, MOTION
   -------------------------------------------------------------------------- */
:root {
  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  22px;
  --space-6:  30px;
  --space-7:  44px;

  --radius-sm:   6px;
  --radius-md:   8px;
  --radius-lg:   10px;
  --radius-pill: 999px;

  --shadow-raise: 0 2px 0 rgba(43, 39, 35, .18), 0 6px 16px -8px rgba(43, 39, 35, .6);
  --shadow-panel: 0 6px 24px -12px rgba(43, 39, 35, .45);
  --shadow-press: 0 1px 0 rgba(43, 39, 35, .18);

  --ease-out: cubic-bezier(.2, .9, .3, 1);
  --dur-fast: .12s;
  --dur-base: .2s;
}

/* -----------------------------------------------------------------------------
   6. LEGACY ALIASES
   The names the featured lessons, pathways.css and water-tutor.css already
   use, pointed at the tiers above. They are the migration's checklist: when
   nothing greps for --ink any more, that line goes.
   -------------------------------------------------------------------------- */
:root {
  --paper:      var(--surface-page);
  --paper-line: var(--surface-rule);
  --panel:      var(--surface-raised);
  --ink:        var(--text-strong);
  --muted:      var(--text-muted);
  --hair:       var(--border-hair);
  --radius:     var(--radius-lg);
  --hot:        var(--accent-hot);
  --cold:       var(--accent-cold);

  --font-sans:   var(--font-ui);
  --font-slab:   var(--font-editorial);
  --font-marker: var(--font-shout);
  --font-hand:   var(--font-aside);

}

/* -----------------------------------------------------------------------------
   6.1 THE EDITORIAL VALUES
   The tiers above keep their structure; these are the numbers the site is
   actually drawn in. They come last so nothing has to reason about cascade
   order, and they overwrite ROLES only — no primitive changes meaning here.

   6.1a SEMANTIC — the paper, the ink and the two accents. The primitives above
   keep their values; nothing outside a role changes.
   -------------------------------------------------------------------------- */
:root {
  --paper-white: var(--brand-paper);
  --paper-card:  var(--brand-card);
  --rule-line:   var(--brand-rule);
  --ink-900:     var(--brand-ink);
  --ink-700:     var(--brand-ink-2);
  --ink-600:     var(--brand-ink-3);
  --ink-500:     var(--brand-mute);
  --green-700:   var(--brand-green);    /* accent */
  --orange-500:  var(--brand-orange);   /* spark, the warm counterpoint */

  --surface-page:   var(--paper-white);
  --surface-rule:   transparent;      /* this paper is flat; see §10.1 */
  --surface-raised: var(--paper-white);
  --surface-card:   var(--paper-card);
  /* Warmer than this paper on purpose. #faf8f4 behind a molecule on #faf8f4
     paper is a frame with nothing in it. */
  --surface-stage:  var(--brand-paper-2);
  --border-hair:    var(--rule-line);
  --border-strong:  var(--ink-900);

  --text-strong:  var(--ink-900);
  --text-body:    var(--ink-700);
  --text-dim:     var(--ink-600);
  --text-muted:   var(--ink-500);
  --text-inverse: var(--paper-white);

  /* THE MICRO-CAP, the system's only heading device outside prose. Two steps:
     -lg names a whole thing on screen (the lesson, the subject), -sm labels a
     part of the chrome (a ledger, a control, a step). Same face, same tracking,
     same weight, same colour — the SIZE is the whole hierarchy, so a page
     cannot invent a third treatment by picking its own tracking. */
  --cap-lg:     12px;
  --cap-sm:     10px;
  --cap-track:  .22em;
  --cap-weight: 600;

  --accent: var(--green-700);
  --spark:  var(--orange-500);

  /* The signature hues are brand.css's, unrenamed on purpose: they are a
     categorical set and giving one a role name here would be the first step to
     a second primary. A section picks one; --accent stays the button. */

  --focus-ring: 2px solid var(--green-700);
}

/* -----------------------------------------------------------------------------
   6.1b TYPE — Futura where the machine has it, Jost as the geometric stand-in.
   The two hands die here: --font-shout and --font-aside are what the marker
   headings and the margin scrawl resolve through, so pointing them at the
   editorial pair converts every page that uses them without touching one.
   -------------------------------------------------------------------------- */
:root {
  /* Two sans faces, split by job. The geometric one is the site's voice and
     it is a DISPLAY face: it sets a wordmark and a micro-cap beautifully and
     a paragraph badly, and its 600 at 13px is a black bar. Body and UI text
     therefore get the system face, which is what the reader's OS has already
     hinted for prose at that size. */
  --font-display: 'Futura', 'Futura PT', 'Jost', -apple-system, 'Segoe UI', sans-serif;
  --font-ui:      system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-editorial: 'Literata', Georgia, serif;
  --font-shout:     var(--font-display);
  --font-aside:     var(--font-editorial);

  --tracking-wide: .02em;

  /* Even corners and a hairline edge. Every radius collapses toward the
     homepage's 4px; the pill stays a pill. */
  --radius-sm: 3px;
  --radius-md: 4px;
  --radius-lg: 4px;

  --shadow-raise: 0 1px 2px rgba(20,22,26,.06), 0 10px 26px -12px rgba(20,22,26,.22);
  --shadow-panel: 0 10px 26px -10px rgba(20,22,26,.16);
  --shadow-press: 0 1px 1px rgba(20,22,26,.10);
}

/* A geometric sans sets tighter than Nunito at the same size, and the display
   steps are where that reads as loose. Headings only; body text is untouched. */
:root {
  --tracking-tight: -.03em;
}

/* =============================================================================
   6.5 THE SITE SHELL

   Every public page is the same three blocks in the same order, so that moving
   between them feels like turning a page rather than opening another site:

       <body class="kodo">
       <div class="page">                       gutter + measure, once
         <nav class="sitenav">                  wordmark, crumb, anything right
           <a class="mark" href="/"><img src="/kodolab-wordmark.svg" alt="kodolab"></a>
           <span class="crumb">proteins</span>
         </nav>
         <header class="pagehead">              name of THIS page, and its deck
           <h1>Myoglobin</h1>
           <p class="deck">...</p>
         </header>
         <main> ... </main>
         <footer class="sitefoot"> ... </footer>
       </div>

   THE WORDMARK IS ONE ELEMENT AND ONE SIZE. `.mark` sets 1.35rem everywhere;
   the homepage is the single exception and says so out loud with `.mark.hero`,
   which is a size change and not a second treatment — same letters, same green
   `lab`, same spark full stop. Four pages each spelling the mark out in their
   own <style> is how one of them ends up a different colour, and it is what
   this replaces.

   THE GUTTER IS A TOKEN, NOT A PADDING. `--page-gutter` is read by `.page`, so
   a full-bleed child (a stage, a rule that has to touch both edges) can undo it
   with one negative margin instead of guessing the clamp. `--page-max` is the
   measure; a page that wants a wider shelf sets the token rather than restating
   the rule.
   ========================================================================== */
:root {
  --page-max:    1180px;
  --page-gutter: clamp(1.25rem, 5vw, 4rem);
  /* Nav to the top of the window, and FLAT: it used to be a clamp, which grew
     the gap with the viewport and read as the bar sinking down a wide screen.
     The value lands the wordmark on the same line the front door's does — not
     the same number, because contribute.html pads .9rem around a row a button
     makes taller and centres the mark in it, and this bar is the mark's height.
     Matching the padding there puts the mark 6px high; matching the MARK is
     what a reader turning the page actually sees. */
  --page-top:    1.29rem;
  --head-gap:    clamp(2rem, 5vw, 3.25rem);     /* masthead rule to first content */
  --mark-size:   1.35rem;
  /* THE APP BAR'S INSET. A full-window page — a lesson over a canvas, the
     builder — sets its bar on these rather than on `--page-top` and the page
     gutter, which are a document's rhythm and sit the mark lower and further
     in. They are tokens because two DIFFERENT bars read them: `.sitenav
     .floating` below, and the builder's own `.bar`, which cannot take that
     class because it is in flow and floating is fixed. A number typed into
     either one is a wordmark that moves when you turn the page. */
  --bar-top:     1.1rem;
  --bar-side:    clamp(1rem, 3vw, 2rem);
  --bar-bottom:  .9rem;
  --page-gap:    clamp(1rem, 2.2vw, 1.5rem);    /* the grid gutter every shelf uses */
}

/* The opt-in. A lesson is a full-bleed #app and must not inherit a document's
   paper rhythm, so this is a class and not `body`. */
body.kodo {
  overflow-x: clip;                /* the bar's full-bleed rule is 100vw wide */
  background: var(--surface-page);
  color: var(--text-strong);
  font-family: var(--font-ui);
  line-height: 1.55;
  height: auto;                    /* §0 pins html,body to the viewport */
  padding: 0 0 clamp(4rem, 9vw, 7rem);
  -webkit-font-smoothing: antialiased;
}

.page {
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 0 var(--page-gutter);
}

/* ---- the bar -------------------------------------------------------------
   Baseline-aligned, because the crumb is set against the wordmark's baseline
   rather than centred on its box: a geometric face descending below nothing
   sits visibly high when it is centred. */
.sitenav {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .75rem;
  padding: var(--page-top) 0 1.25rem;
  position: relative;
}
/* THE LINE UNDER THE BAR IS FULL BLEED, and it is a hair, not the masthead's
   2px: the bar is chrome that runs the width of the window, so its underline
   is the window's, not the text column's. Drawn as a pseudo-element because
   `.sitenav` sits inside `.page`'s gutters; `overflow-x: clip` on the body is
   what keeps a 100vw child from adding a scrollbar. */
.sitenav::after {
  content: ''; position: absolute; bottom: 0; left: 50%;
  width: 100vw; margin-left: -50vw; height: 1px;
  background: var(--border-hair);
}
.sitenav .spacer { flex: 1 }
/* lessons.html's bar, on every document page: its 1180 column (wider than the
   text column, which sits inside `.page`'s gutters), its padding and its 132px
   wordmark, so the mark and the links do not move from one page to the next.
   Not the floating bar: over a lesson's scene the smaller mark covers less. */
.sitenav:not(.floating) {
  padding: 1rem 0 .9rem;
  margin-inline: calc(50% - min(590px, 50vw - 2rem));
}
.sitenav:not(.floating) .mark img { width: 132px; height: auto; }

.mark {
  line-height: 0;                  /* no descender strip under an inline image */
  text-decoration: none;
  margin: 0;
}
/* The mark is the drawn wordmark, sized off the same token the lettered
   fallback used, so `.mark.hero` still scales it by changing --mark-size.
   It stays INLINE on `vertical-align: baseline`, because a replaced element's
   baseline is its bottom edge and the drawn letters sit on that edge: that is
   what puts the crumb on the wordmark's own baseline in `.sitenav`'s baseline
   row. `display:block` forfeits the baseline entirely and `vertical-align:
   bottom` moves it, either way setting the crumb 6px off. The `line-height: 0`
   on `.mark` above is the other half: it takes the descender strip an inline
   image would otherwise sit above. */
.mark img { display: inline-block; vertical-align: baseline; height: var(--mark-size); width: auto; }

/* The homepage, and only the homepage. One token, so the mark cannot pick up a
   second treatment on the way to being bigger. */
.mark.hero { --mark-size: clamp(2.8rem, 9vw, 5.5rem) }

/* Where you are. A <span> when this page IS that place, an <a> when it is a
   level up — same size either way, so the bar does not change shape. */
.crumb {
  font-family: var(--font-ui);
  font-size: .74rem;
  font-weight: var(--cap-weight);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-muted);
  text-decoration: none;
}
a.crumb:hover { color: var(--accent) }

/* ---- the four places, on the right of the bar ----------------------------
   INJECTED BY lib/site.js, not typed into a page: a link added to one bar is a
   link the other nine do not have. The same micro-cap the crumb is set in,
   because the bar is one row of signage and a second size in it reads as two
   bars stacked. Where you are is the one entry that is not a way out, so it
   takes the ink the others take on hover and stays there. */
.sitelinks {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: clamp(.9rem, 2.4vw, 1.7rem);
}
.sitelinks a {
  font-family: 'Jost', var(--font-ui);
  line-height: normal;
  font-size: .74rem;
  font-weight: var(--cap-weight);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-muted);
  text-decoration: none;
  transition: color var(--dur-fast);
}
.sitelinks a:hover,
.sitelinks a[aria-current="page"] { color: var(--text-strong) }
.sitelinks a:hover { color: var(--accent) }

/* ---- the account, at the bar's right end ----------------------------------
   lib/site.js paints it: "Sign in", or the person's own links and an avatar +
   first name that opens a small card (full name, email, sign out). The
   builder's bar takes the same rules, so `.bar .links` is listed with it.
   THE NAME ALONE SETS THE BASELINE: the avatar is taller than the text and is
   taken out of flow and centred on the name, so the pair sits on the bar's
   line in every browser (Safari synthesises a baseline for a flex or details
   box, which put it a few pixels low). */
.sitelinks .signin,
.bar .links .signin {
  padding: .35em .9em; border: 1.5px solid currentColor; border-radius: var(--radius-pill);
}
.sitelinks .rule,
.bar .links .rule { width: 1px; height: 1.1rem; align-self: center; background: var(--border-hair); }
.sitelinks .menu,
.bar .links .menu { position: relative; display: inline-block; }
.sitelinks .menu .summary,
.bar .links .menu .summary {
  position: relative; display: inline-block; padding: 0 0 0 2.2rem; margin: 0;
  border: 0; background: none; cursor: pointer;
  font-family: 'Jost', var(--font-ui); font-size: .74rem; font-weight: var(--cap-weight);
  letter-spacing: .18em; text-transform: uppercase; color: var(--text-muted);
  line-height: inherit; transition: color var(--dur-fast);
}
.sitelinks .menu .summary:hover,
.sitelinks .menu .summary[aria-expanded="true"],
.bar .links .menu .summary:hover,
.bar .links .menu .summary[aria-expanded="true"] { color: var(--text-strong) }
.sitelinks .menu .summary:focus-visible,
.bar .links .menu .summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--radius-pill); }
.sitelinks .menu .acct-name,
.bar .links .menu .acct-name { font: inherit; letter-spacing: inherit; text-transform: inherit; }
.sitelinks .avatar,
.bar .links .avatar {
  position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 1.7rem; height: 1.7rem; border-radius: 50%; overflow: hidden;
  display: grid; place-items: center;
  background: var(--accent); color: var(--surface-page);
  font-size: .8rem; letter-spacing: 0;
}
.sitelinks .avatar img,
.bar .links .avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sitelinks .menu .card,
.bar .links .menu .card {
  position: absolute; right: 0; top: calc(100% + .6rem); z-index: 20;
  min-width: 15rem; padding: .9rem 1rem;
  background: var(--surface-card); border: 1px solid var(--border-hair);
  border-radius: 12px; box-shadow: var(--shadow-panel);
  display: flex; flex-direction: column; gap: .75rem;
  font-family: var(--font-ui); color: var(--text-body); text-transform: none; letter-spacing: 0;
}
.sitelinks .menu .card[hidden],
.bar .links .menu .card[hidden] { display: none }
.sitelinks .menu .who,
.bar .links .menu .who { margin: 0; display: flex; flex-direction: column; gap: .15rem; font-size: .9rem; line-height: 1.3; }
.sitelinks .menu .who b,
.bar .links .menu .who b { color: var(--text-strong); font-weight: 600; }
.sitelinks .menu .who span,
.bar .links .menu .who span { color: var(--text-muted); font-size: .8rem; overflow-wrap: anywhere; }
.sitelinks .menu .card button,
.bar .links .menu .card button {
  align-self: flex-start; cursor: pointer;
  padding: .4em 1em; border: 1.5px solid var(--border-strong); border-radius: var(--radius-pill);
  background: transparent; color: var(--text-strong);
  font: inherit; font-size: .8rem; font-weight: 600;
  transition: background var(--dur-fast), color var(--dur-fast);
}
.sitelinks .menu .card button:hover,
.bar .links .menu .card button:hover { background: var(--border-strong); color: var(--surface-page); }

/* ---- the masthead --------------------------------------------------------
   The 2px rule under it is the one heavy line in the system, and it means "the
   page's own name stops here". A reading page, where the deck runs straight
   into the first section, sets `.pagehead.open` and has none. */
.pagehead {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: 0 clamp(1rem, 2.2vw, 1.5rem);
  padding: clamp(1.25rem, 3.5vw, 2rem) 0 1.5rem;
  border-bottom: 2px solid var(--border-strong);
  margin-bottom: var(--head-gap);
}
.pagehead.open { border-bottom: 0; margin-bottom: clamp(1rem, 2.5vw, 1.75rem) }

.pagehead h1 {
  font-family: var(--font-display);
  font-size: clamp(2.4rem, 7vw, 4.2rem);
  font-weight: 500;
  letter-spacing: -.045em;
  line-height: .9;
  text-transform: lowercase;
  margin: 0;
}
/* A proper noun keeps its capital: a protein is called Myoglobin, and the
   masthead is not the place to argue with that. */
.pagehead h1.proper { text-transform: none; font-weight: 400; letter-spacing: -.01em }

/* The sentence under the name. Serif, because it is prose and the bar above
   it is not. */
.pagehead .deck {
  flex-basis: 100%;
  font-family: var(--font-editorial);
  font-size: clamp(1.05rem, 1.9vw, 1.3rem);
  line-height: 1.35;
  color: var(--text-muted);
  max-width: 34em;
  margin: .9rem 0 0;
}
/* The homepage's tagline: a line of prose set on the masthead's right, ranged
   against the rule so the wordmark and it share one baseline. It is the only
   masthead here whose second element is a sentence rather than a count. */
.pagehead .tagline {
  font-family: var(--font-editorial);
  font-size: clamp(1rem, 1.6vw, 1.22rem);
  line-height: 1.35;
  color: var(--text-muted);
  max-width: 26ch;
  text-align: right;
  margin: 0 0 .35rem auto;
}
@media (max-width: 700px) {
  .pagehead .tagline { text-align: left; margin: .9rem 0 0; width: 100% }
}

/* A count, a date, anything tabular the masthead carries on its right. */
.pagehead .meta {
  margin: 0 0 0 auto;
  font-size: .74rem; font-weight: var(--cap-weight);
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 700px) {
  .pagehead .meta { margin: .9rem 0 0; width: 100% }
}

/* One line under the rule: what the reader is looking at a collection OF. */
.lede {
  font-size: 1rem; line-height: 1.6; color: var(--text-muted);
  max-width: 60ch; margin: 0 0 var(--head-gap);
}

/* ---- section heads inside main -------------------------------------------
   A micro-cap, a hairline that eats the rest of the width, and an optional
   count or link on the end. The homepage's device, available to every page. */
.sechead {
  display: flex; align-items: baseline; gap: 1rem;
  margin: 0 0 1.5rem;
}
.sechead h2 {
  font-family: var(--font-display);
  font-size: .72rem; font-weight: var(--cap-weight);
  letter-spacing: var(--cap-track); text-transform: uppercase;
  color: var(--text-strong);
  margin: 0;
}
.sechead .rule  { flex: 1; height: 1px; background: var(--border-hair) }
.sechead .count,
.sechead .more  { font-size: .72rem; letter-spacing: .1em; color: var(--text-muted);
                  text-decoration: none }
.sechead .more:hover { color: var(--accent) }

body.kodo main > section { margin-bottom: clamp(3rem, 6vw, 4.5rem) }

/* ---- a link in running prose -------------------------------------------- */
.kodo main p a {
  color: var(--text-strong); text-decoration: none;
  border-bottom: 1px solid var(--border-hair); padding-bottom: 1px;
  transition: color .15s, border-color .15s;
}
.kodo main p a:hover { color: var(--accent); border-color: var(--accent) }

/* ---- the foot ------------------------------------------------------------ */
.sitefoot {
  margin-top: clamp(2.5rem, 6vw, 4rem);
  padding-top: 1.5rem;
  border-top: 1px solid var(--border-hair);
  display: flex; justify-content: space-between; flex-wrap: wrap; gap: 1rem;
  font-size: .8rem; letter-spacing: .06em; color: var(--text-muted);
}
.sitefoot a {
  color: var(--text-muted); text-decoration: none;
  border-bottom: 1px solid var(--border-hair); padding-bottom: 1px;
  transition: color .15s, border-color .15s;
}
.sitefoot a:hover { color: var(--accent); border-color: var(--accent) }
.sitefoot .sep { opacity: .5; margin: 0 .15rem }

/* ---- the menu button, phones only -----------------------------------------
   lib/site.js adds it and marks the row it sits in `.navfold`; `.open` on that
   row unfolds the links as a column. The builder's bar takes it too. */
.navfold > .burger { display: none }
@media (max-width: 560px) {
  .navfold { flex-wrap: wrap; align-items: center }
  .navfold > .burger {
    display: grid; place-content: center; gap: 5px; width: 44px; height: 44px; margin: 0 -10px 0 auto;
    padding: 0; border: 0; background: none; cursor: pointer;
  }
  .navfold > .burger i { display: block; width: 22px; height: 2px; border-radius: 1px; background: var(--text-strong); transition: transform var(--dur-fast), opacity var(--dur-fast); }
  .navfold.nav-open > .burger i:nth-child(1) { transform: translateY(7px) rotate(45deg) }
  .navfold.nav-open > .burger i:nth-child(2) { opacity: 0 }
  .navfold.nav-open > .burger i:nth-child(3) { transform: translateY(-7px) rotate(-45deg) }
  /* the mark's line-height:0 box is not the image's; a flex box is, so it centres */
  .navfold > :is(.mark, .mk), .navfold > .burger { display: flex; align-self: center }
  .navfold > .burger { display: grid }
  .navfold > .crumb { order: 1; flex-basis: 100% }
  .navfold > nav { order: 2; display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 0; padding-top: .4rem }
  .navfold.nav-open > nav { display: flex !important }
  .navfold > nav > * { padding: .8rem 0 }
  .navfold > nav .rule { display: none }
  .navfold > nav .menu .card { left: 0; right: auto }
  .sitenav.floating.nav-open .sitelinks { background: var(--surface-card); border-radius: 12px; padding: .4rem 1rem; box-shadow: var(--shadow-panel) }
}

/* ---- the app variant -----------------------------------------------------
   The node graph is one canvas filling the window, so its bar floats over the
   scene instead of sitting above it. Same wordmark, same size, same crumb; the
   gutter narrows because there is no measure to centre against. */
.sitenav.floating {
  position: fixed; inset: 0 0 auto 0; z-index: 5;
  padding: var(--bar-top) var(--bar-side) var(--bar-bottom);
  gap: 1rem;
  pointer-events: none;
}
.sitenav.floating::after { content: none }
.sitenav.floating > * { pointer-events: auto }
.sitenav.floating .spacer { pointer-events: none }

/* ---- About this lesson ---------------------------------------------------
   The lesson in prose, for search and for a student who lands cold. The
   section is in the file (tools/seo.js writes it) with `hidden` on; lib/site.js
   adds the info button to the bar's right end and the close to the panel. A panel over the scene, so the full-window layout is untouched. */
.sitenav .aboutbtn { width: 30px; height: 30px; font-size: 18px; align-self: center }
#lesson-about .aboutclose { position: absolute; top: 6px; right: 6px; width: 30px; height: 30px; font-size: 16px }
#lesson-about {
  /* Above annotate.css's cards (400) and the bar: the lesson is behind it while it is open. */
  position: fixed; top: 60px; right: var(--bar-side); z-index: 500;
  width: min(400px, 86vw); max-height: calc(100vh - 84px); overflow-y: auto;
  padding: 16px 20px 18px; border-radius: 4px;
  background: rgba(255, 255, 255, .95); border: 1px solid var(--border-hair);
  box-shadow: 0 6px 18px -8px rgba(0, 0, 0, .45); backdrop-filter: blur(2px);
  user-select: text; -webkit-user-select: text;
}
#lesson-about h1 {
  padding-right: 28px;
  font-family: var(--font-display); font-size: 17px; font-weight: 600;
  letter-spacing: -.01em; line-height: 1.25; margin: 0 0 10px; color: var(--text-strong);
}
#lesson-about h2 {
  font-family: var(--font-ui); font-size: .68rem; font-weight: var(--cap-weight);
  letter-spacing: .18em; text-transform: uppercase; color: var(--text-muted);
  margin: 14px 0 4px; padding-top: 11px; border-top: 1px solid var(--border-hair);
}
#lesson-about p, #lesson-about li {
  font-family: var(--font-editorial); font-size: 13px; line-height: 1.6;
  color: var(--text-body); margin: 0 0 8px;
}
#lesson-about ol { margin: 0; padding-left: 1.3em }
#lesson-about li { margin: 0 0 2px }

/* =============================================================================
   7. BUTTONS

   Six shapes, lifted from what the featured lessons had already converged on
   rather than invented. Each is a base class plus modifiers, so a page picks a
   shape and then says only what is true of its own button (usually a colour).

     .cta        the page's action. Primary solid, .cta--ghost secondary.
     .pill       capsule. The default for anything ON THE STAGE.
     .btn        drawn rectangle, ink border, inverts on hover. Docks and trays.
     .iconbtn    circular, one glyph. Play, info, close.
     .segmented  the cta capsule cut into pieces. One control, N answers.
     .textbtn    underlined text. Undo, dismiss, anything reversible and minor.

   .cta AND .pill ARE NOT THE SAME BUTTON. A .pill is an instrument standing on
   the model's paper, so it is a solid body with a side and a shadow. A .cta is
   a flat capsule set in a document, where a raised key would read as a sticker
   on the page. Which one a control is depends on what it is lying on, not on
   how important it is.

   Every one of them takes the same focus ring. Half the featured pages ship a
   focus ring today and half do not, and which half you are in is not something
   a student should be able to notice.
   ========================================================================== */

.cta,
.pill,
.btn,
.iconbtn,
.segmented > *,
.textbtn {
  font-family: var(--font-ui);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.cta:focus-visible,
.pill:focus-visible,
.btn:focus-visible,
.iconbtn:focus-visible,
.segmented > *:focus-visible,
.textbtn:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.cta[disabled],
.pill[disabled],
.btn[disabled],
.iconbtn[disabled],
.segmented > *[disabled],
.textbtn[disabled] {
  opacity: .45;
  cursor: default;
}

/* ---------- .cta ----------
   The page's action, and the only control here that is usually an <a>. Two
   weights and no third: solid where the page wants one answer, ghost where it
   offers a second. A page recolours with --cta-fill and says nothing else.

   IT LIFTS AND WARMS ON HOVER RATHER THAN DARKENING. The accent is already the
   darkest thing on the paper; taking it further down reads as disabled, so
   hover moves to the spark, which is the one other colour the system has and
   the only place it is used at this size. */
.cta {
  --cta-fill: var(--accent);
  --cta-hover: var(--spark);
  display: inline-flex;
  align-items: center;
  gap: .6em;
  padding: 1rem 2.2rem;
  border: 1px solid var(--cta-fill);
  border-radius: var(--radius-pill);
  background: var(--cta-fill);
  color: var(--text-inverse);
  /* The display face, uppercase and letterspaced: a .cta is a small piece of
     signage, which is the one job a geometric face is unambiguously for. */
  font-family: var(--font-display);
  font-size: .95rem;
  font-weight: var(--weight-medium);
  letter-spacing: .08em;
  text-transform: uppercase;
  line-height: 1;
  /* A capsule does not wrap. Squeezed in a flex row it would otherwise set its
     label over three lines inside the pill and grow to 80px, which reads as a
     broken control rather than as a narrow one. */
  white-space: nowrap;
  text-decoration: none;
  transition: background var(--dur-base), border-color var(--dur-base),
              color var(--dur-base), transform var(--dur-base),
              box-shadow var(--dur-base);
}
/* A glyph set into the line, not a picture beside it. The negative block margin
   is what keeps a cta with an icon the same height as one without: at 1.1em the
   glyph's box is taller than the label's line box, and as a flex item it would
   otherwise set the button's height and leave the pair 2px out of step wherever
   they sit side by side. */
.cta i { font-size: 1.1em; line-height: 1; margin-block: -1px }

.cta:hover:not([disabled]) {
  background: var(--cta-hover);
  border-color: var(--cta-hover);
  color: var(--text-inverse);
  transform: translateY(-2px);
  box-shadow: 0 8px 20px -6px var(--cta-hover);
}

/* THE SECOND ANSWER. Hollow, in the same outline the solid one fills, so the
   pair reads as one control with two weights rather than two controls. It fills
   on hover, which is the same promise .btn makes. */
.cta--ghost {
  background: transparent;
  color: var(--cta-fill);
}

/* In a row of prose, a footer, a card: the same signage two steps down. */
.cta--sm {
  padding: .6rem 1.2rem;
  font-size: .8rem;
  gap: .45em;
}

/* ---------- .pill ---------- */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border: 0;
  border-radius: var(--radius-pill);
  padding: 16px 32px;
  /* THE SAME SIGNAGE A .cta SETS. Same face, size, weight and tracking, so the
     one control on the stage and the one in the document are read as the same
     kind of act. What still separates them is the body: a pill has a side and
     a shadow because it stands on the model's paper. */
  font-family: var(--font-display);
  font-size: .95rem;
  font-weight: var(--weight-medium);
  letter-spacing: .08em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  /* TWO COLOURS, AND ONE FALLBACK DOING THE DECIDING. --pill-fill is left
     undeclared so that `var(--pill-fill, X)` can tell a page that named a
     colour from one that did not:

       nobody set it  ->  accent at rest, spark on hover, like a .cta
       a page set it  ->  that colour at rest AND on hover

     which is what keeps a substance's pill honest. water-lab's Add Water is
     H-bond blue and massaction's says "press the blue one and blue dots
     arrive"; a button that turned orange under the pointer would be making a
     claim about a different substance. */
  --pill-now:  var(--pill-fill, var(--accent));
  --pill-over: var(--pill-fill, var(--spark));
  /* The key it sits on: the fill's own colour taken down toward ink, so the
     edge reads as the same substance in shadow. Translucent ink is the
     fallback where color-mix is missing. */
  --pill-rest: 4px;
  --pill-lift: var(--pill-rest);
  --pill-edge: rgba(43, 39, 35, .32);
  background: var(--pill-now);
  color: var(--text-inverse);
  /* Top highlight, solid edge, ambient shadow: the three things that make it
     a body with a side rather than a rectangle with a drop shadow. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .22),
    0 var(--pill-lift) 0 var(--pill-edge),
    0 calc(var(--pill-lift) + 4px) 12px -4px rgba(43, 39, 35, .45);
  /* The bottom of the edge is what stays put on the paper: the body rides
     whatever --pill-lift is, so growing or collapsing the edge moves the label
     and nothing else has to be restated. */
  transform: translateY(calc(var(--pill-rest) - var(--pill-lift)));
  transition: filter var(--dur-fast), transform var(--dur-fast) cubic-bezier(.2, .8, .3, 1), box-shadow var(--dur-fast);
}

@supports (color: color-mix(in srgb, red, blue)) {
  .pill { --pill-edge: color-mix(in srgb, var(--pill-now) 68%, #241f1b) }
  /* The side has to follow the top. An accent body over an accent-dark edge
     that stayed put while the body warmed would read as two buttons. */
  .pill:hover:not([disabled]) { --pill-edge: color-mix(in srgb, var(--pill-over) 68%, #241f1b) }
}

/* Hover thickens the edge, which stands the whole key up off the paper, and
   warms to the spark where the page has not named a colour of its own. */
.pill:hover:not([disabled]) {
  background: var(--pill-over);
  filter: brightness(1.04);
  --pill-lift: 7px;
}

/* Edge collapses to nothing, so the body lands flat on the paper. */
.pill:active:not([disabled]) {
  --pill-lift: 0px;
  filter: brightness(.96);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .12),
    0 0 0 var(--pill-edge),
    0 1px 3px -1px rgba(43, 39, 35, .4);
  transition-duration: .06s;
}

@media (prefers-reduced-motion: reduce) {
  .pill { transition: filter var(--dur-fast) }
  .pill,
  .pill:hover:not([disabled]),
  .pill:active:not([disabled]) { transform: none }
}

/* Ghost: the same capsule with the fill taken away. This is what a row of step
   tabs is, so a lesson's spine and its buttons stay one family.

   IT DOES NOT TAKE THE SIGNAGE. The solid pill sets uppercase because it carries
   one or two words and is the thing being pressed; a step tab carries a phrase
   ("the universal solvent", "why ice floats"), and a letterspaced capital
   sentence is a line a reader has to decode rather than scan. Signage is for a
   label, not for a title. */
.pill--ghost {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border-hair);
  --pill-rest: 0px;
  box-shadow: none;
  padding: 8px 16px;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: none;
  /* 14 + 16 padding + 2 border = 32, the same height as .btn. The line-height
     rather than the padding is the lever, so the tabs keep their own shape. */
  line-height: 14px;
}

/* A GHOST HAS NO FILL, HOVERED OR NOT. `.pill:hover` warms to the spark where
   the page has not named a colour, and a ghost has not named one — so without
   this line a step tab went solid orange under the pointer. It is stated here
   rather than by narrowing the base rule because the ghost is the exception:
   every other pill is a body that changes colour, and this one is an outline. */
.pill--ghost:hover:not([disabled]) {
  background: transparent;
  filter: none;
  --pill-lift: 0px;
  color: var(--text-strong);
  border-color: var(--text-muted);
}

/* `:not([aria-current="false"])` and not a bare `[aria-current]`. An honest
   implementation writes the attribute on EVERY tab and sets it false on the
   ones that are not current, which a presence check reads as "all selected" —
   dna-anatomy did exactly that and drew eight chosen tabs. The other pages
   here add and remove the attribute instead, so both spellings have to work,
   along with the values ARIA actually defines (true, step, page). */
.pill--ghost[aria-current]:not([aria-current="false"]),
.pill--ghost.is-on {
  color: var(--text-strong);
  font-weight: var(--weight-semibold);
  border-color: var(--text-strong);
}

/* Matched to .cta--sm's type, the way the full-size pill is matched to .cta's.
   The line-height is stated because .pill's 1 would leave an odd box. */
.pill--sm { padding: 10px 20px; font-size: .8rem; line-height: 14px; --pill-rest: 3px }

/* ---------- .pill--check ---------- */
/* A CHECKBOX SHAPED LIKE THE CONTROL BESIDE IT. `.btn--check` is this switch in
   a document; this is the same switch standing on the model's paper, so it takes
   the ghost capsule and sits level with a `.segmented--sm` in the same row —
   which is where it came from: molecules.html puts a 3D/2D switch and a "C-H
   hydrogens" box side by side, and a square chip beside a capsule reads as two
   unrelated controls.

   Always a <label> with a real <input type="checkbox"> inside. The label is not
   focusable, so the ring is borrowed from the input.

   IT DOES NOT CHANGE WHEN CHECKED, exactly as .btn--check does not: the tick is
   the state, in ink, and darkening the capsule around it says the same thing
   twice. That also keeps it distinct from `.is-on`, which on a ghost pill means
   "this tab is the one you are on". */
/* 16 + 16 padding + 2 border = 34, which is what a `.segmented--sm` stands
   (9.6 padding twice over a 12.8px cap line, plus its own border). The BOX is
   what sets the 16, not the label: a checkbox is taller than the 14px line
   `.pill--ghost` sets, so the two have to be stated together or the capsule
   comes out a pixel short of the switch beside it. */
.pill--check { padding: 8px 16px 8px 12px; line-height: 16px }

.pill--check input {
  margin: 0;
  width: 16px;
  height: 16px;
  accent-color: var(--check-ink, var(--text-strong));
  cursor: inherit;
}

.pill--check:has(input:focus-visible) {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* The nudge: a ring breathing out of a pill while the scene is still too empty
   to show what the step is about. The button itself does not move, so it stays
   an easy target while it is asking. */
.pill.is-nudging { animation: pill-nudge 1.9s ease-out infinite }

/* The ring is offset by half the lift: the button's visual mass is the body
   plus the edge below it, so a ring centred on the body alone sits high. */
@keyframes pill-nudge {
  0%   { box-shadow: 0 var(--pill-lift) 0 var(--pill-edge), 0 calc(var(--pill-lift) / 2) 0 0   var(--pill-now) }
  70%  { box-shadow: 0 var(--pill-lift) 0 var(--pill-edge), 0 calc(var(--pill-lift) / 2) 0 14px transparent }
  100% { box-shadow: 0 var(--pill-lift) 0 var(--pill-edge), 0 calc(var(--pill-lift) / 2) 0 0   transparent }
}

@media (prefers-reduced-motion: reduce) {
  .pill.is-nudging {
    animation: none;
    outline: 2px solid var(--pill-now);
    outline-offset: 3px;
  }
}

/* ---------- .btn ----------
   glycolysis-lab's dock buttons, promoted. Quiet by default: muted type on a
   hair-thin border with nothing behind it, filling with ink only on hover. A
   lesson's stage is busy, and a row of white-filled buttons in the corner
   competes with the molecule for the one thing the student should be looking
   at. The fill on hover is what makes it a door.

   Icons are Phosphor bold (<i class="ph-bold ph-arrow-counter-clockwise">),
   and most .btn instances carry one. The gap is set here so a page adds the
   <i> and nothing else. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--border-hair);
  border-radius: var(--radius-lg);
  padding: 6px 16px;
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: .5px;
  /* 18 + 12 padding + 2 border = 32. Declared in px rather than left at
     `normal`, which is where the dock buttons were: Safari gives a 13px label
     an 18px line box and Chrome gives it 13, so an undeclared button is 34 in
     one browser and 29 in the other. */
  line-height: 18px;
  transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}

.btn:hover:not([disabled]) {
  background: var(--text-strong);
  border-color: var(--text-strong);
  color: var(--text-inverse);
}

/* Tinted: a pale fill that names what the control belongs to, with the edge
   and the tick in the saturated version of the same hue. For a switch that is
   part of a lesson's own apparatus rather than page chrome, so it reads as
   belonging to the thing on stage. A page sets --btn-tint and --btn-edge. */
.btn--tint {
  background: var(--btn-tint);
  border-color: var(--btn-edge);
  color: var(--text-strong);
}

.btn--tint:hover:not([disabled]) {
  background: var(--btn-tint);
  border-color: var(--btn-edge);
  color: var(--text-strong);
  filter: brightness(.96);
}

/* A checkbox is not a door: it takes the border on hover and never the fill,
   because the ink fill is reserved for "this opens something".

   And it does not change when checked. The tick inside it is the state, in ink;
   the chip around the tick is only a label, and darkening it too says the same
   thing twice. glycolysis-lab renders it this way, though by accident rather
   than intent: it carries a `.chk:has(input:checked)` rule that would darken
   the border, dead at (0,2,1) under the `#showenz` rule's (1,0,0). Worth
   deleting there, because raising its specificity would silently undo this. */
.btn--check:hover:not([disabled]) {
  background: transparent;
  border-color: var(--text-strong);
  color: var(--text-strong);
}

.btn--check input {
  margin: 0;
  width: 16px;
  height: 16px;
  /* the tick takes the tint's edge colour when there is one, so the switch and
     the thing it switches are the same hue */
  accent-color: var(--btn-edge, var(--text-strong));
  cursor: pointer;
}

/* ---------- .checkgroup ---------- */
/* SWITCHES THAT BELONG TOGETHER ARE ONE CONTROL, so the box carries the border
   and the switches carry only their ticks. A stack of separately outlined chips
   is as many objects as it has outlines, and the eye counts them before it
   reads any of them.

   The rows are full width and left-aligned, so every tick sits on one vertical
   and the labels start together — the reason for grouping them in the first
   place. Hovering a row tints it instead of moving a border, because there is
   no border of its own left to move. */
.checkgroup {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  border: 1px solid var(--border-hair);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.checkgroup[hidden] { display: none }

.checkgroup > .btn {
  border: 0;
  border-radius: 0;
  width: 100%;
  justify-content: flex-start;
}

/* a hairline between rows, never above the first: the box's own edge is there */
.checkgroup > .btn + .btn { border-top: 1.5px solid var(--border-hair) }

.checkgroup > .btn:hover:not([disabled]) {
  background: color-mix(in srgb, var(--text-strong) 7%, transparent);
  border-color: var(--border-hair);
  color: var(--text-strong);
}

/* ---------- .iconbtn ---------- */
.iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-md);
  transition: color var(--dur-fast), transform var(--dur-fast);
}

.iconbtn:hover:not([disabled]),
.iconbtn.is-on { color: var(--text-strong) }

.iconbtn--solid {
  border: 1px solid var(--text-strong);
  background: var(--text-strong);
  color: #fff;
  font-size: var(--text-base);
}

.iconbtn--solid:hover:not([disabled]) {
  color: #fff;
  transform: scale(1.1);
}

/* ---------- .segmented ----------
   One control with N answers, drawn as the .cta capsule cut into pieces: same
   pill, same display face, same uppercase signage, and the chosen segment
   carries the accent fill a primary .cta carries. A reader should be able to
   see that pressing one of these and pressing the button beside it are the same
   kind of act.

   Not .seg: hemoglobin-lab already builds its progress bar out of elements it
   names 'seg', and a bare class here would have drawn a border round every
   segment of it.

   TWO SIZES, MATCHED TO .cta's TWO. The default stands 49px like a .cta, for a
   control set in a document beside one. `.segmented--sm` stands 34px like a
   .cta--sm, which is what an on-stage switch wants: a 2D/3D toggle at document
   size covers the molecule it is a toggle for. */
.segmented {
  --seg-pad-y: 16px;
  --seg-pad-x: 1.6rem;
  --seg-size: .95rem;
  --seg-fill: var(--accent);
  display: inline-flex;
  border: 1px solid var(--border-hair);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  /* The chosen segment's fill has to stop at the capsule's curve, and it is the
     container that owns that curve. */
  overflow: hidden;
}

.segmented--sm {
  --seg-pad-y: 9.6px;
  --seg-pad-x: 1rem;
  --seg-size: .8rem;
}

.segmented > * {
  border: 0;
  padding: var(--seg-pad-y) var(--seg-pad-x);
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-display);
  font-size: var(--seg-size);
  font-weight: var(--weight-medium);
  letter-spacing: .08em;
  text-transform: uppercase;
  /* line-height is 1 and stated in em, so both sizes come out to the height of
     the .cta they sit beside without either one restating a pixel. */
  line-height: 1;
  white-space: nowrap;
  transition: background var(--dur-base), color var(--dur-base);
}

.segmented > * + * { border-left: 1px solid var(--border-hair) }

.segmented > *:hover:not([disabled]):not([aria-pressed="true"]):not(.is-on) {
  color: var(--accent);
}

.segmented > *[aria-pressed="true"],
.segmented > *.is-on {
  background: var(--seg-fill);
  color: var(--text-inverse);
}
/* THE DIVIDER DIES INTO THE FILL. A hairline running down the side of a filled
   segment reads as a crack in it, so the chosen segment eats the rule on both
   of its edges. */
.segmented > *[aria-pressed="true"] + *,
.segmented > *.is-on + * { border-left-color: transparent }
.segmented > *[aria-pressed="true"],
.segmented > *.is-on { border-left-color: transparent }

/* The ring goes inside, because an outline on one joined segment otherwise
   draws over its neighbours. */
.segmented > *:focus-visible {
  outline: 0;
  box-shadow: inset 0 0 0 2px var(--accent);
}

/* ---------- .textbtn ---------- */
.textbtn {
  border: 0;
  padding: 2px 6px;
  background: none;
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: 18px;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.textbtn:hover:not([disabled]) { color: var(--text-strong) }


/* =============================================================================
   8. SHELL

   The two page shapes. #app is a full-bleed stage and nothing else, which is
   what most lessons are; #app-sidebar adds a right rail, and a page opts in by
   name so the rail is a choice rather than something it inherits.

   THE RAIL IS PLAIN. The paper the student sees is the stage's, and the rail
   lies on top of it. The graph-paper rail this replaced was switched off by
   hand on both of the pages the system was drawn from: glycolysis because an
   opaque column cuts a rectangle out of the scene behind it, and water-tutor
   because grid paper behind a launcher is decoration for a notebook that is
   not there any more. Two pages arriving at the same override independently is
   the system's answer, not a coincidence.
   ========================================================================== */
#app,
#app-sidebar {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  height: 100vh;
  overflow: hidden;
}

#app-sidebar { grid-template-columns: 1fr 372px }

/* The drag surface. Text on it is chrome, and a drag that selects a caption
   instead of orbiting the model is the worst kind of bug, so nothing here is
   selectable. */
#stage {
  grid-column: 1;
  position: relative;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
  background-color: var(--surface-page);
  background-image: radial-gradient(var(--surface-rule) 1.4px, transparent 1.4px);
  background-size: 26px 26px;
  background-position: 13px 13px;
}

#gl {
  display: block;
  width: 100%;
  height: 100%;
}

#side {
  grid-column: 2;
  grid-row: 1;
  position: relative;
  z-index: 8;
  display: flex;
  flex-direction: column;
  padding: 22px 30px 44px 34px;
  /* No ground of its own: the stage's paper reads through. */
  background: none;
  /* The rail takes the mouse only where its contents are, so a drag on the bare
     right-hand side still orbits the molecule underneath. */
  pointer-events: none;
}

#side > * { pointer-events: auto }

@media (max-width: 920px) {
  #app-sidebar {
    grid-template-columns: 1fr;
    grid-template-rows: 56vh 1fr;
  }

  #stage { grid-column: 1 }

  #side {
    grid-column: 1;
    grid-row: 2;
    padding: 20px;
    /* Stacked, the rail is a column of its own again and needs its own ground. */
    background-color: var(--surface-page);
    background-image: radial-gradient(var(--surface-rule) 1.4px, transparent 1.4px);
    background-size: 26px 26px;
    background-position: 13px 13px;
    overflow-y: auto;
  }
}

/* ---------- lesson chrome ---------- */
#brand {
  position: absolute;
  top: 22px;
  left: 26px;
  display: flex;
  align-items: center;
  gap: 11px;
  z-index: 6;
}

#brand h1 {
  margin: 0;
  font-family: var(--font-shout);
  font-weight: var(--weight-regular);
  font-size: 25px;          /* off-scale; --text-2xl is 26. Pending a type pass */
  letter-spacing: .3px;
}

#steps {
  display: flex;
  gap: 14px;
  align-items: center;
  margin-bottom: 22px;
}

#chips {
  position: absolute;
  left: 26px;
  bottom: 150px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: flex-start;
}

/* =============================================================================
   9. INSET

   A framed close-up of one molecule, over a stage drawn at another scale.
   Paired with kit/molbox.js, which owns the renderer and the fit and
   deliberately owns no CSS.

   THE BORDER IS LOAD-BEARING, which is why this is a component and not a
   page's own rule. An inset is a magnification: the molecule inside it is
   orders of magnitude larger than the same molecule in the scene behind.
   Without an edge it reads as an object standing in that scene at that
   size. The frame is what makes it a window instead of a false claim, so
   the honest version is the default one and a page has to work to lose it.

   Opaque paper, not a tint, for the same reason: a scene showing through a
   close-up puts two scales in the same pixels.
   ========================================================================== */
/* A COLUMN, not a canvas with things floating on it. The caption is a real
   row, so the canvas is given the space that is left and the molecule can
   never be drawn underneath the words — which is what an absolutely
   positioned label over a full-height canvas does, invisibly, until a taller
   molecule reaches the bottom of the box. kit/molbox.js re-solves its camera
   from a ResizeObserver, so the canvas being shorter than the frame costs
   nothing and is never something a caller has to subtract by hand. */
/* `display:flex` here is an AUTHOR rule, and author rules beat the UA
   stylesheet's `[hidden] { display: none }` outright — so giving this
   component a display value silently broke the `hidden` attribute on every
   box using it. Restated, because the symptom is a box that will not go
   away rather than anything that errors, and `el.hidden` still reads true
   the whole time, so a test asserting the PROPERTY passes while the thing
   sits on screen. */
.inset[hidden] { display: none; }
.inset {
  position: absolute;
  z-index: 4;
  display: flex;
  flex-direction: column;
  background: var(--surface-page);
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}
/* THE VIEW ROW IS THE CANVAS'S BOX, and that is its whole job. Callouts are
   positioned into this element, and annotate.js divides by the element's own
   clientHeight — so it has to be the canvas exactly, not the frame that also
   holds a caption. `position:relative` because the callout layer inside is
   absolute.

   min-height:0 or a flex child refuses to shrink below its content box and
   pushes the caption out of the frame — the flex default nothing warns about. */
.inset-view {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}
.inset-view canvas { display: block; width: 100%; height: 100%; }

/* The label rides INSIDE the frame. Outside, it is a caption floating over
   the scene with nothing tying it to the box it names. */
/* THE LEADER. Two lines from the frame's silhouette corners to a marked
   point in the scene: the exploded view a textbook draws, and the half of
   the device that says WHICH thing the window is onto. Sits UNDER the frame
   (z-index 3 against the frame's 4) so the lines run to the box's edge and
   stop there rather than crossing the molecule inside it.

   Never takes the mouse: it covers the whole stage, so a leader that did
   would swallow every click on the scene behind it. */
.inset-leader {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%; height: 100%;
  pointer-events: none;
  overflow: visible;
}
/* Filled, faintly. Two bare lines read as two unrelated rules across the
   scene; the wedge between them is what says "this region became that box". */
.inset-leader-wedge {
  fill: var(--clay-900);
  fill-opacity: .06;
  stroke: var(--clay-900);
  stroke-opacity: .55;
  stroke-width: 1.5;
  stroke-linejoin: round;
}
/* The ring around the thing itself. Without it the two lines converge on a
   bare point and the student has to guess how much of the scene was taken. */
.inset-leader-mark {
  fill: none;
  stroke: var(--clay-900);
  stroke-opacity: .75;
  stroke-width: 1.8;
}

/* A CALLOUT IN AN INSET IS A MINIATURE TOO. The label is thrown clear of the
   bond it names, and `.inset` clips — at the stage's own type size an 82 px
   label needs a ~290 px box to have anywhere to go, which is no longer a
   corner inset. Stepping the type down is what makes the throw affordable,
   and it is right anyway: the whole box is a scaled-down figure. */
.inset .annot-label {
  font-size: var(--text-2xs);
  padding: 2px 5px;
}
.inset .annot-dot { transform: scale(.8); }

/* THE CAPTION CARRIES THE POINT, not just the name. A callout inside an inset
   can hold a label but not a card: `.inset` clips, and a card wide enough to
   read is wider than the box it would open in. So the sentence a card would
   have hidden behind a click is printed here instead, under the title, where
   it is always visible and costs no interaction.

   LEFT-ALIGNED, because it is a sentence. Centring a title is fine; centring
   two lines of prose gives every line a different left edge to find. */
.inset .inset-cap {
  flex: 0 0 auto;
  padding: 6px 9px 8px;
  background: var(--surface-page);
  border-top: 1px solid var(--surface-rule);
  font-family: var(--font-ui);
  text-align: left;
}
.inset .inset-cap b {
  display: block;
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.inset .inset-cap span {
  display: block;
  margin-top: 3px;
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  line-height: var(--leading-snug);
  color: var(--text-strong);
}

/* =============================================================================
   10. SHAPES A TOKEN CANNOT REACH
   The sketchbook stated a few things in px — a 2px drawn edge, a tilt, a fold
   printed with a gradient. Each survives a repaint, so each is undone by name.
   pathways.css and water-tutor.css still load after this file on the lessons,
   which is why these are rules and not deletions.
   ========================================================================== */

/* -----------------------------------------------------------------------------
   10.1 SHAPE — the handful of places a token cannot reach.
   -------------------------------------------------------------------------- */

/* The dotted grid printed on the sketchbook paper. --surface-rule is transparent
   above, so the gradient already draws nothing; the image is dropped as well
   so the browser stops compositing a full-stage layer for it every frame. */
:root #stage,
:root .stage,
:root body { background-image: none }

:root .btn:hover:not([disabled]) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-inverse);
}

/* EVERY EDGE IN THE SYSTEM IS A HAIRLINE. The components in §7 state 1px
   themselves; these four are stated in px by pathways.css and annotate.css,
   which load after this file and are shared with the frozen benches, so they
   are corrected here rather than at source.

   The 2px drawn line was the sketchbook's signature and this is the last of it
   on a `body.kodo` page. Two 2px edges survive on purpose and neither is a
   sketchbook leftover: `.pagehead`'s bottom rule, which is not an element's
   edge but the one heavy line on a page, marking where its name stops
   (`.sechead`'s rule is 1px, and the two only read as a hierarchy while they
   differ); and the slider thumbs in `css/lesson-shell.css`, which belong to
   the other shell and are its call, not this sheet's. */
:root #savepng,
:root #restart,
:root #allsteps { border-width: 1px }

/* THE LESSON'S ONE INVITATION IS THE SITE'S PRIMARY. pathways.css paints #actbtn
   the H-bond blue so that Begin and water-lab's Add Water match, which was true
   when blue was the only colour the system had for "press this". It is now a
   substance's colour doing a primary's job, and the two claims are different.

   `initial` rather than the accent: --pill-fill is undeclared on purpose so
   that `var(--pill-fill, X)` can tell a page that named a colour from one that
   did not, and setting it here to the accent would win the rest state and lose
   the spark on hover. This puts the button back to having named nothing. */
:root #actbtn { --pill-fill: initial }

/* Micro-labels are the masthead's device: uppercase, letterspaced, quiet.
   Every heading on a converted page routes through one of these two, which is
   what keeps four lessons saying "this is a label" the same way. */
:root .cap-lg,
:root .cap-sm,
:root .label,
:root .caps,
:root h2.section {
  font-family: var(--font-display);
  font-weight: var(--cap-weight);
  text-transform: uppercase;
  letter-spacing: var(--cap-track);
  color: var(--text-muted);
}
:root .cap-lg { font-size: var(--cap-lg) }
:root .cap-sm,
:root .label,
:root .caps,
:root h2.section { font-size: var(--cap-sm) }

/* The display face, where a page wants the site's voice rather than prose:
   the masthead's micro-caps and any large numeral a lesson reads out. */
:root .caps,
:root .label,
:root h2.section { font-family: var(--font-display) }

/* -----------------------------------------------------------------------------
   10.2 PATHWAY CHROME
   pathways.css is loaded AFTER this file and still draws the sketchbook, so
   its pieces are converted here rather than there. The bench pages that also
   load pathways.css read bench.css instead and are untouched.
   -------------------------------------------------------------------------- */

/* The running ledger was a pinned yellow sticky — paper, a fold, a tilt. In
   this system it is a card lying flat on the page. The tilt has to be undone
   in three places, because the note ARRIVES rotated: the rule, the keyframe it
   animates to, and the fold drawn on ::after. */
:root .sticky {
  background: var(--surface-card);
  border: 1px solid var(--border-hair);
  border-radius: var(--radius-md);
  transform: none;
  box-shadow: var(--shadow-panel);
}
:root .sticky::after { display: none }
:root #ledger.pin { animation-name: ledger-rise }
@keyframes ledger-rise {
  from { opacity: 0; transform: translateY(-10px) }
  to   { opacity: 1; transform: none }
}

/* Off yellow, --muted and --hair resolve honestly again, so the heading and
   the rules stop needing the low-alpha ink pathways.css mixes for them. */
:root .sticky .lhead {
  font-family: var(--font-display);
  font-size: var(--cap-sm);
  font-weight: var(--cap-weight);
  letter-spacing: var(--cap-track);
  color: var(--text-muted);
}
:root .sticky .row { font-family: var(--font-ui); color: var(--text-body) }
:root .sticky .row .v { font-family: var(--font-display); font-weight: 600 }
:root .sticky .row.net,
:root .sticky .row.prod,
:root .sticky .row.carb { border-top: 1px solid var(--border-hair) }

/* The step's heading, and the rail it sits under. A 20px letterspaced marker
   was the page saying its own step number loudly; the micro-cap says it once. */
/* The camera's bottom reserve is a DECLARED --barh, so restyling this cannot
   re-frame the molecule — but it can walk the words up under it. The 20px
   marker overflowed pathways.css's 15px box downward and sat clear of the
   subject; a 12px cap in the same box does not, so the line box keeps the
   marker's height and the element's stays 15. */
:root #stephead {
  font-family: var(--font-display);
  font-size: var(--cap-sm);
  font-weight: var(--cap-weight);
  letter-spacing: var(--cap-track);
  line-height: 24px;
  color: var(--text-muted);
}
:root #stephead em { font-family: inherit; color: inherit }

/* The rail's numbers and the tooltip's marker: hairline, even corners. */
:root .rnum,
:root #stepsbody .rdot { border-width: 1px }
:root .infobtn { border-width: 1px }
:root .modal .mkick { font-family: var(--font-ui); font-size: 13px; font-weight: 500;
  letter-spacing: .22em; text-transform: uppercase; color: var(--brand-orange) }

/* The stage's right column: an aside, then a labelled datum. pathways.css
   sets them at 24 / 16 / 22px, which is three sizes close enough that nothing
   leads — and a label nearly as big as the thing it labels. The register does
   the separating here, so the sizes only have to step once each:
   the note is the tutor's voice (italic), the name is a fact (roman, blue),
   and the label shrinks to the micro-cap it always was. */
:root #enznote {
  font-family: var(--font-editorial);
  font-style: italic;
  font-size: 21px;
  line-height: 1.32;
  /* 170px broke "The split: 6 / carbons / become 3 + 3" — one word alone on
     the middle line. The wider measure sets it in two, and `balance` evens
     the rag on the notes that still take three. */
  max-width: 232px;
  text-wrap: balance;
  color: var(--text-dim);
  opacity: 1;
}
:root #enzname .ek {
  font-family: var(--font-display);
  font-size: var(--cap-sm);
  font-weight: var(--cap-weight);
  letter-spacing: var(--cap-track);
  color: var(--text-muted);
  margin-bottom: 5px;
}
:root #enzname .en {
  font-family: var(--font-editorial);
  font-size: 18px;
  line-height: 1.25;
  letter-spacing: -.01em;
  text-wrap: balance;
}

/* The lesson's name. pathways.css sets a 24px marker, hemoglobin had 16px at
   .02em and membrane an 11px cap — three treatments for one thing. */
:root #pagetitle,
:root #brand h1 {
  font-family: var(--font-display);
  font-size: var(--cap-lg);
  font-weight: var(--cap-weight);
  text-transform: uppercase;
  letter-spacing: var(--cap-track);
  color: var(--text-muted);
}

/* water-tutor.css writes its scrim in its own paper. Retoned to kodo's here:
   it has to be the paper it fades into or the gradient shows as a band lying
   across the molecules. */
:root #titleblock {
  background: linear-gradient(to bottom,
    rgba(250, 248, 244, .97) 0%, rgba(250, 248, 244, .88) 52%,
    rgba(250, 248, 244, 0) 100%);
}

/* The step's claim. membrane-lab's #cap is the same thing on the same shape of
   page, so it sets the same: serif at 17px, regular weight, emphasis carried
   by the <b> inside it rather than by the whole line. Literata has no 500,
   which water-tutor.css asked for and the browser was synthesising. */
:root #stepclaim {
  font-family: var(--font-editorial);
  font-size: 17px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--text-body);
}
:root #stepclaim b,
:root #stepclaim strong { font-weight: 600; color: var(--text-strong) }

/* The temperature readout's description. 19px at 1.05 was sized for Caveat,
   where a tight leading is the point; as a serif it is a paragraph with its
   lines touching. It describes the number above it, so it sets smaller. */
:root #treadout .tdesc {
  font-family: var(--font-editorial);
  font-size: 14px;
  line-height: 1.4;
  margin-top: 8px;
}

/* -----------------------------------------------------------------------------
   10.3 THE ENERGY CARD, THE NOTE, AND THE MODALS
   Same reason as §10.2: pathways.css and energy/energy.css both load after
   this file, so the conversion lives here.
   -------------------------------------------------------------------------- */

/* The coupling box was the second sheet of paper — ruled, tilted, with a
   lifted corner. Flat card, like the ledger. */
:root #couplebox {
  background: var(--surface-card);
  background-image: none;
  border: 1px solid var(--border-hair);
  border-radius: var(--radius-md);
  transform: none;
  box-shadow: var(--shadow-panel);
}
:root #couplebox::after { display: none }

/* The verdict headings. Serif caps at 14/1px was a third heading device; they
   are labels, so they take the micro-cap and keep only their colour. */
:root .note .nhead,
:root .eff .eh,
:root .massaction .ma-net span {
  font-family: var(--font-display);
  font-size: var(--cap-sm);
  font-weight: var(--cap-weight);
  letter-spacing: var(--cap-track);
}
:root .modal h2,
:root .massaction .ma-title {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--text-strong);
}

/* Prose in a card is prose. */
:root .note .nbody,
:root .modal .mtext,
:root .modal .msub,
:root .massaction .ma-sub {
  font-family: var(--font-editorial);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-body);
}
:root .note .nbody b,
:root .modal .mtext b,
:root .modal .msub b,
:root .massaction .ma-sub b { color: var(--text-strong) }

/* The door to the mass-action demo. #4f5bd5 into #7b3fe4 on hover was two
   colours the system does not have; it is the page's one link, so it takes
   the accent and a hairline underline. */
:root .note .minfo {
  font-family: var(--font-ui);
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
:root .note .minfo:hover { color: var(--spark) }

/* Panels inside a card: the tinted box and the modal's own shell. */
:root .eff {
  background: transparent;
  border: 1px solid var(--border-hair);
  border-radius: var(--radius-md);
}
:root .eff .e { font-family: var(--font-editorial); color: var(--text-dim) }
/* The modal is lessons.html's embed sheet: a navy backdrop, round corners,
   depth from a deep shadow rather than a border, and a floating white close. */
:root .modal .mback {
  background: rgba(var(--brand-ink-t), .55);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}
:root .modal .mcard {
  background: var(--surface-page);
  border: 0;
  border-radius: 20px;
  box-shadow: 0 40px 100px -30px rgba(var(--brand-ink-t), .6);
}
:root .modal .mclose {
  top: 12px; right: 14px; z-index: 2;
  width: 36px; height: 36px; padding: 0;
  border: 0; border-radius: 50%;
  background: #fff; color: var(--brand-ink);
  font-size: 22px; line-height: 1;
  box-shadow: 0 2px 10px rgba(var(--brand-ink-t), .2);
  transition: box-shadow .15s;
}
:root .modal .mclose:hover { color: var(--brand-ink); box-shadow: 0 3px 14px rgba(var(--brand-ink-t), .32) }

/* The rail's paper. The dotted grid is the sketchbook's; the bar is the same
   flat page as the stage. */
:root #railbar { background-image: none }
:root .rnum { font-family: var(--font-display); font-weight: var(--cap-weight) }
:root .rnum.rend { font-family: var(--font-display); letter-spacing: var(--cap-track) }
