/* =============================================================================
   bench.css — THE BENCH SHEET. The sketchbook look, and nothing public.

   FROZEN: no new rules. Forty-odd test pages and internal tools are drawn in
   it and none of them is a page a student reaches — the site sheet is
   kodo.css, and new work links that. A bench stays here because repainting it
   proves nothing and costs a day. Renamed from main.css when the protein
   gallery, the last public thing on it, took its own sheet.

   LOAD ORDER. This file first, then the page's own chrome:

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

   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.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Permanent+Marker&family=Zilla+Slab:wght@500;600;700&family=Caveat:wght@500;600;700&family=Nunito:wght@400;600;700&display=swap');

/* -----------------------------------------------------------------------------
   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],
.stickybtn[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 or in editorial.css: 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 */
  --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. --bond-ionic
     stays where it is because palette.js draws the bond with that number. */
  --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 that belong to a lesson rather than to an
   element, hoisted out of the three per-page :root blocks.
   -------------------------------------------------------------------------- */
:root {
  /* bonding, in molecule-builder. --bond-ionic is the ion-dipole amber and is
     the same number palette.js draws the bond with. --bond-covalent-ink is a
     darkened stand-in for it: the stick's stone (#b3a892) is tuned to read on
     cream at sphere size and goes illegible as 13px type. */
  --bond-ionic:        #d9791e;
  --bond-covalent-ink: #7a6f58;

  /* protein structure, in hemoglobin-lab. --protein-hbond is palette.js's
     hbond, so the ribbon and the word for it agree. */
  --protein-hbond: #0042aa;
  --protein-core:  #c2571b;
  --protein-coil:  #8a8378;
  --protein-quat:  #8a5a12;

  /* energy and carriers, in glycolysis-lab and krebs-lab. --energy-phosphate
     is palette.js's P and --energy-carbon is its C. */
  --energy-phosphate: #e07b1f;
  --energy-nad:       #2f7d5c;
  --energy-atp:       #c8452e;
  --energy-carbon:    #3a3a3a;

  /* the membrane's channel green, which membrane-lab repeats as a literal in
     six places and names nowhere */
  --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);

}

/* =============================================================================
   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).

     .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.
     .stickybtn  a note torn off and stuck on. The notebook, as a control.
     .segmented  segmented control, joined by shared borders.
     .textbtn    underlined text. Undo, dismiss, anything reversible and minor.

   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.
   ========================================================================== */

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

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

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

/* ---------- .pill ---------- */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border: 0;
  border-radius: var(--radius-pill);
  padding: 16px 32px;
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  line-height: 1;
  /* Solid is the default because an unqualified .pill on the stage is nearly
     always the thing the step is asking the student to press. A page sets
     --pill-fill to its own substance's colour and changes nothing else.
     The default charcoal is delivered as a var() fallback rather than a
     declaration, so the edge below can substitute a lighter starting colour
     for it: darkening ink by ink leaves no visible side. */
  /* 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-fill, var(--text-strong));
  color: #fff;
  /* 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-fill, var(--clay-600)) 68%, #241f1b) }
}

/* Hover thickens the edge, which stands the whole key up off the paper. */
.pill:hover:not([disabled]) {
  filter: brightness(1.07);
  --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. */
.pill--ghost {
  background: transparent;
  color: var(--text-muted);
  border: 2px solid var(--border-hair);
  --pill-rest: 0px;
  box-shadow: none;
  padding: 8px 16px;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  /* 14 + 16 padding + 4 border = 34, 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;
}

.pill--ghost:hover:not([disabled]) {
  filter: none;
  --pill-lift: 0px;
  color: var(--text-strong);
  border-color: var(--text-muted);
}

.pill--ghost[aria-current],
.pill--ghost.is-on {
  color: var(--text-strong);
  font-weight: var(--weight-semibold);
  border-color: var(--text-strong);
}

/* 14 + 16 padding = 30. .pill's line-height:1 would make it 29 off a 13px
   label, so the small pill states its own. */
.pill--sm { padding: 10px 20px; font-size: var(--text-sm); line-height: 14px; --pill-rest: 3px }

/* 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-fill, var(--text-strong)) }
  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-fill, var(--text-strong));
    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: 2px 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 + 4 border = 34. 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: 2px 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: 2px 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);
}

/* ---------- .stickybtn ---------- */
.stickybtn {
  /* inline-flex, not inline-block: the marker face's line box carries a
     fractional descender, and an inline-block button inherits it as an odd
     height that no amount of even padding can correct. */
  display: inline-flex;
  align-items: center;
  border: 2px solid var(--border-strong);
  border-radius: var(--radius-lg);
  padding: 6px 10px 8px;
  background: var(--surface-card);
  color: var(--text-strong);
  font-family: var(--font-shout);
  font-size: var(--text-md);
  line-height: 1;
  /* The one control that does not come out to an even height, and cannot be
     made to. Permanent Marker's ascent+descent is 1.1617em, so its content box
     is 20.91px at 18px, and Chrome adds that overflow to a button's box
     whatever you set: line-height, height, min-height and overflow:hidden were
     all measured and none of them move the .91. Every other button here is
     even; this one renders at 39 and is left alone rather than wrapped in an
     extra element to win a rounding argument. */
  /* Sat crooked, and it stays crooked through hover and press: a note that
     straightens itself when you touch it stops reading as a piece of paper. */
  --tilt: -.8deg;
  transform: rotate(var(--tilt));
  transition: background var(--dur-fast), transform var(--dur-fast), box-shadow var(--dur-fast);
}

.stickybtn:hover:not([disabled]) {
  background: #fff;
  transform: rotate(var(--tilt)) translateY(-1px);
}

.stickybtn:active:not([disabled]) {
  transform: rotate(var(--tilt)) translateY(2px);
  box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}

/* ---------- .segmented ----------
   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. */
.segmented {
  display: inline-flex;
  border: 2px solid var(--border-strong);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.segmented > * {
  border: 0;
  padding: 8px 14px;
  background: #fff;
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  /* 14 + 16 padding = 30, plus the container's own 4px border = 34. */
  line-height: 14px;
}

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

.segmented > *[aria-pressed="true"],
.segmented > *.is-on {
  background: var(--text-strong);
  color: var(--text-inverse);
}

/* 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(--clay-900);
}

/* ---------- .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` is the same surface without the column: a bench with TWO live
   boxes side by side lays them out itself, and restating the paper in the
   page would be a second copy of the dot grid to keep in step. */
#stage { grid-column: 1; }
#stage, .stage {
  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;
}

/* The wordmark's leading glyph — a Phosphor icon sized to sit with the h1. */
#brand .drop {
  font-size: 26px;
  color: var(--ink);
}

#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: 2px solid var(--clay-900);
  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);
}
