/*
 * Colour tokens for the dark-mode set: chart surface #15181b for the canvas,
 * page plane #0d0d0d for the chrome. The graph's own palette — the cool green
 * heat ramp and the amber hover accent — lives in js/graph.js, which paints to
 * canvas and cannot read CSS variables; nothing in the chrome restates it.
 */

:root {
  --surface:      #15181b;   /* graph canvas */
  --plane:        #0d0d0d;   /* panels */
  --plane-2:      #131416;
  --ink:          #f1f6f9;
  --ink-2:        #b6c1c8;
  --ink-muted:    #858f97;
  --line:         #282c30;
  --line-strong:  #353a3f;
  --accent:       #3987e5;

  --radius:       7px;
  --sidebar-w:    282px;
  --detail-w:     372px;
  /* How tall the corner mark stands, for the panel it sits over to keep clear
     at the foot of its scroll. */
  --mark-h:       56px;

  color-scheme: dark;
}

/* The only face the app loads. It is used for exactly one thing — the Ra 235
   wordmark in the corner — so it is not in any inherited font stack, and it is
   allowed to swap: the mark arriving a frame late is nothing, the chrome
   blocking on a 220 kB file is not. The file sits at the project root under its
   shipped name, space and all. */
@font-face {
  font-family: "Type Machine";
  src: url("../Type%20Machine.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* The other face the mark loads, for the barcode beside it. This one does not
   get to swap: every glyph in it is a run of bars, so a fallback would set the
   letters L-Y-S-F-L-A-T-H stretched across the barcode's box until the file
   landed. Blocking hides the code until it can be drawn properly, which at 22 kB
   is a frame or two. */
@font-face {
  font-family: "Libre Barcode 128";
  src: url("../LibreBarcode128-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

* { box-sizing: border-box; }

/* `hidden` is the UA's `display: none`, so any author rule that sets `display`
   outranks it — `.icon-btn`'s `inline-flex` left every `hidden` icon button on
   screen, the search clear ✕ included. This is the one place !important earns
   its keep: the attribute has to beat whatever the class says. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--plane);
  color: var(--ink-2);
  font: 13px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
  overscroll-behavior: none;
  /* The page itself never scrolls — the shell is exactly the viewport and every
     panel that needs to scroll does it inside itself. Without this, #app's
     100dvh rounds a fraction of a pixel past body's 100%, and one stray pixel
     of overflow is enough for a full-height scrollbar down the right edge. It
     is worst in game mode, where the board runs edge to edge and the bar is the
     only thing over there. */
  overflow: hidden;
}

h1, h2, h3 { margin: 0; font-weight: 600; color: var(--ink); }

/*
 * Both side panels fold away. The widths are written out per state rather
 * than driven through a custom property because a transition on
 * grid-template-columns interpolates concrete track lengths; routing them
 * through var() is not reliably animatable across engines.
 */
#app {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr) var(--detail-w);
  /* The single row must be the viewport, not the tallest panel's content.
     Left implicit it sizes to max-content — the sidebar unscrolled is about
     1650px — and the stage grows with it, which puts anything positioned
     against the stage's bottom or middle below the fold. */
  grid-template-rows: minmax(0, 1fr);
  height: 100vh;
  height: 100dvh;
  transition: grid-template-columns 190ms cubic-bezier(0.4, 0, 0.2, 1);
}
#app.sidebar-off { grid-template-columns: 0 minmax(0, 1fr) var(--detail-w); }
#app.detail-off  { grid-template-columns: var(--sidebar-w) minmax(0, 1fr) 0; }
#app.sidebar-off.detail-off { grid-template-columns: 0 minmax(0, 1fr) 0; }

@media (prefers-reduced-motion: reduce) {
  #app { transition: none; }
}

/* ------------------------------------------------------------- structure */

#sidebar, #detail {
  background: var(--plane);
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* The track squeezes to nothing, which reflows the contents on the way.
     Fading first means that reflow happens behind an already-blank panel;
     visibility then lands at the end of the fold, which is what takes the
     folded panel out of the tab order and out of find-in-page. */
  overflow: hidden;
  transition: opacity 110ms ease, visibility 0s;
}
#sidebar { border-right: 1px solid var(--line); }
#detail  { border-left: 1px solid var(--line); }

#app.sidebar-off #sidebar, #app.detail-off #detail {
  opacity: 0;
  visibility: hidden;
  transition: opacity 90ms ease, visibility 0s linear 190ms;
}
#app.sidebar-off #sidebar { border-right-width: 0; }
#app.detail-off  #detail  { border-left-width: 0; }

.pane-scroll {
  overflow-y: auto;
  overflow-x: hidden;
  flex: 1;
  min-height: 0;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
.pane-scroll::-webkit-scrollbar { width: 9px; }
.pane-scroll::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 5px;
  border: 2px solid var(--plane);
}

.brand {
  padding: 15px 16px 13px;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: flex-start;
  gap: 10px;
}
.brand-text { min-width: 0; flex: 1; }
.brand h1 { font-size: 14px; letter-spacing: -0.01em; }
.brand-sub { margin: 3px 0 0; font-size: 11.5px; color: var(--ink-muted); }

/* The detail pane had no chrome of its own; it needs a strip to hang its
   collapse control off, and the strip may as well say what the pane is. */
.detail-head {
  padding: 15px 16px 13px;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 10px;
}
.detail-head h2 {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.075em;
  color: var(--ink-muted);
  font-weight: 600;
}

.block {
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
}
.block h2 {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.075em;
  color: var(--ink-muted);
  font-weight: 600;
}
.block-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 9px;
}
.field-label {
  display: block;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.075em;
  color: var(--ink-muted);
  font-weight: 600;
  margin-bottom: 7px;
}
.hint { margin: 7px 0 0; font-size: 11.5px; color: var(--ink-muted); }

/* ---------------------------------------------------------------- inputs */

#lens {
  width: 100%;
  appearance: none;
  background: var(--plane-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  font-size: 12.5px;
  padding: 7px 9px;
  cursor: pointer;
}
#lens:focus { outline: none; border-color: var(--accent); }
#lens optgroup { background: var(--plane); color: var(--ink-muted); font-style: normal; }
#lens option { background: var(--plane); color: var(--ink); }

.search-wrap { position: relative; }
#search {
  width: 100%;
  appearance: none;
  background: var(--plane-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  padding: 7px 28px 7px 10px;
}
#search::placeholder { color: #6a6a64; }
#search:focus { outline: none; border-color: var(--accent); }
#search::-webkit-search-cancel-button { display: none; }
#searchClear {
  position: absolute;
  right: 4px; top: 50%;
  transform: translateY(-50%);
  width: 22px; height: 22px;
  font-size: 15px;
  line-height: 1;
}

.icon-btn {
  appearance: none;
  background: var(--plane-2);
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  color: var(--ink-2);
  font: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.icon-btn:hover { color: var(--ink); border-color: #4a4a46; }
.icon-btn.is-off { opacity: 0.5; }

.pane-toggle {
  flex: 0 0 24px;
  width: 24px; height: 24px;
  font-size: 15px;
  line-height: 1;
  background: none;
  border-color: transparent;
  color: var(--ink-muted);
}
.pane-toggle:hover { background: var(--plane-2); border-color: var(--line-strong); }

.link-btn {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  color: var(--ink-muted);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link-btn:hover { color: var(--ink); }

/* --------------------------------------------------------- checks & list */

.checks { list-style: none; margin: 0; padding: 0; }
.checks li { margin: 0; }
.checks label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 2px;
  cursor: pointer;
  font-size: 12px;
  border-radius: 4px;
}
.checks label:hover { background: rgba(255,255,255,0.04); }
.checks input[type=checkbox] {
  appearance: none;
  width: 13px; height: 13px;
  flex: 0 0 13px;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  background: var(--plane-2);
  cursor: pointer;
  position: relative;
}
.checks input[type=checkbox]:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.checks input[type=checkbox]:checked::after {
  content: "";
  position: absolute;
  left: 4px; top: 1px;
  width: 3px; height: 7px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* A box that is a cut of the one above it has nothing to do while that one is
   unticked. It stays where it is and stays readable — it is still the answer to
   "can I do this", and moving or hiding it would make the list jump every time
   its parent was toggled — but it stops offering the hover it cannot honour. */
.checks li.is-off label { opacity: 0.42; cursor: default; }
.checks li.is-off label:hover { background: none; }
.checks li.is-off input[type=checkbox] { cursor: default; }

.checks .count {
  margin-left: auto;
  font-size: 10.5px;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}
.checks.standalone { margin-top: 10px; }

.acc { border-top: 1px solid var(--line); }
.acc:first-of-type { border-top: 0; }

/* The context lens is a tab like the rest of the filters, but the thing inside
   it is a control rather than a list of checkboxes, so it needs the padding the
   .checks rows carry themselves. */
.filter-field { padding: 1px 2px 10px; }
.acc summary {
  cursor: pointer;
  padding: 7px 2px;
  font-size: 12px;
  color: var(--ink-2);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 6px;
}
.acc summary::-webkit-details-marker { display: none; }
.acc summary::before {
  content: "›";
  display: inline-block;
  transition: transform 0.12s ease;
  color: var(--ink-muted);
}
/* Its own summary, not every summary under it: Display holds tabs now, and a
   descendant selector here turned each of their chevrons the moment the block
   they sit in was opened — folded tabs pointing down at nothing. */
.acc[open] > summary::before { transform: rotate(90deg); }
.acc summary:hover { color: var(--ink); }
/* What a folded tab still says about itself, so nothing has to be opened to be
   read. It never wraps: a second line would push the summary's own baseline off
   the row of tabs it sits in. */
.acc-note {
  margin-left: auto;
  padding-left: 8px;
  font-size: 10.5px;
  color: var(--ink-muted);
  white-space: nowrap;
}
.acc > ul { padding-bottom: 8px; }

/* A whole block folded into one tab rather than a tab inside a block: the
   summary is that section's heading, so it wears the heading's type and sits on
   the block's own top edge. Only the chevron and the note give it away as a
   fold, which is the point — it is still a peer of the headings above and below
   it. The note keeps its own case: uppercased, "all" reads as an initialism. */
.acc-block > summary {
  padding: 0 2px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.075em;
  color: var(--ink-muted);
  font-weight: 600;
}
.acc-block[open] > summary { padding-bottom: 9px; }
.acc-block > summary:hover { color: var(--ink-2); }
.acc-block > summary .acc-note {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}
/* The block's reset, in the summary because a block folded into a tab has no
   heading row to hang it off. It sits after the note, where a block's reset
   sits, and drops the heading's type — a reset is a control, not part of the
   name. app.js keeps the click off the fold. */
.acc-block > summary .link-btn {
  margin-left: 4px;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}
/* A tab that ends in a note rather than a list: the list below it already
   carries the tab's bottom padding, so this only closes the gap above. */
.acc > .hint { margin-top: 0; padding: 0 2px; }

/* Tabs inside a block that is itself a tab — the layout sliders, folded under
   Display. The hairline is what tells the two of them from the boxes above,
   so the first one keeps the border .acc:first-of-type takes off a block's
   top tab: here there is content above it, not the block's own edge. The hint
   it follows has to stand off that line rather than sit on it.

   Only where something is above it, though, which is what the `ul ~` is for:
   Filter is a block-tab whose first child is a tab, and a hairline there would
   be a rule drawn under the block's own heading. */
.acc-block > ul ~ .acc:first-of-type { border-top: 1px solid var(--line); }
.acc-block > .hint { padding-bottom: 10px; }

/* ------------------------------------------------------------- sliders */

.sliders { list-style: none; margin: 0; padding: 0; }
.slider { margin: 0 0 11px; }
.slider:last-child { margin-bottom: 3px; }
.slider-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 0 2px 4px;
  font-size: 12px;
  cursor: pointer;
}
/* The number is the whole reason the row can be read at a glance, so it is
   tabular: proportional digits shuffle the value sideways as it is dragged and
   the eye follows the movement instead of the number. */
.slider-val {
  margin-left: auto;
  font-size: 10.5px;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.sliders input[type=range] {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  height: 14px;
  margin: 0;
  padding: 0;
  background: none;
  cursor: pointer;
}
/* Track and thumb have to be written twice over: a rule listing both vendors'
   pseudo-elements in one selector is dropped whole by either engine. */
.sliders input[type=range]::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 2px;
  background: var(--line-strong);
}
.sliders input[type=range]::-moz-range-track {
  height: 3px;
  border-radius: 2px;
  background: var(--line-strong);
}
.sliders input[type=range]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 12px; height: 12px;
  margin-top: -4.5px;          /* centres the thumb on a 3px track */
  border-radius: 50%;
  border: 0;
  background: var(--ink-2);
}
.sliders input[type=range]::-moz-range-thumb {
  width: 12px; height: 12px;
  border-radius: 50%;
  border: 0;
  background: var(--ink-2);
}
.sliders input[type=range]:hover::-webkit-slider-thumb { background: var(--ink); }
.sliders input[type=range]:hover::-moz-range-thumb { background: var(--ink); }
.sliders input[type=range]:focus { outline: none; }
.sliders input[type=range]:focus-visible::-webkit-slider-thumb {
  background: var(--accent);
  box-shadow: 0 0 0 3px rgba(57,135,229,0.28);
}
.sliders input[type=range]:focus-visible::-moz-range-thumb {
  background: var(--accent);
  box-shadow: 0 0 0 3px rgba(57,135,229,0.28);
}

/* search results */
.results { list-style: none; margin: 9px 0 0; padding: 0; }
.results li { margin: 0 0 1px; }
.results button {
  display: block;
  width: 100%;
  text-align: left;
  appearance: none;
  background: none;
  border: 0;
  border-radius: 5px;
  padding: 5px 7px;
  color: var(--ink-2);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.results button:hover { background: rgba(255,255,255,0.06); color: var(--ink); }
.results .r-sub {
  display: block;
  font-size: 10.5px;
  color: var(--ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ----------------------------------------------------------------- stage */

#stage { position: relative; background: var(--surface); min-width: 0; }
#canvas { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }

.stage-controls {
  position: absolute;
  top: 12px; right: 12px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.stage-controls .icon-btn {
  width: 30px; height: 30px;
  font-size: 15px;
  background: rgba(13,13,13,0.86);
  backdrop-filter: blur(6px);
}
/* The wand is the one mark in this column that is drawn rather than typed, so
   it is sized against the ink of the glyphs beside it — 14px, which is about
   what ⤢ and ❚❚ actually cover — rather than against their font size. It fills
   its box where a glyph leaves side bearings, and a wand matched to the em
   would read as the largest thing in the column. */
#animate .wand {
  display: block;
  width: 14px; height: 14px;
}
/* Stopping is the same kind of mark as the rest of the column and stays typed;
   only one of the two is ever showing. */
#animate .stop-mark {
  display: none;
  font-size: 15px;
  line-height: 1;
}
#animate.is-drawing .wand { display: none; }
#animate.is-drawing .stop-mark { display: block; }
/* Edge handles for the folded panels. Vertically centred rather than tucked
   under .stage-controls: they are the way back to a panel that is no longer
   on screen, so they want the least crowded part of the edge they belong to. */
.pane-reopen {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 5;
  width: 22px; height: 46px;
  font-size: 15px;
  background: rgba(13,13,13,0.86);
  backdrop-filter: blur(6px);
}
.pane-reopen-left  { left: 0;  border-left-width: 0;  border-radius: 0 7px 7px 0; }
.pane-reopen-right { right: 0; border-right-width: 0; border-radius: 7px 0 0 7px; }

/* ----------------------------------------------------------- author mark */

/* Fixed to the window rather than to a panel, because the panel it sits over
   can be folded away and the mark should not go with it. No plate and no
   border: it rides the panel behind it and reads as chrome, not as a button —
   the only thing that happens on hover is the ink coming up. */
.author-mark {
  /* The one size to turn. It lives up here rather than on .wordmark because the
     barcode is cut to the wordmark's metrics — its height and its seat on the
     baseline are both fractions of this — and the two are siblings. */
  --wm: 21px;
  position: fixed;
  bottom: 0; right: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  /* Wide enough to clear the digits, which sit a hair above the wordmark's own
     box — the exponent is out of flow, so the line above it does not get
     pushed up by it and the gap has to allow for it. */
  gap: 7px;
  padding: 8px 15px 10px;
  color: var(--ink-muted);
  text-decoration: none;
  transition: color 140ms ease;
}
.author-mark:hover,
.author-mark:focus-visible { color: var(--ink-2); }
.author-mark:focus-visible { outline: 1px solid var(--accent); outline-offset: -2px; border-radius: var(--radius); }
/* Train mode is the whole screen and has no chrome; the mark goes with it. */
#app.game-on .author-mark { display: none; }
/* The mark sits over the foot of the detail pane, so the pane's scroll ends
   above it: the last thing in the panel can always be scrolled clear of the
   mark, and the mark never covers anything. Nothing else lives down in that
   corner — the stage's own status line is at the other end of the edge — so
   there is nothing to move out of the way when the pane folds. */
#detail .pane-scroll { padding-bottom: var(--mark-h); }

.author-line {
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.085em;
  line-height: 1;
  white-space: nowrap;
}

/* The wordmark. --wm is the one size to turn: everything under it, the
   exponent's offsets included, is a fraction of it.

   The digits are anchored to the a rather than set after it, which is the whole
   point of splitting the two letters into their own spans: positioned inside
   the a, the exponent is placed against that letter's own origin, so where the
   2 lands is a fact about the a and not about how wide the R happens to be. */
/* The mark's second row: the wordmark at its own width, the barcode taking
   whatever is left. width: 100% is what squares the block off — the row is as
   wide as the column, and the column is as wide as the line of text above, so
   the two rows always end together.

   The code is flexed from a basis of 0 so it never gets a say in how wide the
   mark is: left to its natural width it would push the block past the line
   above and the rectangle would break the other way. Ends aligned, because the
   bars are seated on the wordmark's baseline, not on its box. */
.mark-row {
  display: flex;
  align-items: flex-end;
  width: 100%;
  /* The code's quiet zone — enough daylight that the opening bracket does not
     read as one more bar. */
  gap: calc(var(--wm) * 0.3);
}
.wm-code {
  flex: 1 1 0;
  min-width: 0;
  /* Cut to the bracket: 0.857em is how tall its ink stands in this face and
     0.167em is how far its foot sits above the bottom of its line box, so the
     bars start and stop exactly where the brackets do and the row reads as one
     object rather than as type with a picture next to it. */
  height: calc(var(--wm) * 0.857);
  margin-bottom: calc(var(--wm) * 0.167);
  /* The ink runs right to the edges of the viewBox; the default hidden overflow
     would shave a bar if the stretched box lands on a fraction of a pixel. */
  overflow: visible;
  /* Same glow as the wordmark, and it lifts with it on hover. The gradient is a
     fixed one rather than the wordmark's travelling tide: the code is a separate
     object from the letters, so nothing has to line up across the gap, and a
     barcode that shimmers reads as a fault rather than as a finish. */
  filter: drop-shadow(0 0 4px rgba(60, 179, 113, 0.5));
  transition: filter 140ms ease;
}
.wm-code text {
  font-family: "Libre Barcode 128", monospace;
  /* The size the viewBox was measured at. Everything visible about the code's
     size is the box it is stretched into, not this. */
  font-size: 100px;
  fill: url(#wmCodeInk);
}
.author-mark:hover .wm-code,
.author-mark:focus-visible .wm-code {
  filter: drop-shadow(0 0 6px rgba(120, 235, 180, 0.7));
}

.wordmark {
  /* Where the exponent starts, measured from the a's own origin: 0.41em puts
     the middle of the 2 over the middle of the a's foot serif, which sits at
     0.55em in this face. Both the digits and the closing bracket are placed off
     this one number, so the mark cannot come apart if it is ever retuned. */
  --exp-x: calc(var(--wm) * 0.41);
  font-family: "Type Machine", ui-monospace, "Courier New", monospace;
  font-size: var(--wm);
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--ink);
  transition: filter 140ms ease;
}

/* The mark is inked by a sea-green gradient clipped to the glyphs and drifting
   across them, with the glow carried by drop-shadow rather than text-shadow —
   drop-shadow works off the rendered alpha, so it follows the letter shapes
   instead of the element's box, and being on .wordmark it filters the whole
   mark as one group, exponent included.

   The exponent has to carry its own copy of the gradient. A text clip masks
   with the element's own in-flow text only, and the digits are out of flow, so
   the mark's background never reaches them — left to inherit, they come out
   with a transparent fill and nothing painting it, which is to say invisible.

   That makes the two backgrounds one picture rather than two, so both are cut
   from the same tile: --wm-period is an absolute length, not a percentage of
   two differently sized boxes, and the digits' copy is pulled back by exactly
   how far into the mark they sit. 90deg keeps it honest — a tilted gradient's
   period along x depends on the tile's height, and the two tiles are not the
   same height. */
.wordmark {
  /* One tile is three mark-widths, which at this size is eight --wm: wide
     enough that the mark shows a third of the sweep at a time and reads as a
     tide rather than a band crossing it. */
  --wm-period: calc(var(--wm) * 8);
  /* How far the digits sit into the mark, measured from its left edge: the
     bracket and the R are two character advances of this monospace face with
     its tracking (1.048em, and it is the only number here taken off the face
     rather than out of the geometry above), then the exponent's own offset. */
  --wm-lead: calc(var(--wm) * 1.048 + var(--exp-x));
  --wm-ink: linear-gradient(90deg,
    #1c6b46 0%, #2e8b57 18%, #3cb371 38%, #7ff0c0 50%,
    #3cb371 62%, #20b2aa 82%, #1c6b46 100%);
  background-image: var(--wm-ink);
  background-size: var(--wm-period) 100%;
  background-position-x: 0px;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  /* Headroom for the clip. line-height: 1 sets a box shorter than this face's
     em box, and the brackets are the tallest thing in the mark — their ink
     starts half a pixel above the box, which a text clip would cut off. The
     padding lifts the background box clear of them and the negative margin
     hands the space straight back, so nothing in the flex column moves and the
     x origin the gradient is positioned from is untouched. */
  padding-top: calc(var(--wm) * 0.15);
  margin-top: calc(var(--wm) * -0.15);
  filter: drop-shadow(0 0 4px rgba(60, 179, 113, 0.55))
          drop-shadow(0 0 11px rgba(32, 178, 170, 0.30));
  animation: wm-tide 9s linear infinite;
}
.wm-exp {
  background-image: var(--wm-ink);
  background-size: var(--wm-period) 100%;
  background-position-x: calc(0px - var(--wm-lead));
  -webkit-background-clip: text;
          background-clip: text;
  animation: wm-tide-exp 9s linear infinite;
  /* Same headroom, and here it costs nothing: the digits are anchored by their
     bottom edge with the height left auto, so the box grows upward and they do
     not move. */
  padding-top: calc(var(--wm) * 0.1);
}
/* Hover lifts the glow instead of the ink — the gradient is the colour now, so
   the old white-out has nothing left to say. */
.author-mark:hover .wordmark,
.author-mark:focus-visible .wordmark {
  filter: drop-shadow(0 0 5px rgba(120, 235, 180, 0.75))
          drop-shadow(0 0 16px rgba(32, 178, 170, 0.45));
}
/* One period of travel, so the loop closes on itself with no seam to see. The
   digits run the same distance at the same rate from their own offset, which
   is what keeps the two halves of the picture together while it moves. */
@keyframes wm-tide {
  from { background-position-x: 0px; }
  to   { background-position-x: var(--wm-period); }
}
@keyframes wm-tide-exp {
  from { background-position-x: calc(0px - var(--wm-lead)); }
  to   { background-position-x: calc(var(--wm-period) - var(--wm-lead)); }
}
@media (prefers-reduced-motion: reduce) {
  .wordmark { animation: none; background-position-x: calc(var(--wm-period) / 2); }
  .wm-exp   { animation: none; background-position-x: calc(var(--wm-period) / 2 - var(--wm-lead)); }
}
/* Older engines without text clipping keep the plain mark rather than an
   invisible one. */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .wordmark { color: var(--ink); -webkit-text-fill-color: currentColor; background-image: none; }
  .wm-exp { background-image: none; }
}
.wm-a {
  position: relative;
  display: inline-block;
}
/* The closing bracket has to clear the digits, and the digits are out of flow,
   so they cannot push it along themselves: this is how far the 5 ends up past
   the a's advance once the exponent has been moved out over the serif, plus a
   hair of daylight. The bracket then closes the mark round the whole of it,
   exponent included, and its own right edge is what the line of text above
   lines up with. */
.wm-b-close { margin-left: calc(var(--exp-x) + var(--wm) * 0.283); }
/* Off the face's own metrics rather than by eye: 0.60em is where the digits'
   baseline lands on the top of the a's x-height in Type Machine, and the extra
   0.02 is the hairline of daylight that keeps the 5 off the a's shoulder. The
   digits clear the R's cap by about a fifth of an em, which is what makes it
   read as an exponent rather than as a word set in two sizes. */
.wm-exp {
  position: absolute;
  left: var(--exp-x);
  bottom: calc(var(--wm) * 0.62);
  font-size: calc(var(--wm) * 0.52);
  line-height: 1;
  letter-spacing: 0.02em;
}

.stage-status {
  position: absolute;
  left: 12px; bottom: 11px;
  font-size: 11px;
  color: var(--ink-muted);
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}

.loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: var(--surface);
  color: var(--ink-muted);
  font-size: 13px;
  z-index: 8;
}
.loading[hidden] { display: none; }
.spinner {
  width: 15px; height: 15px;
  border: 2px solid var(--line-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------------------------------------------------------- detail panel */

.empty-state { padding: 18px; }
.empty-state p { margin: 0 0 13px; font-size: 12px; color: var(--ink-muted); }
.empty-state .empty-lead { color: var(--ink-2); margin-bottom: 12px; }


.d-head {
  padding: 16px 18px 14px;
  border-bottom: 1px solid var(--line);
}
/* Where the thing in the header lives, in its cluster's hue — the tie back to
 * the canvas that the old swatch dot was there for. The hue arrives per tag as
 * --tag-ink, so one rule covers all twelve clusters. */
.ctx-tags { display: flex; flex-wrap: wrap; gap: 5px; }
.ctx-tag {
  --tag-ink: var(--ink-muted);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  border-radius: 4px;
  padding: 2px 7px;
  color: var(--tag-ink);
  border: 1px solid color-mix(in srgb, var(--tag-ink) 34%, transparent);
  background: color-mix(in srgb, var(--tag-ink) 11%, transparent);
}
.d-title {
  font-size: 19px;
  line-height: 1.25;
  margin: 7px 0 0;
  letter-spacing: -0.015em;
  word-break: break-word;
}
.d-sub { margin: 5px 0 0; font-size: 12px; color: var(--ink-muted); }

.chord-row { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin: 11px 0 0; }
kbd {
  display: inline-block;
  background: #232321;
  border: 1px solid #3d3d39;
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 3px 8px;
  font: 600 12px/1.35 system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--ink);
  white-space: nowrap;
}
.chord-plus { color: var(--ink-muted); font-size: 11px; }

/* A key you merely hold has no action to be titled by, so its keycaps follow
 * the tags directly and have to carry the header on their own — the adjacency
 * is the test for that, since a shortcut with a name puts an h2 in between. */
.ctx-tags + .chord-row { margin-top: 9px; }
.ctx-tags + .chord-row kbd { font-size: 14px; padding: 4px 10px; }

.d-sect { padding: 14px 18px; border-bottom: 1px solid var(--line); }
.d-sect:last-child { border-bottom: 0; }
.d-sect > h3 {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.075em;
  color: var(--ink-muted);
  margin-bottom: 9px;
}

.d-desc { margin: 0; font-size: 13px; line-height: 1.55; color: var(--ink-2); }
.d-desc + .d-desc { margin-top: 9px; }
.d-desc code, .mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px;
}

.kv { display: grid; grid-template-columns: auto 1fr; gap: 5px 12px; font-size: 12px; margin: 0; }
.kv dt { color: var(--ink-muted); }
.kv dd { margin: 0; color: var(--ink-2); word-break: break-word; }

.op-card {
  background: var(--plane-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 11px 12px;
  margin-bottom: 9px;
}
.op-card:last-child { margin-bottom: 0; }
.op-name { font-size: 14px; font-weight: 600; color: var(--ink); margin: 0 0 5px; }
.op-text { margin: 0; font-size: 12.5px; line-height: 1.55; }
.op-meta { margin: 9px 0 0; display: flex; flex-wrap: wrap; gap: 5px; }
.tag {
  font-size: 10.5px;
  background: #232321;
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  padding: 2px 6px;
  color: var(--ink-2);
  white-space: nowrap;
}
.tag.tag-auto { border-style: dashed; color: var(--ink-muted); }

/* Tips in this panel carry no cursor change. Nothing here is only in a
 * tooltip — every tip explains something already written next to it — so the
 * tip can stay quiet and let the pointer alone; a question mark trailing the
 * cursor across a panel you are mostly just reading is noise. */

.jump-list { list-style: none; margin: 0; padding: 0; }
.jump-list li { margin: 0 0 1px; }
.jump-list button {
  display: flex;
  width: 100%;
  gap: 9px;
  align-items: baseline;
  text-align: left;
  appearance: none;
  background: none;
  border: 0;
  border-radius: 5px;
  padding: 5px 7px;
  color: var(--ink-2);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.jump-list button:hover { background: rgba(255,255,255,0.06); color: var(--ink); }
.jump-list .j-main { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jump-list .j-side { color: var(--ink-muted); font-size: 10.5px; white-space: nowrap; }
.more-note { margin: 8px 0 0; font-size: 11px; color: var(--ink-muted); }

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

@media (max-width: 1180px) {
  :root { --sidebar-w: 250px; --detail-w: 320px; }
}
@media (max-width: 940px) {
  /* Stacked, so the panels fold along the other axis. Every collapsed variant
     has to be restated here: #app.sidebar-off outranks a bare #app on
     specificity, and would otherwise drag the three-column track list back
     into a layout that has one column. */
  #app,
  #app.sidebar-off,
  #app.detail-off,
  #app.sidebar-off.detail-off {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0,1fr) auto;
    transition: grid-template-rows 190ms cubic-bezier(0.4, 0, 0.2, 1);
  }
  #app.sidebar-off { grid-template-rows: 0 minmax(0,1fr) auto; }
  #app.detail-off  { grid-template-rows: auto minmax(0,1fr) 0; }
  #app.sidebar-off.detail-off { grid-template-rows: 0 minmax(0,1fr) 0; }

  #sidebar { border-right: 0; border-bottom: 1px solid var(--line); max-height: 33vh; }
  #detail  { border-left: 0; border-top: 1px solid var(--line); max-height: 42vh; }
  #app.sidebar-off #sidebar { border-bottom-width: 0; }
  #app.detail-off  #detail  { border-top-width: 0; }
  #detail.is-empty { display: none; }

  /* The handles follow the edge their panel went out through. */
  .pane-reopen {
    top: auto; right: auto;
    left: 50%;
    transform: translateX(-50%);
    width: 46px; height: 22px;
  }
  .pane-reopen-left  { top: 0;    border-radius: 0 0 7px 7px; border-top-width: 0; border-left-width: 1px; }
  .pane-reopen-right { bottom: 0; border-radius: 7px 7px 0 0; border-bottom-width: 0; border-right-width: 1px; }

  /* Stacked, the detail pane is the bottom row and the mark still lands on it,
     so the room reserved at the foot of its scroll carries over unchanged. The
     one thing that does not: with the pane hidden the mark sits on the bare
     stage, and the pane's own reopen handle is now along the bottom edge with
     it — centred, so the two do not meet, but the handle is why the mark keeps
     its distance from the corner rather than tucking into it. */
  .author-mark { padding-bottom: 12px; }
}
