:root {
  --nx-desktop-bg: #002822;
  --nx-panel: #a8a8a8;
  --nx-panel-light: #d4d4d4;
  --nx-panel-dark: #707070;
  --nx-window-bg: #f8f8f8;
  --nx-titlebar-bg: #000000;
  --nx-titlebar-fg: #ffffff;
  --nx-link-blue: #0000c8;
  --nx-text: #000000;
  --nx-ui-font: "Helvetica Neue", Helvetica, Arial, sans-serif;
  --nx-content-font: Georgia, "Times New Roman", Times, serif;
  --nx-titlebar-height: 20px;
  --nx-menu-width: 132px;
}

* {
  box-sizing: border-box;
}

/* Suppressed for the duration of any pointer-drag (window drag/resize,
   seek bar, volume knob) -- see WindowManager.beginDrag/endDrag -- so the
   cursor sweeping across other windows/the desktop mid-drag doesn't
   trigger native text selection there. */
body.nx-dragging {
  -webkit-user-select: none; /* Safari needs the prefix; unprefixed alone is unreliable there */
  user-select: none;
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  font-family: var(--nx-ui-font);
  font-size: 12px;
  -webkit-font-smoothing: antialiased;
}

#desktop {
  position: relative;
  width: 100%;
  height: 100%;
  background: var(--nx-desktop-bg);
  overflow: hidden;
}

/* ---------- App menu (top-left panel) ---------- */

#app-menu {
  position: absolute;
  top: 8px;
  left: 8px;
  width: var(--nx-menu-width);
  background: var(--nx-panel);
  border: 1px solid #000;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
  /* No fixed z-index -- js/app-menu.js manages it via the same shared
     stacking order as windows (WindowManager.bringToFront), so the menu
     can be brought above or fall behind windows on interaction instead
     of permanently sitting on top of everything. */
  user-select: none;
  transform: scale(var(--menu-scale, 1));
  transform-origin: top left;
}

/* Same override pattern as .nx-title-text-editable[contenteditable="true"]
   below -- #app-menu's own user-select:none (needed so the menu's chrome
   doesn't accidentally text-select during ordinary clicking) would
   otherwise block selecting/marking/linking its text once edit mode makes
   it contentEditable (see settings.js's setEditMode). */
#app-menu[contenteditable="true"] {
  user-select: text;
}

/* End it (app.js's toggleHide, "endit" stage): permanent, no toggle back
   -- the only way to see the menu bar again is reloading the page. */
#app-menu.app-menu-terminated {
  display: none;
}

#app-menu .menu-title {
  background: var(--nx-titlebar-bg);
  color: var(--nx-titlebar-fg);
  padding: 3px 8px;
  font-weight: bold;
  font-size: 12px;
}

#app-menu ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

#app-menu li {
  position: relative;
}

#app-menu .menu-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 3px 8px;
  cursor: default;
  border-top: 1px solid var(--nx-panel-light);
  border-bottom: 1px solid var(--nx-panel-dark);
}

#app-menu .menu-item:hover,
#app-menu .menu-item.open {
  background: #7f7fa0;
  color: #fff;
}

#app-menu .menu-item .arrow {
  font-size: 9px;
  opacity: 0.7;
}

/* ---------- Submenus (Navigate / TOC / Links etc) ---------- */

.submenu {
  position: absolute;
  top: -1px;
  left: 100%;
  min-width: 150px;
  background: var(--nx-panel);
  border: 1px solid #000;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
  display: none;
  /* Only needs to clear its own siblings within #app-menu's local
     stacking context (#app-menu itself now has a dynamic, JS-managed
     z-index -- see app-menu.js) -- a small value is enough. */
  z-index: 1;
}

.submenu.visible {
  display: block;
}

.submenu .menu-item {
  padding: 3px 8px;
  border-top: 1px solid var(--nx-panel-light);
  border-bottom: 1px solid var(--nx-panel-dark);
  white-space: nowrap;
  display: flex;
  justify-content: space-between;
  gap: 12px;
}

.submenu .menu-item:hover:not(.disabled) {
  background: #7f7fa0;
  color: #fff;
}

.submenu .menu-item.disabled {
  opacity: 0.45;
  cursor: default;
}

.submenu .key-hint {
  opacity: 0.7;
  font-size: 11px;
}

.submenu .menu-separator {
  height: 1px;
  background: var(--nx-panel-dark);
  border-top: 1px solid var(--nx-panel-light);
  margin: 2px 0;
}

/* ---------- Hide/Run/End-it transition (app.js's transitionMenuItem) ----------
   Three phases in sequence: a short burst of distortion, a hard flash of
   the phrase, then the same distortion again before settling on the new
   label. The flash itself (.hide-flash) is deliberately animation-free --
   at its default 50ms (adjustable via Settings' "Flash duration"), a
   browser paints at most a few frames before it's gone, so a multi-step
   keyframe would never complete even one cycle. Just a hard, instant,
   high-contrast swap. The bookending .hide-glitch distortion (fixed
   30ms each side, not adjustable) is the opposite on purpose -- the
   pre-existing "page is breaking" look brought back specifically to
   bracket the flash: row shake/skew + inverting-signal filter, torn
   magenta/cyan ghost slices of the label (read from data-glitch-text,
   kept in sync with the real, scrambling textContent by app.js on every
   tick), and the label's real text scrambled character-by-character in
   JS so it reads as actively corrupting, not just colored. */
#app-menu .menu-item.hide-flash {
  background: #000;
  color: #ff2b6e;
}

#app-menu .menu-item.hide-flash .menu-item-label {
  color: #ff2b6e;
  font-weight: bold;
}

#app-menu .menu-item.hide-glitch {
  animation: hide-glitch-shake 0.11s steps(2, jump-none) infinite, hide-glitch-invert 0.37s steps(1) infinite;
}

@keyframes hide-glitch-shake {
  0% { transform: translate(0, 0) skewX(0deg); }
  20% { transform: translate(-3px, 1px) skewX(-4deg); }
  40% { transform: translate(2px, -2px) skewX(3deg); }
  60% { transform: translate(-2px, 2px) skewX(-2deg); }
  80% { transform: translate(3px, -1px) skewX(4deg); }
  100% { transform: translate(0, 0) skewX(0deg); }
}

@keyframes hide-glitch-invert {
  0%, 60% { filter: none; }
  65% { filter: invert(1) contrast(1.5); }
  70% { filter: none; }
  85% { filter: invert(1) hue-rotate(180deg); }
  90% { filter: none; }
}

#app-menu .menu-item.hide-glitch .menu-item-label {
  position: relative;
  display: inline-block;
}

#app-menu .menu-item.hide-glitch .menu-item-label::before,
#app-menu .menu-item.hide-glitch .menu-item-label::after {
  content: attr(data-glitch-text);
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  overflow: hidden;
}

#app-menu .menu-item.hide-glitch .menu-item-label::before {
  color: #ff2b6e;
  clip-path: polygon(0 0, 100% 0, 100% 40%, 0 40%);
  animation: hide-glitch-tear-a 0.23s steps(2, jump-none) infinite;
}

#app-menu .menu-item.hide-glitch .menu-item-label::after {
  color: #00e5ff;
  clip-path: polygon(0 60%, 100% 60%, 100% 100%, 0 100%);
  animation: hide-glitch-tear-b 0.19s steps(2, jump-none) infinite reverse;
}

@keyframes hide-glitch-tear-a {
  0% { transform: translate(0, 0); }
  25% { transform: translate(-5px, -1px); }
  50% { transform: translate(4px, 1px); }
  75% { transform: translate(-3px, 0); }
  100% { transform: translate(0, 0); }
}

@keyframes hide-glitch-tear-b {
  0% { transform: translate(0, 0); }
  30% { transform: translate(5px, 0); }
  60% { transform: translate(-4px, 1px); }
  100% { transform: translate(0, 0); }
}

/* ---------- Windows ---------- */

/* --win-* custom properties are set inline per-window by the Style panel
   (settings.js's applyWindowStyle) -- unset windows fall through to the
   site-wide --nx-* defaults, so this is purely additive. */
.nx-window {
  position: absolute;
  min-width: 220px;
  min-height: 140px;
  background: var(--win-bg-color, var(--nx-window-bg));
  border: 1px solid #000;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
}

.nx-window.hidden {
  display: none;
}

.nx-window .nx-titlebar {
  height: var(--nx-titlebar-height);
  background: var(--win-header-color, var(--nx-titlebar-bg));
  color: var(--nx-titlebar-fg);
  display: flex;
  align-items: center;
  padding: 0 6px;
  gap: 6px;
  cursor: grab;
  user-select: none;
  flex: none;
}

.nx-window .nx-titlebar:active {
  cursor: grabbing;
}

.nx-window .nx-close {
  width: 13px;
  height: 13px;
  background: var(--win-button-color, var(--nx-panel));
  border: 1px solid #666;
  color: #000;
  font-size: 9px;
  line-height: 11px;
  text-align: center;
  cursor: pointer;
  flex: none;
}

.nx-window .nx-title-text {
  flex: 1;
  text-align: center;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* .nx-titlebar's own user-select:none (needed so dragging a window never
   also starts a stray text selection) would otherwise block selecting/
   placing a cursor in an editable title too, contentEditable or not --
   override it back on specifically while this title is actually in its
   editable state (see window-manager.js's opts.editableTitle /
   Settings.applyEditableToTitle). */
.nx-window .nx-title-text-editable[contenteditable="true"] {
  user-select: text;
  cursor: text;
}

/* Fullscreen toggle button and the mobile-only zoom in/out buttons are
   each optional app-wide (independent Settings toggles); pinch/ctrl-wheel
   zoom keeps working regardless of either. */
#desktop.hide-zoom-buttons .nx-window .nx-fullscreen-btn {
  display: none;
}

#desktop.hide-zoom-in-out-buttons .nx-window .nx-zoom-btn {
  display: none;
}

.nx-window.fixed-size .nx-resize-handle:not(.nx-resize-handle-proportional) {
  display: none;
}

.nx-window .nx-fullscreen-btn {
  flex: none;
  width: 15px;
  height: 13px;
  font-size: 10px;
  line-height: 1;
  background: var(--win-button-color, var(--nx-panel));
  border: 1px solid #666;
  cursor: pointer;
  padding: 0;
}

/* Per-window zoom buttons: hidden on desktop (trackpad pinch/ctrl-wheel
   already zooms the hovered window there) -- shown under the mobile
   breakpoint below, where touch pinch is reclaimed for whole-canvas
   pan/zoom instead (see Plans/archive/2026-07-13_next-steps.md). */
.nx-window .nx-zoom-btn {
  display: none;
  flex: none;
  width: 15px;
  height: 13px;
  font-size: 10px;
  line-height: 1;
  background: var(--win-button-color, var(--nx-panel));
  border: 1px solid #666;
  cursor: pointer;
  padding: 0;
}

.nx-window .nx-body {
  flex: 1;
  overflow: auto;
  padding: 14px 18px;
  font-family: var(--nx-content-font);
  line-height: 1.5;
  color: var(--win-text-color, var(--nx-text));
  /* Flips the native scrollbar to the left edge (matching the NeXTSTEP
     reference screenshots) without hand-building a draggable replacement --
     text content direction is reset back to ltr immediately below, on the
     window's actual content wrapper. */
  direction: rtl;
}

.nx-window .nx-body > * {
  direction: ltr;
}

/* scrollbar-width/scrollbar-color are the standard properties Firefox
   invented and Chromium later adopted too -- but recent Chromium applies
   them even when the legacy ::-webkit-scrollbar-* pseudo-elements below
   are also defined, silently replacing the whole beveled look with a
   plain thin bar (confirmed directly: with both declared, Chrome renders
   only a flat grey line, no distinct track/thumb/button). Scoping these
   behind a feature query so only browsers that can't render
   ::-webkit-scrollbar at all (Firefox) fall back to them keeps the full
   custom look everywhere that can actually display it (Chrome, Safari). */
@supports not selector(::-webkit-scrollbar) {
  .nx-window .nx-body {
    scrollbar-width: thin;
    scrollbar-color: var(--nx-panel) var(--nx-panel-light);
  }
}

.nx-window .nx-body::-webkit-scrollbar {
  width: 16px;
}

.nx-window .nx-body::-webkit-scrollbar-track {
  background: var(--nx-panel-light);
  box-shadow: inset 1px 0 0 var(--nx-panel-dark), inset -1px 0 0 var(--nx-panel-dark);
}

.nx-window .nx-body::-webkit-scrollbar-thumb {
  background: var(--nx-panel);
  border: 1px solid #666;
  box-shadow: inset 1px 1px 0 var(--nx-panel-light), inset -1px -1px 0 var(--nx-panel-dark);
}

.nx-window .nx-body::-webkit-scrollbar-button {
  display: block;
  height: 15px;
  background: var(--nx-panel);
  border: 1px solid #666;
  box-shadow: inset 1px 1px 0 var(--nx-panel-light), inset -1px -1px 0 var(--nx-panel-dark);
}

.nx-window .nx-body h1,
.nx-window .nx-body h2 {
  font-family: var(--nx-content-font);
}

.nx-window .nx-body a {
  color: var(--nx-text);
  text-decoration: underline;
  cursor: pointer;
}

/* Per-essay Navigation row (modeled on the reference "Navigation"
   window): appears at both the top and bottom of each essay's content. */
.nav-row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  padding: 6px 0;
  margin-bottom: 10px;
  border-bottom: 1px solid #ccc;
  font-family: var(--nx-ui-font);
}

.nav-row:last-child {
  margin-bottom: 0;
  margin-top: 10px;
  border-bottom: none;
  border-top: 1px solid #ccc;
}

.nav-row button {
  font-family: var(--nx-ui-font);
  font-size: 11px;
  padding: 4px 10px;
  background: var(--win-button-color, var(--nx-panel));
  border: 1px solid #666;
  cursor: pointer;
}

.nav-row button:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Table of Contents window (Home / Contents-menu target) -- a flat list
   of every essay/interlude/reference pane, styled plainly like the rest
   of the chrome. No linkEffects entry is assigned to the "toc" pane id,
   so its <a> links stay plain black/underlined via the fallback noted
   below rather than needing an explicit override here. */
.toc-body {
  font-family: var(--nx-ui-font);
}

.toc-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.toc-list li {
  padding: 4px 0;
  border-bottom: 1px solid #ddd;
}

.toc-list li a {
  color: var(--nx-link-blue);
  text-decoration: underline;
  cursor: pointer;
}

/* Home's edit-mode view (toc.js's buildTocJsonEditorContent) -- a bare,
   unannotated textarea over the raw contentMap JSON, deliberately plain
   (no syntax coloring/line numbers) so it reads as editing the actual
   underlying data, not a GUI over it. */
.toc-json-editor-wrap {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.toc-json-editor {
  flex: 1;
  width: 100%;
  border: none;
  resize: none;
  font-family: monospace;
  font-size: 12px;
  padding: 0;
}

.toc-json-editor-error {
  color: #b30809;
  font-family: var(--nx-ui-font);
  font-size: 11px;
  padding-top: 4px;
}

.s-citation-shorthand-config {
  width: 100%;
  box-sizing: border-box;
  font-family: monospace;
  font-size: 11px;
}

.s-citation-shorthand-error {
  color: #b30809;
  font-family: var(--nx-ui-font);
  font-size: 11px;
  padding-top: 4px;
}

/* Footnotes: lettered markers ([A][B][C]..., continuous across the whole
   site) and citations (numbered 1 2 3..., reset per essay) each render as
   either a "Box" popover near the marker or an "Endnote" bottom-of-pane
   list, independently switchable in Settings -- see link-system.js's
   renderNotes for the click wiring and numbering rules. Markers are black,
   not link-blue: they're navigation UI (jump/box-toggle), not thematic
   prose links, so they shouldn't visually read as ordinary hyperlinks. */
.nx-window .nx-body .footnote-ref {
  font-size: 0.8em;
}

.nx-window .nx-body .footnote-ref a {
  color: #000;
  cursor: pointer;
  text-decoration: underline;
}

.footnote-list[hidden],
.citation-list[hidden] {
  display: none;
}

.footnote-list,
.citation-list {
  margin-top: 24px;
  padding-top: 8px;
  border-top: 1px solid #999;
  font-size: 0.9em;
}

.footnote-list + .citation-list {
  margin-top: 12px;
  border-top: none;
  padding-top: 0;
}

.footnote-list h2,
.citation-list h2 {
  font-size: 1em;
  margin: 0 0 8px;
}

.footnote-list-items,
.citation-list-items {
  list-style: none;
  margin: 0;
  padding: 0;
}

.footnote-list-items li,
.citation-list-items li {
  margin-bottom: 6px;
}

/* The letter/number itself is the up-link back to its originating
   superscript -- no separate arrow glyph. */
.footnote-backlink,
.citation-backlink {
  color: var(--nx-link-blue);
  cursor: pointer;
  text-decoration: none;
  font-weight: bold;
}

/* Citations: numbered markers -- authored .citation-body (APA text + any
   source link), either moved into a popover box near the marker (see
   link-system.js's openNotePopover) or shown as an entry in the always-
   present .citation-list above (hidden outside Endnote mode). */
.nx-window .nx-body .citation-ref {
  color: var(--nx-link-blue);
  cursor: pointer;
  font-size: 0.8em;
}

.note-popover {
  display: inline-block;
  max-width: 260px;
  margin-left: 4px;
  padding: 6px 8px;
  background: #ffffe0;
  border: 1px solid #333;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3);
  font-size: 11px;
  font-family: var(--nx-ui-font);
  vertical-align: top;
}

/* Per-window link-effect regimes (base/inactive state keeps the normal
   black link color; .fx-active is toggled by link-system.js on hover,
   the only trigger mode -- click-persist has been removed). Windows with
   no assigned regime (interludes, Further Reading, footnote lists,
   citation popovers) simply never get .effect-* classes, so they stay
   plain black/underlined already. */

/* Distorts individual .fx-char spans within radiusPx of the cursor (see
   LinkEffects.wireChromaticRadius). */
.effect-chromatic-aberration .fx-char.fx-active {
  text-shadow: -2px 0 #a020f0, 2px 0 #00c8c8;
  animation: fx-chromatic-jitter 0.4s infinite;
}

@keyframes fx-chromatic-jitter {
  0%, 100% { text-shadow: -2px 0 #a020f0, 2px 0 #00c8c8; }
  50% { text-shadow: 2px 0 #a020f0, -2px 0 #00c8c8; }
}

.effect-blood-splatter.fx-active {
  /* sampled directly from the splatter art's solid core (assets/blood/) */
  color: #b30809 !important;
}

/* Existential Brutality footnote markers flash the same red only *while*
   clicked (native :active, not a JS-added class -- see wireBloodSplatter's
   two-loop split), reverting to the normal footnote color the instant the
   click ends. Citations keep the persistent .fx-active recolor above;
   footnotes deliberately don't. */
.footnote-ref.effect-blood-splatter a:active {
  color: #b30809;
}

/* Admin-marked text (settings.js's Distort button), any pane: a faint
   dashed underline at rest so it's identifiable while editing, regardless
   of which effect actually applies on hover. In the three essay panes with
   a native link-effect regime, a mark just joins that regime's own effect
   targets (see wireWindow) and picks up its classes/styling same as a real
   link -- no separate rule needed here for those. In the two intermission
   panes (no native regime), each character glitches independently within
   radiusPx of the cursor (see LinkEffects.wireDistortionMarks) rather than
   the whole marked span glitching as one unit. */
.marked-distort {
  text-decoration: underline dashed;
  text-decoration-color: rgba(255, 0, 0, 0.35);
}

.marked-distort .fx-char.distort-active {
  text-shadow: -2px 0 #a020f0, 2px 0 #00c8c8;
  animation: fx-chromatic-jitter 0.4s infinite;
}

/* Links menu authoring marks (link-editor.js's Mark selection/Mark all).
   Following the original WorldWideWeb (NextStep TextToy.m, where `Mark` is a
   single Anchor at a time and only the current one is "the mark"), only the
   *active* mark draws an outline -- a superseded or already-linked mark keeps
   its id/span so links still resolve to it, but no longer shows a visual,
   instead of every mark ever made accumulating a persistent outline (bug
   report 7.15). The span is left in place, not deleted, matching WWW's own
   "we don't delete anchors" rule (HyperText.m unlinkSelection). Scoped to
   [contenteditable="true"] so marks only show while editing is on. */
[contenteditable="true"] .link-mark.link-mark-active {
  outline: 2px dashed rgba(200, 30, 30, 0.7);
  outline-offset: 1px;
}

/* Whole-pane marks (Mark all) use an inset offset so the active outline isn't
   clipped by the pane's own scrolling body. */
[contenteditable="true"] .link-mark.link-mark-whole.link-mark-active {
  outline-offset: -3px;
}

/* A touched player control that's the current "Mark selection" candidate
   (see media-player.js's touchControl/applyControlMarkVisibility) -- the
   control equivalent of a live text selection, since a control has no
   native "selected" appearance of its own to piggyback on. Not scoped to
   [contenteditable="true"] like .link-mark above (the control it's on
   isn't necessarily inside a contentEditable ancestor at all) -- shown/
   hidden directly in JS instead, gated on edit mode there. */
.control-mark-active {
  outline: 2px dashed rgba(200, 30, 30, 0.7);
  outline-offset: 2px;
}

/* The current committed state-mark on a player control (link-editor.js's Mark
   selection run on a fader, via setActiveMark) -- distinct from
   .control-mark-active above, which is media-player.js's own live,
   not-yet-committed touch-candidate indicator that disappears the instant a
   different control is touched. Like text marks (see .link-mark above), only
   the *current* one shows an outline, following WWW's single-Mark model (bug
   report 7.15). Gated on body.editing rather than a [contenteditable="true"]
   ancestor selector like .link-mark, since a player control isn't necessarily
   inside one at all. */
body.editing .state-mark.state-mark-active {
  outline: 2px dashed rgba(200, 30, 30, 0.7);
  outline-offset: 2px;
}

/* A selection "parked" in the background after the user clicked into a
   different window (see settings.js's parkRange/trackEditableSelection) --
   approximates the look of a native text selection highlight, since the
   real browser Selection has moved elsewhere and its own ::selection
   rendering no longer shows here at all. Stays visible until this
   specific window is clicked into again. */
.parked-selection {
  background: rgba(0, 100, 255, 0.35);
}

/* Blocks margin collapsing: without it, the essay's first heading's own
   top margin escapes upward through .essay-content (which has no
   padding/border of its own) and collapses with .nav-row's margin-bottom
   instead of staying inside .essay-content's box -- so .essay-content
   visually starts further down than its own getBoundingClientRect edge,
   leaving a gap of true empty space that .blood-splatter-layer's
   overflow:hidden (anchored to that same edge) can't cover, cutting off
   splatters placed near the top with unexplained whitespace above them. */
.essay-content {
  padding-top: 1px;
}

/* Cursor-tracked splatter instances (see LinkEffects.wireBloodSplatter)
   live in a dedicated layer behind the essay text, not a CSS pseudo-
   element -- real DOM nodes so they can be placed at the cursor and
   stack up over each other as the user moves. No overflow:hidden here on
   purpose -- a splatter placed near the essay's top/bottom edge is
   allowed to bleed past .essay-content's own box rather than getting a
   hard clip. */
.blood-splatter-layer {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

.blood-splatter-instance {
  position: absolute;
  pointer-events: none;
  opacity: 0.8;
}

.effect-strobe.fx-active {
  color: #0000ff !important;
  animation-name: fx-strobe-flash;
  animation-timing-function: steps(2, jump-none);
  animation-iteration-count: infinite;
}

@keyframes fx-strobe-flash {
  0%, 49% { text-shadow: 0 0 6px #0000ff, 0 0 12px #0000ff; }
  50%, 100% { text-shadow: none; }
}

.nx-window .nx-body ul.reference-list {
  padding-left: 20px;
}

.nx-window .nx-body ul.reference-list li {
  margin-bottom: 10px;
}

#video-backdrop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 1;
}

/* Run's natural end (media-player.js's playRunOverride onEnded) --
   a <video>'s last decoded frame always paints over any CSS background on
   the element itself, so ending "on black" needs this separate covering
   layer rather than a style tweak on #video-backdrop. Same z-index as the
   video, positioned right after it in the DOM so equal-z-index paint
   order puts it on top. */
#video-blackout {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: #000;
  display: none;
}

#video-blackout.visible {
  display: block;
}

/* Sits above the teal/video backdrop, below window chrome -- a fixed
   "screen" layer whose opacity/blend/scale are tuned live from Settings
   (see Settings.applyOverlayTexture). Two independent sub-layers:
   .overlay-tint (a flat color wash, "how green the glass looks") sits
   UNDERNEATH .overlay-grain (the actual texture image, tiled at its
   native 372x182 size by default). The grain is drawn on top and
   blended, rather than the tint sitting on top of it -- the tint would
   otherwise wash out the grain, which is only 1-5% opaque per pixel to
   begin with (that faintness is the source image's own design, not
   something to compensate for with default CSS opacity reduction). */
#overlay-texture {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

#overlay-texture .overlay-tint {
  position: absolute;
  inset: 0;
  background-color: var(--nx-desktop-bg);
}

#overlay-texture .overlay-grain {
  position: absolute;
  inset: 0;
  background-image: url(assets/overlay-texture.png);
  background-repeat: repeat;
  background-size: 372px auto;
  opacity: 1;
}

#dock {
  position: absolute;
  right: 16px;
  bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  transform: scale(var(--icon-scale, 1));
  transform-origin: bottom right;
  z-index: 9000;
}

/* End it (app.js's toggleHide, "endit" stage): permanent, same as
   #app-menu.app-menu-terminated -- only a page reload brings it back. */
#dock.dock-terminated {
  display: none;
}

/* Unscoped from #dock (was "#dock .dock-icon"/"#dock .dock-icon:active")
   so the essay-title icon button (toc.js's buildTitleIcon, same
   .dock-icon class reused deliberately) picks up the same box look
   without duplicating it -- .essay-title-icon below only overrides size. */
.dock-icon {
  width: 48px;
  height: 48px;
  padding: 0;
  background: var(--nx-panel);
  border: 1px solid #000;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.dock-icon:active {
  box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.4);
  transform: translate(1px, 1px);
}

#dock img {
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
}

/* Essay title icon (toc.js's buildTitleIcon): a duplicate of the dock
   icon for that essay's track, floated right and level with the <h1>
   (placed *before* it in the DOM -- see buildEssayContent -- so the
   title's own text wraps around it on a narrow window, standard float
   behavior). Reuses .dock-icon's own box/border/shadow look rather than
   introducing a new visual language for the same affordance.
   font-size: inherit + em sizing (rather than a fixed px size like
   .dock-icon's) is what makes it scale with the essay's own zoom level,
   same as the body text -- <button> doesn't inherit font-size from its
   container by default in most browsers' UA stylesheets, so without the
   explicit inherit here the em values below would resolve against that
   browser default instead of the essay's actual current font-size. */
.essay-title-icon {
  float: right;
  font-size: inherit;
  width: 4em;
  height: 4em;
  margin: 0 0 8px 12px;
  position: relative;
}

.essay-title-icon img {
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
}

/* Semi-transparent grey play glyph, centered over the icon -- a visual
   hint that clicking it opens the player, without a second real icon
   asset. pointer-events: none so the click still lands on the button
   underneath. */
.essay-title-icon::after {
  content: "▶";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5em;
  color: rgba(120, 120, 120, 0.65);
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

/* Winamp-style player: fixed-size, uniformly scaled skin (see the
   "player" window's fixedSize/zoomMode:'scale' options). All pixel
   values below are exact sprite-sheet coordinates from the real classic
   Winamp skin resources (Src/Winamp/draw_main.cpp), not approximations. */

.nx-window[data-window-id="player"] {
  overflow: visible;
  /* the generic .nx-window min-height (140px) is taller than the actual
     skin (102px body + 14px titlebar) and would otherwise leave a blank
     white strip below the skin */
  min-height: 0;
}

.nx-window[data-window-id="player"] .nx-titlebar {
  position: relative;
  height: 14px;
  background-image: url(assets/winamp/titlebar.png);
  background-position: -27px 0px;
  image-rendering: pixelated;
}

.nx-window[data-window-id="player"] .nx-title-text {
  display: none;
}

/* Real classic-Winamp close button sits near the titlebar's right edge
   (draw_tbuttons' b3, dest x=264 of a 275px-wide window) -- taken out of
   the titlebar's flex flow so it doesn't default to flex-start (left). */
.nx-window[data-window-id="player"] .nx-close {
  position: absolute;
  top: 3px;
  right: 2px;
  width: 9px;
  height: 9px;
  background-image: url(assets/winamp/titlebar.png);
  background-position: -18px 0px;
  background-color: transparent;
  border: none;
  color: transparent;
  image-rendering: pixelated;
}

.nx-window[data-window-id="player"] .nx-body {
  padding: 0;
  overflow: visible;
  direction: ltr; /* no scrollbar here -- skip the left-scrollbar rtl trick */
  touch-action: none; /* defense-in-depth against touch pinch-zoom; see blockNativePinchZoom */
}

.winamp-wrapper {
  width: 275px;
  font-family: var(--nx-ui-font);
}

.winamp-skin {
  position: relative;
  width: 275px;
  height: 102px;
  background-image: url(assets/winamp/main.png);
  background-position: 0px -14px;
  image-rendering: pixelated;
  user-select: none;
}

.winamp-eq-skin {
  position: relative;
  width: 275px;
  height: 102px;
  background-image: url(assets/winamp/eqmain.png);
  background-position: 0px -14px;
  image-rendering: pixelated;
  user-select: none;
}

/* Style panel color tint for the player (see settings.js's
   applyWindowStyle/hexToTintRgba) -- background-blend-mode mixes the
   sprite background-image with an otherwise-invisible transparent
   background-color, so an untouched (transparent) tint var is a no-op
   and a chosen color blends in proportional to its own saturation-driven
   alpha. --winamp-*-tint default to transparent (not set) so a window
   with no custom style applied gets no blend at all. */
.nx-window[data-window-id="player"] .winamp-skin,
.nx-window[data-window-id="player"] .winamp-eq-skin {
  background-color: var(--winamp-window-tint, transparent);
  background-blend-mode: color;
}

.nx-window[data-window-id="player"] .winamp-sprite {
  background-color: var(--winamp-button-tint, transparent);
  background-blend-mode: color;
}

.nx-window[data-window-id="player"] .nx-titlebar,
.nx-window[data-window-id="player"] .nx-docked-titlebar {
  background-color: var(--winamp-header-tint, transparent);
  background-blend-mode: color;
}

/* Docked EQ panel (see media-player.js dockEqPanel): appended as a real
   sibling inside the player's own .nx-window, not a separate WindowManager
   window -- it shares the player's drag/focus/z-index/zoom(scale)/resize
   for free this way, exactly matching "the same window" behavior, while
   still LOOKING like the reference's own stacked "WINAMP EQUALIZER" bar
   (the text is baked into this exact crop of eqmain.png, not CSS text --
   see draw_eq_tbar in the vendored source). Playlist docks the same way. */
.nx-docked-titlebar {
  position: relative;
  width: 275px;
  height: 14px;
  background-image: url(assets/winamp/eqmain.png);
  background-position: 0px -134px;
  image-rendering: pixelated;
  cursor: grab;
}

.nx-docked-titlebar:active {
  cursor: grabbing;
}

/* Specificity must beat .nx-window[data-window-id="player"] .nx-close
   (line ~540) -- the docked titlebar's close button is nested inside the
   player window too, so without this it would silently inherit that
   rule's titlebar.png close icon instead of staying transparent. */
.nx-window[data-window-id="player"] .nx-docked-titlebar .nx-close {
  position: absolute;
  top: 3px;
  right: 2px;
  width: 9px;
  height: 9px;
  background: transparent;
  border: none;
  color: transparent;
  cursor: pointer;
}

/* Playlist's titlebar has no single self-contained crop like EQ's --
   pledit.png's is a resizable multi-piece composition (see
   buildPlaylistTitlebarStrip), built as sprite() children rather than one
   CSS background. buildPlaylistTitlebarStrip now tiles real sprite content
   across the full 275px (no flat-color gap remains) -- this background-
   color is just a safety-net fallback in case that ever changes, sampled
   directly from the source image's own flat padding past x=179 so it'd
   still match seamlessly if it were ever visible. */
.nx-docked-titlebar-playlist {
  position: relative;
  width: 275px;
  height: 20px;
  background-color: rgb(56, 56, 88);
  cursor: grab;
}

.nx-docked-titlebar-playlist:active {
  cursor: grabbing;
}

/* The baked-in close-X glyph sits inside the last 9px of the icon+close
   segment (see buildPlaylistTitlebarStrip, now placed flush at the bar's
   true right edge, same as EQ/main's own close buttons) -- confirmed
   directly against the source pixels (assets/winamp/pledit.png, x 169-178
   of the 154-178 crop). */
.nx-window[data-window-id="player"] .nx-docked-titlebar-playlist .nx-close {
  position: absolute;
  top: 5px;
  right: 0;
  width: 9px;
  height: 9px;
  background: transparent;
  border: none;
  color: transparent;
  cursor: pointer;
}

.winamp-playlist-body {
  width: 275px;
  height: 110px;
  background: #0a0a2e;
  display: flex;
  flex-direction: column;
  font-family: "Courier New", monospace;
}

.winamp-playlist-list {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

.winamp-playlist-row {
  display: flex;
  align-items: center;
  width: 100%;
  text-align: left;
  padding: 2px 6px;
  background: transparent;
  border: none;
  color: #00ff00;
  font-family: "Courier New", monospace;
  font-size: 11px;
  cursor: pointer;
  /* Dragged across the pointer path mid-reorder (see media-player.js's
     wireRowDrag) -- without this, the drag also selects the row's own
     (non-editable) text as the pointer sweeps past it. */
  -webkit-user-select: none;
  user-select: none;
  touch-action: none;
}

.winamp-playlist-row:hover {
  background: #1a1a4e;
}

.winamp-playlist-row.current {
  background: #2c2c6e;
  color: #ffffff;
}

/* The row actively being dragged (wireRowDrag) -- lifted above its
   siblings, which are themselves preview-shifting via transform underneath
   it, and lightly dimmed as a "this one's mid-move" cue. */
.winamp-playlist-row-dragging {
  position: relative;
  z-index: 2;
  opacity: 0.85;
}

.winamp-playlist-row-num {
  flex: none;
}

.winamp-playlist-row-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.winamp-playlist-buttons {
  display: flex;
  gap: 1px;
  padding: 2px;
}

/* Self-sufficient (not built on .winamp-slot-btn, which is
   position:absolute and would pull these out of the flex row above). */
.winamp-playlist-slot-btn {
  position: static;
  flex: 1;
  height: 16px;
  font-family: "Courier New", monospace;
  font-size: 8px;
  font-weight: bold;
  letter-spacing: 0.5px;
  color: #d8d8d8;
  background: linear-gradient(#4a4a5a, #2c2c38);
  border: 1px solid #17171f;
  border-radius: 1px;
  padding: 0;
}

.winamp-playlist-slot-btn:disabled {
  cursor: default;
  opacity: 0.6;
}

/* ADD button's picker (media-player.js's openAddPicker) -- a small floating
   overlay, not part of any .nx-window's own stacking, so a flat high
   z-index is enough (same reasoning as .submenu, just simpler since this
   has no sibling submenus of its own to out-rank). */
.winamp-add-picker {
  position: fixed;
  z-index: 9999;
  min-width: 160px;
  background: #0a0a2e;
  border: 1px solid #17171f;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
  display: flex;
  flex-direction: column;
  font-family: "Courier New", monospace;
}

.winamp-add-picker-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 3px 6px;
  background: transparent;
  border: none;
  border-bottom: 1px solid #17171f;
  color: #00ff00;
  font-family: "Courier New", monospace;
  font-size: 11px;
  cursor: pointer;
  white-space: nowrap;
}

.winamp-add-picker-item:last-child {
  border-bottom: none;
}

.winamp-add-picker-item:hover {
  background: #1a1a4e;
}

.winamp-title-marquee {
  color: #00ff00;
  background: #000;
  font-family: "Courier New", monospace;
  font-size: 8px;
  line-height: 10px;
  overflow: hidden;
  white-space: nowrap;
  padding-left: 2px;
}

/* Transparent typing surface over the classic sprite-digit time display
   (see media-player.js's buildTimeDisplay) -- the digits themselves stay
   the visual source of truth; this only exists to catch keystrokes while
   editing, and clears itself on blur/Enter so it never visually competes
   with them. */
.winamp-time-overlay {
  position: absolute;
  font-family: "Courier New", monospace;
  font-size: 9px;
  color: #00ff00;
  background: transparent;
  outline: none;
  overflow: hidden;
  white-space: nowrap;
  cursor: text;
}

.winamp-time-overlay:not([contenteditable="true"]) {
  pointer-events: none;
}

/* Lyrics control, sitting in the skin's own shuffle/repeat button slot
   (see buildShuffleRepeatSlotControls) rather than a separate panel --
   styled like a recessed toggle button to match the skin's beveled
   cbuttons look instead of standing out as plain HTML controls. */
.winamp-slot-btn {
  position: absolute;
  height: 15px;
  font-family: "Courier New", monospace;
  font-size: 8px;
  font-weight: bold;
  letter-spacing: 0.5px;
  color: #d8d8d8;
  background: linear-gradient(#4a4a5a, #2c2c38);
  border: 1px solid #17171f;
  border-radius: 1px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

.winamp-slot-btn:active {
  background: linear-gradient(#2c2c38, #1a1a22);
}

.winamp-lyrics-btn {
  width: 47px;
}

/* Highlights the dock icon for whichever track is currently loaded --
   the dock is now the sole way to switch tracks, so it doubles as the
   "now playing" indicator that the removed per-track player buttons used
   to provide via their own .active state. */
#dock .dock-icon.active {
  background: #7f7fa0;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4), inset 0 0 0 1px #fff;
}

.lyric-pane {
  display: flex;
  flex-direction: column;
  height: 100%;
  font-family: var(--nx-ui-font);
}

/* The scroll container wraps the timed lines AND any authored footnote/
   citation lists, so endnote lists scroll with the lyrics (see
   lyric-sync.js's buildPaneContent / Plans/roadmap/annotatable-lyrics.md). */
.lyric-scroll {
  flex: 1;
  overflow: auto;
}

/* Authored footnote/citation lists in a lyric pane: set them off from the
   lyrics above with a rule and normal (non-grey) reading color, otherwise
   they inherit essay note styling. */
.lyric-scroll .footnote-list,
.lyric-scroll .citation-list {
  margin-top: 14px;
  padding-top: 8px;
  border-top: 1px solid rgba(0, 0, 0, 0.15);
  color: #222;
  font-family: var(--nx-content-font);
}

.lyric-line {
  padding: 4px 2px;
  color: #888;
  font-family: var(--nx-content-font);
  cursor: pointer;
}

.lyric-line.active {
  color: #000;
  font-weight: bold;
}

.settings-body {
  font-family: var(--nx-ui-font);
  font-size: 11px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.settings-body fieldset {
  border: 1px solid #999;
  padding: 6px 8px;
}

.settings-body legend {
  font-weight: bold;
  padding: 0 4px;
}

.settings-body label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  margin: 4px 0;
}

.settings-body input[type="number"] {
  width: 60px;
}

.style-panel-body {
  font-family: var(--nx-ui-font);
  font-size: 11px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px;
}

.style-panel-body fieldset {
  border: 1px solid #999;
  padding: 6px 8px;
}

.style-panel-body legend {
  font-weight: bold;
  padding: 0 4px;
}

.style-panel-body label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  margin: 4px 0;
}

.style-panel-body input[type="color"] {
  width: 40px;
  height: 20px;
  padding: 0;
  border: 1px solid #666;
  cursor: pointer;
}

.offset-grid {
  width: 100%;
  border-collapse: collapse;
}

.offset-grid th,
.offset-grid td {
  padding: 2px 4px;
  text-align: center;
  font-weight: normal;
}

.offset-grid thead th {
  font-weight: bold;
}

.offset-grid tbody th {
  text-align: left;
  font-weight: normal;
}

.offset-grid input[type="number"] {
  width: 52px;
}

.settings-actions {
  display: flex;
  align-items: center;
  gap: 6px;
}

.s-sync-hint {
  font-size: 10px;
  color: #555;
}

.settings-actions button {
  font-family: var(--nx-ui-font);
  font-size: 11px;
  padding: 4px 8px;
  background: var(--nx-panel);
  border: 1px solid #666;
  cursor: pointer;
}

.admin-gate-body {
  font-family: var(--nx-ui-font);
  font-size: 12px;
  padding: 4px;
}

.admin-gate-input {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--nx-ui-font);
  font-size: 12px;
  padding: 4px;
  margin: 6px 0;
}

.admin-gate-actions {
  display: flex;
  justify-content: flex-end;
}

.admin-gate-actions button {
  font-family: var(--nx-ui-font);
  font-size: 11px;
  padding: 4px 10px;
  background: var(--nx-panel);
  border: 1px solid #666;
  cursor: pointer;
}

.admin-gate-error {
  color: #a00;
  margin-top: 6px;
}

/* Menu/dock stay hidden for as long as the first-visit gate is open
   (app.js's FirstVisitGate) -- nothing behind the gate is reachable until
   it's actually been acknowledged. */
body.gate-open #app-menu,
body.gate-open #dock {
  display: none;
}

/* First-visit content warning (app.js's FirstVisitGate) -- same
   minimalist look as the admin gate/app menu (panel background, plain
   sans-serif, small buttons), no extra imagery. */
.first-visit-gate-body {
  font-family: var(--nx-ui-font);
  font-size: 12px;
  padding: 4px;
}

.first-visit-gate-ack {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  cursor: pointer;
}

.first-visit-gate-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 10px;
}

.first-visit-gate-actions button {
  font-family: var(--nx-ui-font);
  font-size: 11px;
  padding: 4px 10px;
  background: var(--nx-panel);
  border: 1px solid #666;
  cursor: pointer;
}

.first-visit-gate-actions button:disabled {
  opacity: 0.5;
  cursor: default;
}

.bug-report-body {
  font-family: var(--nx-ui-font);
  font-size: 12px;
  padding: 4px;
}

.bug-report-comment {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--nx-ui-font);
  font-size: 12px;
  padding: 4px;
  margin: 6px 0;
  resize: vertical;
}

/* Read-only preview of the payload that Send submits -- same width as the
   comment box, deliberately short and scrollable rather than expanding. */
.bug-report-json {
  width: 100%;
  box-sizing: border-box;
  font-family: monospace;
  font-size: 10px;
  padding: 4px;
  margin: 0 0 6px;
  resize: none;
  overflow: auto;
  color: #444;
  background: var(--nx-panel-light, #f2f2f2);
}

.bug-report-actions {
  display: flex;
  justify-content: flex-end;
}

.bug-report-actions button {
  font-family: var(--nx-ui-font);
  font-size: 11px;
  padding: 4px 10px;
  background: var(--nx-panel);
  border: 1px solid #666;
  cursor: pointer;
}

.bug-report-actions button:disabled {
  opacity: 0.5;
  cursor: default;
}

.bug-report-status {
  margin-top: 6px;
  color: #444;
}

.style-toolbar-body {
  font-family: var(--nx-ui-font);
  font-size: 11px;
  padding: 8px;
}

.style-toolbar-row {
  display: flex;
  align-items: center;
  gap: 6px;
}

.style-toolbar-row + .style-toolbar-row {
  margin-top: 8px;
}

.style-toolbar-row button {
  font-family: var(--nx-ui-font);
  font-size: 13px;
  width: 28px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--nx-panel);
  border: 1px solid #666;
  cursor: pointer;
}

.style-toolbar-row button.style-small-label {
  font-size: 9px;
}

/* Zalgo glyph's combining marks stack well above/below the base "X" --
   letting them overflow the button box (rather than clipping) is what
   makes the corrupted-text effect actually read. */
.style-toolbar-row button.style-distort {
  overflow: visible;
  font-size: 15px;
}

.style-size-group {
  display: flex;
  gap: 2px;
  margin-left: auto;
}

.style-toolbar-row input[type="color"] {
  width: 28px;
  height: 24px;
  padding: 0;
  border: 1px solid #666;
  cursor: pointer;
  margin-left: auto;
}

.settings-export-area {
  width: 100%;
  height: 100px;
  font-family: monospace;
  font-size: 10px;
}

/* Montage overlay (js/montage.js) -- a full-viewport layer that each frame is
   painted onto. z-index far above the windows' dynamic stacking (zTop starts
   at 100 and climbs) so frames always play on top. Transparent by default;
   an image with blackFill paints its own black backing. pointer-events auto
   so a click anywhere skips the rest of the montage. */
#montage-layer {
  position: fixed;
  inset: 0;
  z-index: 2000000000;
  display: none;
  pointer-events: auto;
  background-repeat: no-repeat;
  background-position: center;
  cursor: pointer;
}

/* Edit-images structured field editor (settings.js openEditImages/
   buildEditImagesFields) -- fills the window so the scrollable field area
   grows with a resize rather than staying a fixed box. */
.edit-images-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  height: 100%;
  box-sizing: border-box;
  padding: 4px;
  font-family: var(--nx-ui-font);
}

.edit-images-hint {
  font-size: 10px;
  color: #555;
  margin: 0;
}

.ei-select {
  flex: 1;
}

.edit-images-fields-scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 120px;
  border: 1px solid var(--nx-panel-dark);
}

.edit-images-fields {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 6px;
}

/* Each row's own label is a block above its input(s); a row with two
   inputs (montage/skin pairs) lays them out side by side via the nested
   <span> pairs, same two-column shape throughout the whole form. */
.edit-images-row {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 11px;
}

.edit-images-row > span:not(.edit-images-hint) {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.edit-images-row:has(> span:nth-of-type(2)) {
  flex-direction: row;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
}

.edit-images-row input[type="number"],
.edit-images-row input[type="text"],
.edit-images-row select {
  font-size: 11px;
}

.edit-images-row input[type="number"] {
  width: 64px;
}

.ei-tags {
  width: 100%;
  box-sizing: border-box;
}

.ei-dependencies {
  width: 100%;
  box-sizing: border-box;
  height: 80px;
  font-family: monospace;
  font-size: 10px;
  resize: vertical;
}

.ei-notes {
  width: 100%;
  box-sizing: border-box;
  height: 60px;
  font-size: 11px;
  resize: vertical;
}

.images-message-area {
  flex: 0 0 auto;
  width: 100%;
  box-sizing: border-box;
  height: 48px;
  font-family: monospace;
  font-size: 10px;
}

/* The edit-images-preview-layer flash (settings.js's flashMontagePreview) --
   same full-viewport shape as montage.js's own #montage-layer, but a
   separate element (see that function's comment for why). */
#edit-images-preview-layer {
  position: fixed;
  inset: 0;
  z-index: 9998;
  display: none;
  pointer-events: none;
}

/* Uniform look for every in-app JSON surface (Contents editor, Edit-images
   dependencies snippet, settings export/import popup, bug-report preview,
   citation-shorthand config): black background, white monospace text.
   Placed after each element's own rule so these colors win. */
.toc-json-editor,
.settings-export-area,
.ei-dependencies,
.bug-report-json,
.s-citation-shorthand-config {
  background: #000;
  color: #fff;
  caret-color: #fff;
}

.nx-window .nx-resize-handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 14px;
  height: 14px;
  z-index: 5; /* stays clickable above docked panels appended after it (see media-player.js dockEqPanel) */
  cursor: nwse-resize;
  background:
    linear-gradient(135deg, transparent 0 50%, var(--nx-panel-dark) 50% 60%, transparent 60% 70%,
      var(--nx-panel-dark) 70% 80%, transparent 80%);
}

/* ---------- Mobile (see Plans/archive/2026-07-13_next-steps.md's mobile design decisions) ----------
   Breakpoint matches WindowManager.isMobileLayout()/media-player.js's own
   matchMedia check, so JS and CSS branch at the same width. Desktop-canvas
   metaphor survives on phone as a pannable/zoomable canvas (not a
   single-window rewrite): #desktop stops clipping so its absolutely-
   positioned windows can extend past one screen, body becomes the native
   scroll/pinch-zoom surface, and the menu/dock switch to fixed positioning
   so they stay pinned to the screen instead of scrolling away with the
   canvas underneath them. */
@media (max-width: 768px) {
  html, body {
    overflow: auto;
    -webkit-overflow-scrolling: touch;
  }

  #desktop {
    overflow: visible;
  }

  #app-menu,
  #dock {
    position: fixed;
  }

  /* Bigger touch targets for small mouse-precise controls -- titlebar and
     its buttons grow together (a taller titlebar is the only way to give
     .nx-close/.nx-fullscreen-btn/.nx-zoom-btn a real 28px+ tap height,
     since they're flex children sized to the titlebar's own height). */
  .nx-window .nx-titlebar {
    height: 32px;
    padding: 0 10px;
    gap: 10px;
  }

  .nx-window .nx-close,
  .nx-window .nx-fullscreen-btn,
  .nx-window .nx-zoom-btn {
    width: 26px;
    height: 24px;
    font-size: 14px;
  }

  .nx-window .nx-zoom-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  .nx-window .nx-resize-handle {
    width: 28px;
    height: 28px;
  }

  #app-menu .menu-item {
    padding: 10px 12px;
  }

  /* Footnote/citation superscripts: invisible padding expands the tap
     target without growing the visible glyph or disturbing surrounding
     text layout (negative margin cancels the padding's box-model effect). */
  .nx-window .nx-body .footnote-ref,
  .nx-window .nx-body .citation-ref {
    display: inline-block;
    padding: 10px 6px;
    margin: -10px -6px;
  }

  /* Settings/Style panel rows: drop the control to its own line instead of
     squeezing it against a long label at phone width. */
  .settings-body label,
  .style-panel-body label {
    flex-wrap: wrap;
  }
}

/* Print... (app.js) tags every currently-open window .printing before
   calling window.print() and untags them all on 'afterprint' -- without
   this, the browser prints the whole desktop as literally rendered (menu,
   dock, every overlapping window, chrome and all) cut off mid-page
   instead of one page per document the user actually has open. */
@media print {
  #overlay-texture,
  #app-menu,
  #dock,
  #video-backdrop,
  #video-blackout,
  .nx-window {
    display: none !important;
  }

  .nx-window.printing {
    display: block !important;
    position: static !important;
    width: auto !important;
    height: auto !important;
    box-shadow: none !important;
    border: none !important;
    transform: none !important;
  }

  /* Every window after the first starts on a fresh page (break-before,
     not break-after, so there's no trailing blank page after the last
     one printed). */
  .nx-window.printing ~ .nx-window.printing {
    break-before: page;
    page-break-before: always;
  }

  /* The Winamp player's entire skin is CSS background-image with no
     text/foreground content at all -- browsers omit backgrounds by
     default when printing (usually gated behind a "print backgrounds"
     checkbox the user never sees), so without this override it printed
     as a blank box. Forcing color-adjust here makes it print like any
     other visible content, matching what's on screen. */
  .nx-window.printing,
  .nx-window.printing * {
    print-color-adjust: exact !important;
    -webkit-print-color-adjust: exact !important;
  }

  .nx-window.printing .nx-titlebar,
  .nx-window.printing .nav-row {
    display: none !important;
  }

  .nx-window.printing .nx-body {
    position: static !important;
    overflow: visible !important;
    /* Overrides the generic .nx-body flex:1 (flex-basis:0%), which sizes
       purely from distributed flex space -- with height forced to auto
       above (no fixed container height to distribute), that would
       collapse the body to zero height. flex:none defers sizing to the
       body's own content instead, same as a normal block. */
    flex: none !important;
    height: auto !important;
    max-height: none !important;
    direction: ltr !important;
    padding: 0 !important;
  }

  body {
    background: #fff !important;
  }
}

/* Rainbow-bracket JSON highlighter (js/json-highlight.js) -- a transparent
   textarea over a colored <pre>. Both no-wrap (white-space:pre) so they align
   line-for-line and scroll together. */
.json-hl-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 100px;
  display: block;
}
/* Follow the wrapped textarea's own hidden state (the settings-export area is
   hidden until "Export"/"Save default" fills it). Needs :has (Firefox 121+). */
.json-hl-wrap:has(> .json-hl-input[hidden]) {
  display: none;
}
.json-hl-overlay {
  position: absolute;
  inset: 0;
  margin: 0;
  box-sizing: border-box;
  overflow: hidden;
  pointer-events: none;
  white-space: pre;
  background: #000;
  color: #fff;
}
.json-hl-input.json-hl-input {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  border: none;
  background: transparent !important;
  color: transparent !important;
  caret-color: #fff !important;
  white-space: pre;
  overflow: auto;
  resize: none;
}
.jhl-b { font-weight: bold; }
.jhl-d0 { color: #ffd700; }
.jhl-d1 { color: #da70d6; }
.jhl-d2 { color: #7ec8ff; }
.jhl-d3 { color: #90ee90; }
.jhl-d4 { color: #ff8c69; }
.jhl-d5 { color: #f0e68c; }
.jhl-str { color: #9edb9e; }
