/* viewer.css — kcharter's interactive page.
 *
 * **The page opens in the reader's system scheme, like the front page, and
 * the toggle's choice wins over it.** The dark theme is here because this is
 * often read beside a terminal and a profiler, for long stretches, and a white
 * page next to those is the thing people complain about — but that is an
 * argument about a common reading and not about every reader, so what the
 * page opens in is the one signal it has about this reader, their desktop's
 * scheme, and the toggle in the bottom bar is for the reader whose page should
 * differ from it.
 *
 * **A theme is a block of tokens, never a second stylesheet.** `:root` below
 * is the dark set, the sheet's base; `:root[data-theme="light"]` redefines the
 * same names and nothing else, for a chosen light theme; and a media block
 * redefines them again under `:root:not([data-theme])` for a page given no
 * choice under a light desktop. The base block and the attribute block each
 * name one more selector, the gear's tile for that theme
 * (`#settings .tile[data-theme="…"]`), so a tile is painted in its own
 * theme's tokens whatever `<html>` is in — the same body, one selector more,
 * and no second copy of a palette. No rule under any of them names a colour, so
 * the themes can only differ where they are meant to. `data-theme` on `<html>`
 * means a choice and its absence means follow the system: the markup writes
 * none, the script writes it only for a remembered or a pressed choice, and a
 * page whose script never ran is still a themed page because this sheet, not
 * the script, is what reads the system.
 *
 * The palette is kernelize's, copied (not imported) from the kvik deck — deep
 * navy-black canvas, electric azure as the working accent, teal for "this is
 * the real work", amber for a caveat.
 *
 * **Colour encodes, or it is not spent.** Exactly one thing carries hue in the
 * canvas: a node's op category. Everything else — panels, rules, labels, the
 * breadcrumb — is greyscale, so the categories are the only thing competing for
 * the eye. Category hues are chosen so the two that matter most to a kernel
 * developer (matmul, attention) are the warmest marks on a cold ground, and the
 * two that are plumbing (shape, memory) are the coldest and dimmest.
 *
 * The light theme's category hues are **re-derived against that rule, not
 * inverted**. Inverting keeps the numbers and loses the rule: on white the
 * value ordering flips, and the two hues that were brightest become the two
 * that are hardest to see. So each category was picked again on a white
 * ground — the warm pair strongest in chroma, the plumbing pair the least
 * chromatic marks on the canvas. "Dimmest" is read there as *least chromatic*
 * rather than *palest*: a stroke pale enough to be dim by luminance on white
 * has stopped being a stroke, and the same box's category chip in the panel
 * has to stay readable. Every light hue clears its own box fill by more than
 * the dark theme's dimmest one does.
 */

#settings .tile[data-theme="dark"],
:root {
  /* canvas — kernelize */
  --bg:      #000717;
  --bg2:     #10162b;
  --bg3:     #161d33;
  --ink:     #eef1f6;
  --dim:     #9aa2b4;
  --faint:   #566079;
  --accent:  #0099ff;
  --accent2: #36aefc;
  --done:    #57d1b7;
  --warn:    #ffbc00;
  --loss:    #ff4100;
  --line:    #23294a;

  /* "present, not run" — a module the capture never reached. Deliberately a
   * desaturated slate and NOT --loss/--warn: nothing is wrong with an inert
   * block, it is simply outside what this capture executed. It sits below the
   * canvas's dimmest category (--cat-shape) in chroma, so it reads as backdrop
   * beside any real op while still being plainly a box you can open. */
  --inert:      #5a6480;
  --inert-fill: #0d1224;

  /* the heat bar's two ends — a scale of its own, green at nothing and red
   * at the whole, mixed by `.node rect.heat` at the ratio the script writes
   * to `--heat`. Its own green rather than --done: that hue means "an input
   * lit" on the same canvas, and a bar sharing it would read as a wire. */
  --heat-cold: #35c46b;
  --heat-hot:  var(--loss);

  /* op categories — the one encoding channel */
  --cat-matmul:      #ff9d3d;
  --cat-attention:   #ff6b4a;
  --cat-norm:        #b48ce8;
  --cat-activation:  #57d1b7;
  --cat-reduction:   #ffbc00;
  --cat-elementwise: #36aefc;
  --cat-shape:       #55607a;
  --cat-memory:      #7a6a8c;
  --cat-triton:      #00e5c0;
  --cat-param:       #3f6ea8;
  --cat-io:          #8d97ad;
  --cat-module:      #0099ff;
  --cat-other:       #9aa2b4;

  /* The three non-colour tokens a theme still has to own. A filter and a
   * shadow are as ground-dependent as a hex is: `brightness(1.5)` lifts a box
   * off a dark canvas and washes one out on a white one, and a black shadow
   * under a menu is a smudge on white. `--chip-ink` is what the panel's
   * category badge writes over a category hue, and `viewer.js` reads it by
   * name rather than carrying the dark theme's near-black as a literal. */
  --chip-ink:    #000717;
  --hover-lift:  brightness(1.5);
  --mark-filter: invert(1);

  /* A box's face is embossed: lit from above, so a bright band runs along its
   * top edge and the fill falls from there to a foot darker than the ground.
   * Tone only — steps of the greyscale ladder around `--bg2` (an op) and
   * `--bg3` (a block), never a hue — so the face says nothing the stroke does
   * not. */
  --face-hi:   #2b3559;
  --face-mid:  #171e38;
  --face-lo:   #070b19;
  --face3-hi:  #3a4670;
  --face3-mid: #1e2745;
  --face3-lo:  #0b1128;
  /* A stage frame's face is one flat step of the same ladder, between the
   * canvas and a block's ground: darker than the boxes' band, lighter than
   * their foot, so an embossed box still reads raised on it and the frame
   * still reads as ground against the canvas. */
  --frame-face: #0e1530;
  /* A loop box's face is the next step of the ladder past the frame's — a
   * run repeating inside a unit is a region of the region — still under
   * the boxes' band and over their foot. */
  --loop-face:  #141c3b;
  --emph-lift:   brightness(1.35);
  --menu-shadow: 0 8px 20px rgba(0, 0, 0, 0.55);

  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}

/* ----- the light theme -----
 *
 * The same names, under the attribute the bottom bar's toggle writes — a
 * chosen light theme; the media block after it is this set again for a light
 * desktop that chose nothing. Read the header first: this is a re-derivation
 * on a white ground, not the dark theme with its numbers turned round.
 *
 * The greyscale ladder is the dark one's mirror and keeps its *spacing*, so
 * `--faint` is as quiet beside `--dim` here as it is there. The accent goes
 * deeper rather than brighter, and `--accent2` — the accent's emphasis, one
 * step away from it — goes deeper again, because away from the ground is the
 * direction that separates on white. */
#settings .tile[data-theme="light"],
:root[data-theme="light"] {
  --bg:      #ffffff;
  --bg2:     #f4f6fb;
  --bg3:     #e8edf6;
  --ink:     #0b1020;
  --dim:     #4f5a72;
  --faint:   #79839a;
  --accent:  #0072c2;
  --accent2: #005b9e;
  --done:    #0a8a72;
  --warn:    #9a6400;
  --loss:    #c4320a;
  --line:    #d7dde9;

  /* "present, not run", on white: the same job — below the dimmest category in
   * chroma, plainly a box, nothing alarming about it. */
  --inert:      #8492ab;
  --inert-fill: #f1f3f8;
  --heat-cold: #1f9a4e;
  --heat-hot:  var(--loss);

  /* op categories — the one encoding channel, re-derived (see the header) */
  --cat-matmul:      #c25a00;
  --cat-attention:   #c2321a;
  --cat-norm:        #7040bd;
  --cat-activation:  #0e8a72;
  --cat-reduction:   #9a6f00;
  --cat-elementwise: #1a76c4;
  --cat-shape:       #7c8699;
  --cat-memory:      #8a7f9c;
  --cat-triton:      #00796a;
  --cat-param:       #4f7bb0;
  --cat-io:          #6b7484;
  --cat-module:      #0072c2;
  --cat-other:       #5f6878;

  /* A lift is *away from the ground*, which on white is downward; a shadow is
   * the page's own ink at low alpha rather than black. */
  --chip-ink:    #ffffff;
  --hover-lift:  brightness(0.94);
  --mark-filter: none;

  /* The light still comes from above: the band is the page's white and the
   * foot two steps into the ladder, so a raised box reads raised on either
   * ground. */
  --face-hi:   #ffffff;
  --face-mid:  #f2f4f9;
  --face-lo:   #dae0ec;
  --face3-hi:  #ffffff;
  --face3-mid: #e6ebf4;
  --face3-lo:  #ccd5e4;
  --frame-face: #eef2f8;
  --loop-face:  #e4e9f2;
  --emph-lift:   brightness(0.88);
  --menu-shadow: 0 8px 20px rgba(11, 16, 32, 0.18);
}

/* The same light set again, for the page that was given no choice under a
 * desktop that asked for light. A rule is a media condition and a selector,
 * and no one pair says "the system is light and nothing was chosen, or light
 * was chosen" — so this block and the one above are two rules with one body,
 * and `tests/test_schema.py` holds them equal to the token. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --bg:      #ffffff;
    --bg2:     #f4f6fb;
    --bg3:     #e8edf6;
    --ink:     #0b1020;
    --dim:     #4f5a72;
    --faint:   #79839a;
    --accent:  #0072c2;
    --accent2: #005b9e;
    --done:    #0a8a72;
    --warn:    #9a6400;
    --loss:    #c4320a;
    --line:    #d7dde9;

    /* "present, not run", on white: the same job — below the dimmest category in
     * chroma, plainly a box, nothing alarming about it. */
    --inert:      #8492ab;
    --inert-fill: #f1f3f8;
    --heat-cold: #1f9a4e;
    --heat-hot:  var(--loss);

    /* op categories — the one encoding channel, re-derived (see the header) */
    --cat-matmul:      #c25a00;
    --cat-attention:   #c2321a;
    --cat-norm:        #7040bd;
    --cat-activation:  #0e8a72;
    --cat-reduction:   #9a6f00;
    --cat-elementwise: #1a76c4;
    --cat-shape:       #7c8699;
    --cat-memory:      #8a7f9c;
    --cat-triton:      #00796a;
    --cat-param:       #4f7bb0;
    --cat-io:          #6b7484;
    --cat-module:      #0072c2;
    --cat-other:       #5f6878;

    /* A lift is *away from the ground*, which on white is downward; a shadow is
     * the page's own ink at low alpha rather than black. */
    --chip-ink:    #ffffff;
    --hover-lift:  brightness(0.94);
    --mark-filter: none;

    /* The light still comes from above: the band is the page's white and the
     * foot two steps into the ladder, so a raised box reads raised on either
     * ground. */
    --face-hi:   #ffffff;
    --face-mid:  #f2f4f9;
    --face-lo:   #dae0ec;
    --face3-hi:  #ffffff;
    --face3-mid: #e6ebf4;
    --face3-lo:  #ccd5e4;
    --frame-face: #eef2f8;
    --loop-face:  #e4e9f2;
    --emph-lift:   brightness(0.88);
    --menu-shadow: 0 8px 20px rgba(11, 16, 32, 0.18);
  }
}

* { box-sizing: border-box; }

/* The page's type is set by one number: the root's font size, 10px, and
 * every piece of chrome below is sized in `rem` of it — so the gear's type
 * step (`#settings`, below) writes one inline size on `<html>` and the whole
 * chrome follows. The canvas is the exception on purpose: a box's name and
 * meta line and a frame's title (`.node`, `.frame`, `.loopbox`, below) stay in `px`,
 * because the canvas is zoomed, not sized. */
html { font-size: 10px; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}
body { font-size: 1.6rem; }

#app { display: flex; flex-direction: column; height: 100%; }

/* ----- masthead: what model, what capture ----- */
header {
  display: flex;
  align-items: baseline;
  gap: 18px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--line);
  background: var(--bg2);
  flex: none;
}
/* The wordmark, first in the masthead, and the page's one link off the root:
   out to the kernelize.ai homepage, the site this page is a face for. Accent,
   because accent is what can be pressed on this page — the crumbs are the
   other one — and it is the accent the front page's nav spends on its own
   wordmark: the same blue on the navy ground, the deeper one on white, which
   is what `--accent` already resolves to under each theme. It is set in the
   site's weight rather than the masthead's so that it reads as the mark and
   not as a first crumb, and it stays a step under the model name in size: the
   model is what the reader came for. */
header .wordmark {
  font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em;
  color: var(--accent); text-decoration: none; flex: none;
}
header .wordmark:hover { color: var(--accent2); }
/* `Charter`, beside the wordmark: the way back up to the front page beside
   this one, so the pair reads as the site's mark and the product's button —
   one mark and one control, not two crumbs. It is an anchor drawn as a button,
   because it navigates: the bar's button shape (`#controls button`, below) in
   the accent the wordmark spends, so it sits with the mark and not with the
   greyed `up`. Pulled in under the header's gap so the two read as
   one unit, with the model name a full gap away. */
header .charter {
  font-size: 1.7rem; font-weight: 600; letter-spacing: 0;
  color: var(--accent); background: var(--bg3);
  border: 1px solid var(--accent); border-radius: 4px; padding: 2px 9px;
  text-decoration: none; flex: none; margin-left: -8px;
}
header .charter:hover { color: var(--accent2); border-color: var(--accent2); }
/* The Charter mark inside the button, before the word — `assets/charter.png`,
   committed beside this sheet and fetched through the page's own `img` so the
   rule that reads `src` sees it (the tab's icon is its vector twin, which
   colours itself by the browser's scheme and is not this sheet's). Black ink
   on a clear ground, standing a little taller than the label and sunk to
   centre on it, inverted to white under the dark theme — `--mark-filter`, a
   token, so the inversion follows the theme however it was reached. */
header .charter .mark {
  height: 1.4em; width: auto; margin-right: 6px; vertical-align: -0.3em;
  filter: var(--mark-filter);
}

/* The model name, which is also the picker (`#docname` below). The `<h1>` is
   the menu's positioning box and carries the type; the button inside it is
   transparent until it is hovered, so the masthead reads as a title first and
   as a control second — the reader who never switches documents should not be
   looking at a widget where the model's name goes. */
header h1 {
  font-size: 1.6rem; font-weight: 600; margin: 0; letter-spacing: -0.01em;
  position: relative; flex: none; min-width: 0;
}
header .warnpill {
  font-family: var(--mono); font-size: 1.1rem; color: var(--warn);
  border: 1px solid var(--warn); border-radius: 4px; padding: 1px 7px;
}
/* The masthead is filled from the document, so its slots ship empty and the
   pill is only ever shown on a capture that recorded an error. */
header .warnpill.hidden { display: none; }

/* ----- the bar: breadcrumb on the left, navigation on the right -----
 *
 * Only what moves the reader between levels lives here — the crumbs and `up`.
 * (The picker left it for the masthead: the model name is what pulls the
 * documents down.) What changes how the page is drawn — the theme, the
 * font size, the plumbing switch — is the gear's (`#view`, below); the key
 * to what the canvas draws is a pop-up on the canvas itself (`#legend-box`,
 * below). */
#bar {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 20px; border-bottom: 1px solid var(--line); flex: none;
  font-family: var(--mono); font-size: 1.2rem;
}
#crumbs { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
#crumbs .crumb {
  color: var(--accent); cursor: pointer; padding: 2px 6px; border-radius: 4px;
}
#crumbs .crumb:hover { background: var(--bg3); }
#crumbs .crumb.here { color: var(--ink); cursor: default; }
#crumbs .crumb.here:hover { background: none; }
#crumbs .sep { color: var(--faint); }

#controls { margin-left: auto; display: flex; align-items: center; gap: 14px; }
#controls button, #fields button {
  font-family: var(--mono); font-size: 1.2rem; color: var(--dim);
  background: var(--bg3); border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; cursor: pointer;
}
#controls button:hover, #fields button:hover {
  color: var(--ink); border-color: var(--accent);
}
/* Greyed while it has nothing to do: the reset, with every field at its
 * captured value. Inert to the hover as well, so it does not promise a press. */
#fields button:disabled, #fields button:disabled:hover {
  opacity: 0.45; cursor: default; color: var(--dim); border-color: var(--line);
}

/* ----- the details drop-down: the masthead's line drops the details -----
 *
 * The line at the masthead's right end reads what the page is sized at — the
 * regime, the batch and the length or the context, at the values the page
 * draws at — and is itself the button: a press drops a panel under it, over
 * the canvas: a column of name/value rows — the architecture, the op counts,
 * the capture's provenance and the cache, then the three fields — and the
 * reset last, with the tokens the picker's menu uses. A client of the page's one open-menu handle. The line is set as
 * the meta line always was — small mono in the faint tone — and is
 * transparent until pointed at, like `#docname`, boxed in the accent while
 * its panel is open. `#config` is the positioning box; the header sets no
 * overflow, so the panel is free to leave it, and the z-index puts it over
 * #main. `margin-left: auto` is what puts the line at the right end. */
#config { position: relative; flex: none; align-self: center; margin-left: auto; }
#config-head {
  font-family: var(--mono); font-size: 1.1rem; color: var(--faint); cursor: pointer;
  background: none; border: 1px solid transparent; border-radius: 4px;
  padding: 2px 7px; margin: 0; line-height: 1.4; white-space: nowrap;
}
#config-head:hover { color: var(--ink); background: var(--bg3); border-color: var(--line); }
#config-head:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
#config-head[aria-expanded="true"] {
  color: var(--ink); background: var(--bg3); border-color: var(--accent);
}
/* The panel: the facts and the fields one named row each, the reset —
 * anchored under the line's right edge so it grows leftward over the canvas
 * and never off the page. `width: max-content` for the reason `#legend`
 * gives: an absolutely positioned box shrinks to its containing block, which
 * here is the gear's own 26px. An input turns accent-edged while it differs
 * from the captured value; the captured value itself is not drawn — the
 * reset's hover names it. A step above the canvas's legend trigger
 * (`#legend-box`, z-index 40): the panel drops over the canvas's corner, and
 * a trigger showing through it read as part of it. */
#fields {
  display: none; position: absolute; right: 0; top: calc(100% + 8px);
  z-index: 50; width: max-content; max-width: min(420px, calc(100vw - 40px));
  flex-direction: column; align-items: flex-start; gap: 6px;
  padding: 10px 12px 11px; background: var(--bg3);
  border: 1px solid var(--line); border-radius: 5px;
  box-shadow: var(--menu-shadow);
  font-family: var(--mono); font-size: 1.2rem; color: var(--dim);
}
#config.open #fields { display: flex; }
/* Every row is a name and a value. A fact row pairs its name with plain
 * text — the regime, the architecture, the two op counts, the backend, the
 * torch, a tensor-parallel degree above one — and a
 * field row pairs its label with an input; the two share one name column,
 * so the values align down the panel whichever kind the row is. A fact the
 * document lacks is a hidden row, as a field it did not trace is. The facts
 * stand over a hairline from the fields: the rows above it are read, the
 * rows under it are moved — but the context on a prefill document, which
 * stands among the fields where its input stands on decode and is a fact
 * there, the model's declared window (`#context-fact`).
 * A value with no break point — an architecture such as
 * `Gemma4ForConditionalGeneration` — breaks anywhere it has to
 * (`overflow-wrap: anywhere`) rather than run past the padding to the
 * border: the panel's cap is the viewport's at a narrow width, so the width
 * cannot always follow the value, and the break keeps the name column and
 * the values aligned under it. */
#fields .facts {
  display: flex; flex-direction: column; gap: 4px; width: 100%;
  padding-bottom: 7px; margin-bottom: 2px; border-bottom: 1px solid var(--line);
}
#fields .fact, #fields .field {
  display: flex; align-items: baseline; gap: 6px; width: 100%;
}
#fields .field { align-items: center; }
#fields .fact.hidden, #fields .field.hidden { display: none; }
#fields .fact .name, #fields label { width: 13ch; flex: none; color: var(--dim); }
#fields label { cursor: text; }
#fields .fact .value {
  color: var(--ink); min-width: 0; white-space: normal; line-height: 1.5;
  overflow-wrap: anywhere;
}
/* The regime's value where the listing offers a choice: one button per
 * regime the record holds, the one showing marked in the accent — the
 * card's axis control, in this sheet's buttons — and a press on another
 * boots that capture into the page. Plain text where there is one. */
#fields .fact .value .choice { padding: 1px 8px; margin-right: 6px; }
#fields .fact .value .choice.chosen, #fields .fact .value .choice.chosen:hover {
  color: var(--accent-ink); border-color: var(--accent); background: var(--bg);
  cursor: default;
}
#fields input[type="number"] {
  font-family: var(--mono); font-size: 1.2rem; color: var(--ink);
  background: var(--bg2); border: 1px solid var(--line); border-radius: 4px;
  padding: 2px 6px; width: 7.5ch; -moz-appearance: textfield;
}
#fields input[type="number"]::-webkit-outer-spin-button,
#fields input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#fields input:focus { outline: none; border-color: var(--accent); }
#fields input.set { border-color: var(--accent); }
#fields .hint { color: var(--faint); font-size: 1.1rem; }
#fields #reset { margin-top: 2px; }

/* ----- the view settings: one gear, both pages -----
 *
 * The gear at the bar's right end drops the view settings — the theme
 * tiles, the font size and, on the viewer, the plumbing switch: how the page
 * looks, the same on every document and on the front page. The name column
 * is one width for every row, sized to the longest name, `font size`. These rules are the same text in
 * `index.css` and `viewer.css`, from this marker to the closing one, and
 * `tests/test_index.py` holds the two copies equal — neither page loads a
 * third sheet, so the share is a copy that cannot drift. The gear is an
 * inline glyph in `currentColor`, transparent until pointed at and boxed in
 * the accent while its panel is open; the panel drops under the gear's right
 * edge, over whatever is below, in the page's mono at its own size — a nav's
 * uppercase and tracking are undone here, because a setting is not a link.
 * The type buttons are never disabled: a step past either end lands the
 * size it started on, as the canvas's zoom steps do.
 *
 * The theme row is a tile per theme, each painted in that theme's own
 * colours whatever the page is in: the sheet's token blocks name the tile
 * beside `:root` (`.tile[data-theme="…"]`, above), so on the tile every
 * `var(--…)` is that theme's value — its ground, its ink, its line, and the
 * accent as a chip — and no colour is named here. The tile in force carries
 * `aria-pressed="true"`, drawn as an accent border. */
#view { position: relative; flex: none; align-self: center; }
#view-head {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0; cursor: pointer;
  color: var(--dim); background: none;
  border: 1px solid transparent; border-radius: 4px;
}
#view-head .gear { display: block; }
#view-head:hover { color: var(--ink); background: var(--bg3); border-color: var(--line); }
#view-head:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
#view-head[aria-expanded="true"] {
  color: var(--ink); background: var(--bg3); border-color: var(--accent);
}
#settings {
  display: none; position: absolute; right: 0; top: calc(100% + 8px);
  z-index: 50; width: max-content;
  flex-direction: column; align-items: stretch; gap: 6px;
  padding: 10px 12px 11px; background: var(--bg3);
  border: 1px solid var(--line); border-radius: 5px;
  box-shadow: var(--menu-shadow);
  font-family: var(--mono); font-size: 1.2rem; font-weight: 400;
  letter-spacing: 0; text-transform: none; color: var(--dim); text-align: left;
}
#view.open #settings { display: flex; }
#settings .setting { display: flex; align-items: center; gap: 6px; }
#settings .what { width: 9ch; flex: none; color: var(--faint); }
#settings button {
  font-family: var(--mono); font-size: 1.2rem; color: var(--dim);
  background: var(--bg2); border: 1px solid var(--line); border-radius: 4px;
  padding: 3px 10px; cursor: pointer; line-height: 1.2;
}
#settings button:hover { color: var(--ink); border-color: var(--accent); }
#settings #type-down, #settings #type-up { padding: 3px 8px; min-width: 3ch; }
#settings .tile {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--bg); color: var(--ink); border-color: var(--line);
}
#settings .tile::before {
  content: ""; width: 8px; height: 8px; border-radius: 2px; flex: none;
  background: var(--accent);
}
#settings .tile[aria-pressed="true"] {
  border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent);
}
/* ----- end of the view settings ----- */
/* The viewer's own row under the shared ones: the plumbing switch, a
 * checkbox whose state is whether plumbing is drawn — off bypasses it, and
 * off is what a fresh page opens in. Only this page draws plumbing, so the
 * row and this rule are its alone; the name column is a label, so the word
 * is the click target as well as the box. */
#settings input[type="checkbox"] {
  accent-color: var(--accent); margin: 0; width: 14px; height: 14px; cursor: pointer;
}
#settings label.what { cursor: pointer; }

/* ----- the document picker: the model name pulls it down -----
 *
 * The documents in reach hang off the thing the masthead already names, so the
 * page spends no chrome on a control that is a title away. Two transports, as
 * before — the manifest's siblings, and one the reader hands over, which is
 * the whole picker under file:// where a directory cannot be enumerated — and
 * now one surface: the listed documents as rows, the file entry always last.
 *
 * The trigger inherits the `<h1>`'s type and draws no box until it is pointed
 * at; the caret is the standing hint that there is something under it. A
 * button and a list, because a native <select> cannot be styled as a page's
 * title. */
#docname {
  font: inherit; color: var(--ink); cursor: pointer;
  background: none; border: 1px solid transparent; border-radius: 4px;
  padding: 1px 7px; margin-left: -7px;
  display: inline-flex; align-items: baseline; gap: 7px; max-width: 46ch;
}
#docname:hover { background: var(--bg3); border-color: var(--line); }
#docname:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
#docname .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#docname .caret {
  display: inline-block; color: var(--faint); font-size: 0.9rem;
  font-weight: 400; flex: none;
}
#docname[aria-expanded="true"] {
  background: var(--bg3); border-color: var(--accent);
}
#docname[aria-expanded="true"] .caret { transform: rotate(180deg); }

/* The menu drops out of the masthead and over the canvas. `header` sets no
   overflow of its own, so the absolutely-positioned list is free to leave it;
   the z-index puts it above #main, a later sibling that would otherwise paint
   on top. */
#docmenu {
  display: none; position: absolute; left: 0; top: calc(100% + 8px);
  z-index: 40; min-width: 100%; max-height: 60vh; overflow-y: auto;
  padding: 4px; background: var(--bg3);
  border: 1px solid var(--line); border-radius: 5px;
  box-shadow: var(--menu-shadow);
  font-family: var(--mono); font-size: 1.2rem; font-weight: 400;
  letter-spacing: 0;
}
#docmenu.open { display: block; }
#docmenu .entries { display: block; }
#docmenu .entry {
  display: block; width: 100%; text-align: left; white-space: nowrap;
  font: inherit; color: var(--dim); background: none; border: 0;
  border-radius: 3px; padding: 4px 9px; cursor: pointer;
}
#docmenu .entry:hover, #docmenu .entry.active {
  color: var(--ink); background: var(--bg2);
}
/* The document showing. It stays in the list rather than being dropped from
   it: a pull-down that hid the open document would read as a list of the
   others, and the reader would have no way to see what they are standing in. */
#docmenu .entry.here {
  color: var(--ink); box-shadow: inset 2px 0 0 var(--accent);
}
/* The file entry, last and set apart by a rule: everything above it is a
   sibling of this page, and this one is bytes from the reader's own disk. The
   same rule stands over the first model row on `topology.html`, which shares
   this sheet: the models under the machines there open the model viewer
   rather than that page. */
#docmenu .file, #docmenu .entry.first { margin-top: 4px; border-top: 1px solid var(--line);
  padding-top: 7px; border-radius: 0 0 3px 3px; }
/* The native file input is unstyleable and says "No file chosen" forever; the
   label around it is the entry, so the input itself never shows. */
#docmenu #docfile { display: none; }

/* ----- main split: canvas + detail panel ----- */
#main { display: flex; flex: 1; min-height: 0; }
#canvas { flex: 1; min-width: 0; position: relative; }
#canvas svg { display: block; width: 100%; height: 100%; cursor: grab; }
#canvas svg.dragging { cursor: grabbing; }

/* ----- the legend: a pop-up over the canvas's lower-right corner -----
 *
 * The key to the canvas's four encodings sits ON the canvas, because the
 * keys are the canvas's. A key is consulted, not read: the reader who knows the fills and
 * the line styles pays nothing for it, and the one who does not opens it
 * once. So it is a small trigger overlaid on the corner, transparent until
 * pointed at like the masthead's `#docname`, and the rows open upward from
 * it as a panel drawn with the same tokens the picker's menu uses — it is
 * a client of the page's one open-menu handle, not another way to open
 * something. The corner is the lower one: a panel at the bottom stands clear
 * of the crumbs and the masthead's drop-downs, which hang from the top. `#canvas` is the
 * positioning box; the svg is appended after this markup, but a positioned
 * box paints over in-flow content, and the z-index puts the panel over the
 * hover tip and the detail panel beside the canvas.
 *
 * The box is the canvas's corner column — one button wide, the legend's
 * trigger at its foot and the view controls (`#zoom`) stacked over it — so
 * the cluster takes one button's width of the canvas, and the pop-up,
 * anchored to the box's right edge above its top, rises past the buttons
 * along the same edge. */
#legend-box {
  position: absolute; bottom: 12px; right: 12px; z-index: 40;
  display: flex; flex-direction: column; align-items: stretch; gap: 2px;
  font-family: var(--mono); font-size: 1.2rem;
}
/* The column's four buttons share one face — the crosshairs, `+`, `−` and
 * the legend's map, top to bottom: one size, transparent until pointed at,
 * and the open state (the map's alone, in practice) accent-edged the way
 * the gear's is. The crosshairs frames the selected box and what it lights,
 * or the level; the two below step the transform the wheel and the drag
 * move, about the canvas's centre; the map opens the key. The glyphs are
 * inline SVG in `currentColor` like the config gear, so both themes hold
 * them, and the word `legend` is the map's label and title, not its face:
 * three icons over one word read as two kinds of control. None is ever
 * disabled: a step at the extent's end changes nothing and says so by
 * changing nothing, and the crosshairs always has a level to frame. */
#legend-box button {
  font: inherit; color: var(--dim); cursor: pointer;
  background: none; border: 1px solid transparent; border-radius: 4px;
  height: 24px; padding: 0 5px; font-size: 1.4rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
#legend-box button:hover {
  color: var(--ink); background: var(--bg3); border-color: var(--line);
}
#legend-box button:focus-visible {
  outline: 1px solid var(--accent); outline-offset: 1px;
}
#legend-box button[aria-expanded="true"] {
  color: var(--ink); background: var(--bg3); border-color: var(--accent);
}
#legend-box button svg { display: block; }
#zoom { display: flex; flex-direction: column; align-items: stretch; gap: 2px; }

/* The panel: the keys one under another, anchored above the trigger's
 * right edge so it grows upward and leftward over the canvas and never off
 * the page. `width: max-content` is load-bearing: an absolutely positioned
 * box shrinks to its containing block, which here is the cluster's own
 * width, and the entries would break inside; sized to its content, each
 * entry is one unbroken line. The cap is a height: a key read once is
 * scanned, and a column scans, so the panel scrolls past a tall level
 * rather than folding sideways. `legend()` builds into `#legend` exactly
 * as it did in the bar; only the element's home moved. */
#legend {
  display: none; position: absolute; right: 0; bottom: calc(100% + 7px);
  z-index: 40; flex-direction: column; align-items: flex-start; gap: 9px;
  width: max-content; max-height: calc(100vh - 220px); overflow-y: auto;
  padding: 9px 12px; background: var(--bg3);
  border: 1px solid var(--line); border-radius: 5px;
  box-shadow: var(--menu-shadow);
  font-size: 1.05rem; color: var(--dim);
}
#legend-box.open #legend { display: flex; }
#legend span { display: flex; align-items: center; gap: 5px; white-space: nowrap; }
#legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }

#empty {
  position: absolute; inset: 0; display: none;
  align-items: center; justify-content: center;
  color: var(--faint); font-family: var(--mono); font-size: 1.3rem;
}

/* A document that did not arrive. Loud on purpose: the failure it replaces is
   a blank canvas indistinguishable from a level with nothing at it. */
#fail {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px; padding: 0 40px;
  text-align: center; color: var(--dim); font-size: 1.3rem; line-height: 1.55;
}
#fail.hidden { display: none; }
#fail h2 {
  margin: 0; font-size: 1.5rem; font-weight: 600; color: var(--warn);
}
#fail .what { font-family: var(--mono); font-size: 1.2rem; color: var(--ink); }
#fail p { margin: 0; max-width: 62ch; }
#fail code {
  font-family: var(--mono); font-size: 1.2rem; color: var(--accent);
  background: var(--bg2); border-radius: 4px; padding: 1px 5px;
}

/* ----- nodes ----- */
.node { cursor: pointer; }
.node rect {
  rx: 5; stroke-width: 1.5;
  fill: url(#face);
  transition: filter 0.12s ease;
}

/* The two faces are gradients the script defines once in the canvas's `<defs>`
 * (`#face` over `--bg2` for an op, `#face-block` over `--bg3` for a block),
 * vertical and in each rect's own bounding box, so one fits every size. Their
 * stops are coloured here and nowhere else, from the theme's tokens, so a theme
 * flip re-tones the faces with the rest of the page. */
#face stop.hi        { stop-color: var(--face-hi); }
#face stop.mid       { stop-color: var(--face-mid); }
#face stop.lo        { stop-color: var(--face-lo); }
#face-block stop.hi  { stop-color: var(--face3-hi); }
#face-block stop.mid { stop-color: var(--face3-mid); }
#face-block stop.lo  { stop-color: var(--face3-lo); }
.node:hover rect { filter: var(--hover-lift); }
.node.selected rect { stroke-width: 2.5; }
.node text { font-family: var(--mono); pointer-events: none; }
.node .name { font-size: 11px; fill: var(--ink); }
.node .sub  { font-size: 9.5px; fill: var(--dim); }
.node .meta { font-size: 9px; fill: var(--faint); }

/* A block is a module you can descend into — heavier border, accent tint, and
 * a count badge. It must read as a *container*, not as one more op. */
.node.block rect { fill: url(#face-block); stroke-dasharray: none; stroke-width: 2; }
.node.block .name { font-size: 12.5px; font-weight: 600; }

/* Inert: in the model, never run by this capture. It keeps a block's shape —
 * it is still a door, and its weights are still real — but loses the accent
 * stroke and the bright label, so a level's live graph is what the eye finds
 * first and the inert row reads as context beside it. Muted, not alarming:
 * this is not an error state, and it is not dashed either, because dashed
 * already means "outside this level". */
.node.inert rect {
  fill: var(--inert-fill);
  stroke: var(--inert);
  stroke-width: 1.5;
  stroke-dasharray: none;
  opacity: 0.9;
}
.node.inert .name { fill: var(--dim); font-weight: 500; }
.node.inert .sub  { fill: var(--faint); }
.node.inert .meta { fill: var(--faint); }

/* External: on the other side of this level's boundary. Dimmed and dashed —
 * present so the wires make sense, not to be studied. */
.node.external rect { fill: none; stroke-dasharray: 4 3; opacity: 0.75; }
.node.external .name { fill: var(--dim); }

/* Plumbing, when the filter is on: still drawn (its wires still route) but
 * pushed all the way back. */
.node.dimmed { opacity: 0.16; }
.edge.dimmed { opacity: 0.08; }

/* A stage frame: a toned face under a border and a label, around one unit
 * of a stack stage at a stack level (`framed`). The face is `--frame-face`,
 * a flat step of the greyscale ladder between the canvas and a block's
 * ground — distinct from both, never a hue, like every mark that is not an
 * op category — and the border is thinner than a box's stroke, so the boxes
 * stay the things and the frame the ground they are grouped on. The title,
 * `stage N` at the top-left, is set larger than a block's name because it
 * names the group, not a member: it is read first, from further out. The
 * repeat stands alone at the top-right, larger again and in the accent — the
 * one number the frame is there to say, in the tone the page's own controls
 * wear, which is no op category's hue. A copy inside a frame is its member's
 * box, undistinguished: it IS that layer, at another position of the unit. */
.frame rect { fill: var(--frame-face); stroke: var(--faint); stroke-width: 1.2; rx: 8; }
.frame text { font-family: var(--mono); font-size: 14px; font-weight: 600; fill: var(--dim); }
.frame text.repeat { font-size: 20px; font-weight: 700; fill: var(--accent); text-anchor: end; }

/* A loop box: a run repeating inside a stage's unit, drawn once on a face
 * one step past the frame's, under a thinner border still — a region of the
 * region. Its title is its address, `0.1` — the stage's index, then its
 * index among the frame's loop boxes, a nested one appending its own — set
 * at the top-left in the frame title's face a step smaller, because it is
 * read after the frame's; its hover names its positions within the unit.
 * Its repeat stands at the top-right in the accent like the frame's,
 * smaller, so the two counts a reader multiplies read as the outer and the
 * inner. */
.loopbox rect { fill: var(--loop-face); stroke: var(--faint); stroke-width: 0.9; rx: 6; }
.loopbox text.title { font-family: var(--mono); font-size: 13px; font-weight: 600; fill: var(--dim); }
.loopbox text.repeat { font-family: var(--mono); font-size: 15px; font-weight: 700; fill: var(--accent); text-anchor: end; }

/* ----- edges ----- */
.edge { fill: none; stroke: var(--faint); stroke-width: 1.2; }
.edge:hover { stroke: var(--accent2); stroke-width: 2.5; }
.edge.hot { stroke: var(--accent); stroke-width: 2; }

/* ----- neighbourhood highlight (hovering a box, or having clicked one) -----
 *
 * Hovering a node answers "what feeds this, and what does it feed?" — the
 * question you ask constantly while reading a graph, and the one a static
 * picture answers worst once more than a few wires cross. A click *pins* the
 * answer: the same classes, carried by the selection rather than by the
 * pointer, so reading the panel or the pane it lit does not cost the picture.
 *
 * Direction is the whole point, so upstream and downstream get *different*
 * colours rather than one "related" colour: teal for what flows in, amber for
 * what flows out. Everything unrelated fades, so the neighbourhood is what is
 * left rather than something you hunt for. (The two hues are already spoken for
 * elsewhere — teal is `activation`, amber `reduction` — but a category fill and
 * a highlight stroke never appear on the same mark at the same time, and the
 * fade makes the highlight unambiguous.) */
.hovering .node:not(.lit-in):not(.lit-out):not(.lit-self) { opacity: 0.3; }
.hovering .edge:not(.lit-in):not(.lit-out) { opacity: 0.12; }

.edge.lit-in  { stroke: var(--done); stroke-width: 2.4; opacity: 1; }
.edge.lit-out { stroke: var(--warn); stroke-width: 2.4; opacity: 1; }

/* A lit box must read solid even when it is an `external` one, whose normal
 * styling is deliberately transparent and dashed. Being on the other end of the
 * hovered node's wire outranks being outside the level. */
.node.lit-in rect, .node.lit-out rect, .node.lit-self rect {
  opacity: 1;
  fill: url(#face-block);
  stroke-dasharray: none;
}
.node.lit-in  rect { stroke: var(--done); stroke-width: 2.5; }
.node.lit-out rect { stroke: var(--warn); stroke-width: 2.5; }
.node.lit-in  .name, .node.lit-in  .sub { fill: var(--done); }
.node.lit-out .name, .node.lit-out .sub { fill: var(--warn); }
.node.lit-in .meta, .node.lit-out .meta, .node.lit-self .meta { fill: var(--dim); }

/* The hovered box itself: brightest, and neither colour — it is the subject,
 * not an input or an output. */
.node.lit-self rect {
  stroke: var(--ink);
  stroke-width: 3;
  filter: var(--emph-lift);
}

/* ----- the pinned trace (a row of the op panel's call, clicked) -----
 *
 * The neighbourhood above answers "what feeds this box" wholesale: every
 * in-wire lights at once, and on a dense level `arg 0`'s wire and `arg 3`'s
 * are the same colour. Clicking the row narrows it to *that* operand — one
 * wire and the box at the end of it, picked out **within** the lit set rather
 * than instead of it, so the answer stays in its context.
 *
 * The accent, which is the page's "this is the thing you asked for" colour and
 * is neither of the two directional hues — the direction is already said by
 * the lit-in/lit-out underneath, and the trace is saying *which one*.
 *
 * Its own class rather than `.hot`: that one belongs to the edge hover and is
 * cleared on the hit path's `mouseleave`, so a pin sharing it would be rubbed
 * out by a pointer passing over the canvas. Written after every `lit-*` rule
 * above and at the same specificity, so it wins over them without !important
 * — and before the coverage mark below, which must stay last. */
.edge.traced { stroke: var(--accent); stroke-width: 3.4; opacity: 1; }
.node.traced rect {
  stroke: var(--accent);
  stroke-width: 3.4;
  opacity: 1;
  fill: url(#face-block);
  stroke-dasharray: none;
}
.node.traced .name, .node.traced .sub { fill: var(--accent); }

/* ----- the coverage mark -----
 *
 * A kerneldb key implements this box: a short tick inside its left edge. It is
 * deliberately *not* a colour — fill is the op category and that is the only
 * hue the canvas spends, so a second colour encoding over the same boxes would
 * be competing for the channel that already means something. Greyscale at the
 * `--dim` step: plainly there once you know to look, quiet enough that a page
 * where nearly everything is covered still reads as the graph first.
 *
 * Last in this section on purpose: `.node rect` and every lit-* rule above
 * select it too — same specificity, so what wins is what comes later. The tick
 * keeps its own fill and its own corner radius through a hover, a selection
 * and a neighbourhood light, all of which restyle "the box's rect". */
.node rect.keymark {
  rx: 1.25;
  fill: var(--dim);
  stroke: none;
  stroke-width: 0;
  opacity: 0.85;
  pointer-events: none;
}
.node.selected rect.keymark, .node.lit-self rect.keymark { fill: var(--ink); }

/* The heat bar (`heatBars`): a column standing at the right edge of every
 * priced op box and every module box — a faint track from just under the
 * name line to the box's foot and, over it, the bar rising from the foot.
 * The fill is the two heat tokens mixed at `--heat`, the percentage the
 * script writes on the element — the one place the canvas spends a second
 * hue over a box, and a scale rather than a class, so it stays a narrow
 * column and never reads as a second border. The
 * mix runs in oklch so the middle is a yellow rather than a grey. Beside the
 * keymark for the same reason it is: `.node rect` and the lit-* rules select
 * these too, and what comes later wins. */
.node rect.heat-track {
  rx: 1;
  fill: var(--line);
  stroke: none;
  stroke-width: 0;
  opacity: 0.9;
  pointer-events: none;
}
.node rect.heat {
  rx: 1;
  fill: color-mix(in oklch, var(--heat-hot) var(--heat, 0%), var(--heat-cold));
  stroke: none;
  stroke-width: 0;
  pointer-events: none;
}

/* A loop edge is the stack's recurrence: layer N's output feeding layer N+1's
 * input, which after the repeat-collapse points back at the same boxes. Dashed
 * amber so it reads as "this goes round again" rather than as a normal wire
 * drawn upward by mistake. */
.edge.loop { stroke: var(--warn); stroke-dasharray: 6 4; opacity: 0.7; }
.edgehit { fill: none; stroke: transparent; stroke-width: 11px; cursor: crosshair; }

/* ----- hover tooltip for a wire ----- */
#tip {
  position: absolute; pointer-events: none; opacity: 0;
  background: var(--bg3); border: 1px solid var(--accent);
  border-radius: 5px; padding: 5px 9px;
  font-family: var(--mono); font-size: 1.1rem; color: var(--ink);
  transition: opacity 0.1s ease; white-space: nowrap; z-index: 5;
}

/* ----- the memory pane -----
 *
 * Everything the viewed module's subtree costs to hold — its weights and its
 * KV cache — as a stacked memory column on the left, opposite the detail panel.
 * A fixed, narrow strip: it is a *scale*, and a scale whose width moves with
 * the window is not one — what varies is a block's height, which is the one
 * thing here that encodes.
 *
 * Height is bytes, at two nested levels: a section grows by its share of the
 * subtree's total, a block by its share of its section. `flex-grow` carries
 * both, so the column always fills exactly, whatever the window; `min-height`
 * keeps a norm scale beside a projection from collapsing to an unclickable,
 * unreadable hairline. That floor is the one place the encoding is knowingly
 * inexact, and it is worth it: a block nobody can read encodes nothing either.
 *
 * The `param` hue is the same one the weight nodes used to carry on the canvas,
 * on the left border only — the fill stays greyscale so the pane cannot compete
 * with the one channel the canvas spends on op category. */
#memory {
  width: 208px; flex: none; min-height: 0; display: flex; flex-direction: column;
  padding: 12px 12px 14px; overflow: hidden;
  border-right: 1px solid var(--line); background: var(--bg2);
}
#memory.hidden { display: none; }
/* The head is two lines like every face under it: the word over the number.
 * The split, the count and the level are its hover, so the face is what gets
 * quoted and the hover is what qualifies the quote. */
#memory .mhead { flex: none; margin-bottom: 10px; cursor: help; }
#memory .mhead b {
  font-size: 1.1rem; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--faint); font-weight: 600;
}
#memory .mhead .total {
  display: block; font-family: var(--mono); font-size: 1.6rem; color: var(--ink);
  font-weight: 600; margin-top: 3px;
}
/* The split under the total: the same bytes, said as the two things they are
 * bought as — a checkpoint and a context length. Dim, because the total is the
 * answer and this is its breakdown. */
/* Scrolls when the floor wins: a level with more weights than the column has
 * room for at 20px each keeps every one of them, shortest-first off the bottom,
 * rather than silently drawing fewer blocks than the head says there are. */
#memory .mcol {
  flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 2px;
  overflow-y: auto;
}
/* A section: the weights one op spends, under a head that names it and totals
 * them. It grows by its own share and its blocks divide what it got — the same
 * partition the flat column drew, with the op breakout laid over it. */
#memory .msec {
  display: flex; flex-direction: column; gap: 2px; min-height: 0;
}
#memory .msechead {
  flex: none; display: flex; justify-content: space-between; gap: 6px;
  padding: 5px 1px 2px; border-top: 1px solid var(--line);
  font-family: var(--mono); font-size: 1rem; color: var(--faint);
}
/* The top section needs no rule above it — but a listed divider does, even
 * where it is the only thing in the pane: the divider is what says the list
 * below it is outside the total above it. */
#memory .msec:first-child:not(.borrowed):not(.tied) .msechead {
  border-top: none; padding-top: 0;
}
#memory .mop {
  color: var(--ink); letter-spacing: 0.04em; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
#memory .msum { flex: none; font-weight: 600; }
/* The remainders read as remainders. `unread` is held memory the capture never
 * saw touched, so it stacks like any other section but says so in its head;
 * `borrowed` is somebody else's memory and `tied` a second name for bytes the
 * column already stacked, so neither stacks at all — each is a list below a
 * divider, sized by its content, outside the total at the top. */
#memory .msec.unread .mop { color: var(--faint); font-style: italic; }
/* The cache is not a weight and does not pretend to be one: its own section
 * under the op breakout, its blocks marked by the attention hue on the left
 * border — the one place a second colour earns itself here, because "these
 * bytes are the cache" is exactly the distinction the column is drawing. */
#memory .msec.kv .mop { color: var(--cat-attention); }
#memory .mblock.cache { border-left-color: var(--cat-attention); }
#memory .msec.borrowed,
#memory .msec.tied {
  flex: none; margin-top: 8px;
}
#memory .msec.borrowed .msechead,
#memory .msec.tied .msechead {
  border-top: 1px dashed var(--line); color: var(--faint); font-style: italic;
}
/* A block is the name over its size — the same two words as the divider
 * over it, the size the bold half, each on its own line so neither clips the
 * other. Everything else a block used to say is its hover. */
#memory .mblock {
  min-height: 20px; overflow: hidden; border-radius: 3px;
  background: var(--bg3); border-left: 3px solid var(--cat-param);
  padding: 3px 7px; cursor: default;
  display: flex; flex-direction: column; align-items: stretch; gap: 1px;
}
#memory .mblock.loan {
  flex: none; background: none; border-left-style: dashed;
  border-left-color: var(--line);
}
#memory .msec.unread .mblock { border-left-color: var(--line); }
/* A rolled-up bar is a *handle*: one bar is one child, and clicking it opens
 * that child. The pointer is the whole of the affordance — a second colour
 * here would compete with the cache hue for the one channel the pane spends.
 * The level's own bar is not a handle: there is no box on the canvas for the
 * module the reader is already standing in, so it says so in tone instead. */
#memory .mblock.bar { cursor: pointer; }
#memory .mblock.bar.own { cursor: default; border-left-style: dotted; }
#memory .mblock.bar.own .mname { font-style: italic; }
#memory .mname {
  flex: none; min-width: 0; font-family: var(--mono); font-size: 1.1rem;
  color: var(--ink); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}
#memory .msize {
  flex: none; font-family: var(--mono); font-size: 1.1rem; color: var(--ink);
  font-weight: 600; white-space: nowrap;
}
/* Lit: this weight is read by the box that is selected. The accent, and only on
 * demand — the pane says how much by default and who reads it when asked, which
 * is the trade the wires it replaces could not make. */
#memory .mblock.lit {
  background: var(--bg); border-left-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
#memory .mblock.lit .mname { color: var(--accent2); }

/* No selection-dependent region under the column. The pane is the level's, and
 * a selection only lights it — so the only thing a click may change here is
 * `.mblock.lit` above. What a tensor *is* the column says itself, on the bar
 * that is exactly that tensor; a bar over several is opened to reach them. */

/* ----- detail panel ----- */
#panel {
  width: 340px; flex: none; border-left: 1px solid var(--line);
  background: var(--bg2); overflow-y: auto; padding: 16px 18px;
  font-size: 1.25rem;
}
#panel.hidden { display: none; }
#panel h2 {
  font-family: var(--mono); font-size: 1.3rem; font-weight: 600;
  margin: 0 0 2px; word-break: break-all;
}
#panel .cat {
  display: inline-block; font-family: var(--mono); font-size: 1rem;
  text-transform: uppercase; letter-spacing: 0.09em;
  padding: 1px 7px; border-radius: 3px; margin-bottom: 14px;
}
#panel dl { margin: 0 0 16px; display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; }
#panel dt { color: var(--faint); font-size: 1.1rem; }
#panel dd { margin: 0; font-family: var(--mono); font-size: 1.1rem; word-break: break-all; }
#panel h3 {
  font-size: 1.1rem; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--faint); font-weight: 600;
  margin: 18px 0 7px; padding-top: 10px; border-top: 1px solid var(--line);
}
#panel table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 1.1rem; }
#panel td { padding: 2px 0; vertical-align: top; }
#panel td.k { color: var(--dim); padding-right: 10px; }
#panel td.v { color: var(--ink); text-align: right; white-space: nowrap; }
#panel .enter {
  display: block; width: 100%; margin-top: 14px; padding: 7px;
  font-family: var(--mono); font-size: 1.2rem; color: var(--bg);
  background: var(--accent); border: none; border-radius: 4px; cursor: pointer;
}
#panel .enter:hover { background: var(--accent2); }
#panel .none { color: var(--faint); font-style: italic; }

/* ----- the call's rows, as a two-way surface -----
 *
 * Under `inputs` and `outputs`, a row is a handle on the canvas: clicking it
 * pins that operand's wire (see `.traced` above). Both cells of the grid carry
 * the class, because the row is one thing to a reader even though the grid
 * lays it out as two.
 *
 * The affordance is the pointer, a key lifted one step out of `--faint`, and a
 * colour on hover — not a border or a chip: a detail panel is a list of facts
 * and 340px wide, and decorating every row with a control would cost more room
 * than the interaction is worth. A pinned row keeps the accent, so "which row
 * is the lit wire" is answerable without looking away from the canvas.
 *
 * `.inert` is a row with nothing on the canvas to light — an operand whose
 * producer this level did not draw, an `out` with no wire leaving, `consts`,
 * which are values the call carried and not wires into it. Every affordance
 * is written `:not(.inert)`, so an inert row is a plain row: the plain key
 * tone, the plain cursor, nothing on hover. Beside its live neighbours it does
 * not offer a click, rather than offering one that does nothing visible —
 * which is what reads as a broken page. Its *value* is untouched (an
 * unresolved operand keeps its `.none` italic), and where there is a reason to
 * give, the row's own hover has been giving it since the operand list
 * existed. */
#panel dl .trace { cursor: pointer; }
#panel dl .trace.inert { cursor: default; }
#panel dl dt.trace:not(.inert) { color: var(--dim); }
#panel dl dt.trace:not(.inert):hover,
#panel dl dd.trace:not(.inert):hover { color: var(--accent2); }
#panel dl dt.traced, #panel dl dd.traced { color: var(--accent); }
#panel dl dt.traced { font-weight: 600; }

/* A bypassed edge stands in for a chain of hidden plumbing ops rather than
 * naming one tensor. Dotted, so it does not read as a direct wire. */
.edge.bypassed { stroke-dasharray: 2 4; opacity: 0.5; }

/* ----- legend rows -----
 * Two encodings, two rows: node fill (op category) and line style (what kind of
 * connection). The line row exists because a dashed wire was previously
 * something you had to read the source to decode. The rows are the pop-up's
 * (`#legend`, with the canvas above), and each is a column: its heading
 * over its entries, one entry a line, because a key is scanned, not
 * skimmed, and a line per entry scans. */
.legend-row { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.legend-row > b {
  color: var(--faint); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.1em; font-size: 0.95rem; margin-bottom: 1px;
}
.legend-row .hint { color: var(--faint); }
.legend-row [title] { cursor: help; }

/* Line swatches: a short rule drawn with the same stroke the canvas uses. */
#legend i.line { width: 20px; height: 0; border-top-width: 2px; border-radius: 0; }
#legend i.line-solid    { border-top: 2px solid var(--faint); }
#legend i.line-loop     { border-top: 2px dashed var(--warn); }
#legend i.line-bypassed { border-top: 2px dotted var(--dim); }
#legend i.line-dashedbox {
  width: 18px; height: 10px; border: 1px dashed var(--dim); border-radius: 2px;
}
#legend i.line-frame {
  width: 18px; height: 10px; border: 1px solid var(--faint); border-radius: 3px;
  background: var(--frame-face);
}
#legend i.line-loopbox {
  width: 18px; height: 10px; border: 1px solid var(--faint); border-radius: 2px;
  background: var(--loop-face);
}
#legend i.line-inertbox {
  width: 18px; height: 10px; border: 1px solid var(--inert); border-radius: 2px;
  background: var(--inert-fill);
}

/* The coverage tick, at the size the canvas draws it — a swatch that is not the
 * mark is a key to something else. Only on an annotated page; see `legend()`. */
#legend i.keymark {
  width: 3px; height: 12px; border-radius: 1.5px; background: var(--dim);
}

/* The heat bar's swatch: the scale itself, cold to hot, at the bar's height.
 * Only on a page with a priced instance to draw; see `legend()`. */
#legend i.heat {
  width: 20px; height: 3px; border-radius: 1px;
  background: linear-gradient(in oklch to right, var(--heat-cold), var(--heat-hot));
}
