*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

/* js-hidden / initial-hidden state for elements toggled at runtime.
   a class, not an inline style, so index.html needs no 'unsafe-inline' in style-src.
   !important is required: this sits alongside a component class that sets its own
   `display` at the same specificity, so without it whichever rule is declared LATER in
   this file wins the tie regardless of which class is actually on the element. */
.js-hidden { display: none !important; }
.warp-anchor { position: relative; }

/* surface tokens / two colors across nine declarations, with invariants that break
   silently. that is why they are tokens rather than literals.

   --workspace-slate is the shell's black: body, #toolbar, #statusbar, #editorCol's
   gutters, and ALSO one surface INSIDE the editor panel meant to read as that same black
   showing through a cut in it (#docTabsFormat). that one is load-bearing: the formatting
   group reads as part of the toolbar system only for as long as it paints the exact same
   value. a drift of a few RGB points turns it into a dark box inside the panel.

   --canvas-surface is the editor panel itself, and it is FLAT. #docTabs/.doctab are
   transparent rather than duplicating this literal, and must stay that way: an opaque
   background on any of them OCCLUDES THE RIM LIGHT, which alone is disqualifying.

   the surface sits at 1.12:1 against the slate, deliberately, under WCAG's 3:1 floor for
   a non-text boundary. the surface value alone does not make the panel read as separate,
   so the two edge rims below are the ONLY thing distinguishing it from the workspace.
   anything that occludes a rim removes the panel's only boundary rather than dimming it.
   that ratio is also why no drop shadow works here: pure #000 on #0d0d0d is 1.08:1, so
   the ENTIRE dynamic range available to an outward shadow is below the perceptual floor.
   depth on this background comes from added light, never subtracted. */
:root {
  --workspace-slate: #0d0d0d;
  --canvas-surface: #1a1a1a;      /* 1.12:1 against the slate */
  /* the rim light, held as a whole gradient rather than a color: #editorUnit uses this
       SAME property for both its left and right edge, so the two cannot drift the way two
       hand-copied literals would.
       warm, matching the canvas text rather than the red accent: red is reserved for
       #editorScrollThumb, the one lit thing on these edges. brightest at the top and
       decaying downward is light from above, and these rims are the only thing carrying
       that direction.
  
       --canvas-rim-hue holds the bare R,G,B for the same reason --accent-bloom does: a
       HORIZONTAL rim segment can't reuse this vertical gradient, since compressed into a
       1px strip it averages the whole ramp and lands well below the top stop. sharing the
       components keeps the two from drifting in COLOR even though they can't share a value. */
  --canvas-rim-hue: 212, 196, 168;
  --canvas-rim: linear-gradient(180deg,
    rgba(var(--canvas-rim-hue), 0.11) 0,
    rgba(var(--canvas-rim-hue), 0.05) 45%,
    rgba(var(--canvas-rim-hue), 0.02) 100%);
  /* the lit-red accent / ONE value shared by #toolbar's bottom divider and .doctab.active's
       top border. they are meant to read as the same light, so they are a token rather than
       two hand-copied literals.
  
       --accent-lit is the line core. --accent-bloom is its halo, held as bare R,G,B because
       each place that blooms it needs its own alpha.
  
       warm and slightly orange-shifted rather than pure red: this is #editorScrollThumb's
       family, the app's established lit red. */
  --accent-lit: rgba(255, 90, 60, 0.6);
  --accent-bloom: 255, 60, 30;
}

/* the need-skulls flash and the adventure cursor both blink via infinite keyframes, pure
   decoration conveying nothing the visible text doesn't. freeze them at fully visible
   rather than disabling display, so nothing disappears for a reduced-motion user. */
@media (prefers-reduced-motion: reduce) {
  #needSkullsFlash.flashing, .modal-need-skulls-flash.flashing, .adventure-cursor, #skullWarningTitle,
  #needSkullsFlash.reward-flashing, .modal-need-skulls-flash.reward-flashing {
    animation: none !important;
    opacity: 1 !important;
  }
  /* the shake conveys nothing the banner text doesn't already carry. safe to drop outright
       rather than freeze at a mid-shake frame. */
  #skullLogoWrap.skull-shake {
    animation: none !important;
  }
  /* unlike the rules above, the glow classes ARE the entire "something new is here"
       signal. animation:none would erase the signal, not just its pulse. show a static
       version of the glow's peak state instead. */
  .codex-tab.glow, #codexWrap .spells-head.glow .feature-emoji, #mobCodex.glow, .mob-hamburger.glow::after, .build-out-label.glow, .spells-flyout-btn.glow {
    animation: none !important;
    text-shadow: 0 0 8px rgba(230,190,90,0.85) !important;
    opacity: 1 !important;
  }
  /* same freeze-at-fully-revealed treatment as the glow rules: the ellipsis widening is
       decoration on top of already-legible placeholder text, so freezing loses nothing. */
  #editor.is-empty p:first-child::after {
    animation: none !important;
    width: 5ch !important;
  }
}

/* every clickable control here is a <button> or this canvas. without this, rapid taps
   can be read as a double-tap and trigger the browser's zoom-to-that-spot. */
button, #skullLogo { touch-action: manipulation; }

html, body {
  /* 100vh, not 100%: with viewport-fit=cover plus a black-translucent status bar, iOS
       standalone shifts content up behind the status bar, but a %-based height resolves
       against a containing block that doesn't grow to compensate, leaving a gap at the
       bottom. 100vh is tied to the real viewport instead. */
  height: 100vh;
  overflow: hidden;
  background: var(--workspace-slate);
  font-family: Consolas, monospace;
  /* forces native keyboards, form controls, and system pickers into dark mode regardless
       of the device setting, so they don't flash light against Worg's dark UI. */
  color-scheme: dark;
}

::selection {
  background: rgba(180,30,30,0.30);
}

::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #1a1a1a; }
::-webkit-scrollbar-thumb { background: #6b1a1a; border-radius: 4px; }

/* toolbar */
#toolbar {
  display: flex;
  align-items: center;
  background: var(--workspace-slate);
  padding: 6px 18px;
  gap: 2px;
  flex-shrink: 0;
  user-select: none;
  /* containing block for #toolbar::after below. deliberately NO z-index: position:relative
       at z-index:auto creates no stacking context, so the toolbar's own flyouts keep
       resolving against the root context instead of being capped under this element.
       it changes no flyout's containing block either, since each anchors to its own
       positioned wrapper. the mobile block re-declares position:relative WITH a z-index on
       purpose; see its comment there. */
  position: relative;
  /* the divider is a lit line, not a drawn one. --accent-lit is shared with
       .doctab.active's top border, tokenized so the two edges cannot drift.
       --accent-bloom is this rule's alone at the moment; if the tab's matching halo
       returns, it takes this token with it. */
  border-bottom: 1px solid var(--accent-lit);
  /* the upward half of the glow, blooming into the toolbar's own black.
       spread = -blur is what confines an inset shadow to the single edge its offset points
       at. without it the blur wraps all four inner edges. */
  box-shadow: inset 0 -6px 7px -6px rgba(var(--accent-bloom), 0.18);
}
/* the downward half, falling onto #editorUnit's panel and #editorCol's gutters.
   this must be a positioned pseudo-element and not an outset box-shadow on #toolbar:
   #editorUnit is at z-index 2 and #editorCol paints its own opaque background later in
   DOM order, so an outset shadow lands underneath both and is invisible below the bar.
   z-index 5 clears #editorUnit's 2, the wall lines' 3, and the thumb's 4.
   top: calc(100% + 1px), not 100%: percentages on an absolutely positioned box resolve
   against the containing block's PADDING box, which stops just ABOVE the border-bottom,
   so at a bare 100% the glow starts on top of the very line it is cast by.
   dimming states need no handling: their opacity and filter on #toolbar apply here too. */
#toolbar::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 1px);
  height: 7px;
  z-index: 5;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(var(--accent-bloom), 0.12) 0,
    rgba(var(--accent-bloom), 0) 100%);
}

.tb-btn {
  background: transparent;
  color: #8a8a7a;
  border: none;
  border-radius: 4px;
  padding: 6px 12px;
  font-family: Consolas, monospace;
  font-size: 13px;
  font-weight: normal;
  text-transform: uppercase;
  min-width: 30px;
  cursor: pointer;
  line-height: 1.4;
}
.tb-btn:hover { background: rgba(180,30,30,0.35); color: #e0d0c0; }
.tb-btn.active { background: rgba(180,30,30,0.55); color: #f0e0d0; }

.tb-sep {
  width: 1px;
  height: 32px;
  background: rgba(180,30,30,0.25);
  margin: 0 8px;
  flex-shrink: 0;
}
/* divides the inline icons from the block-style icons within #docTabsFormat, scoped past
   the base .tb-sep line so only this one becomes a single faint red dot. */
#docTabsFormat .tb-sep {
  width: 2px;
  height: 2px;
  background: rgba(220, 60, 60, 0.5);
}

/* chapter-heading style dropdown / Worgish, the cheat-code-earned pixel heading font.
   no separate caret control: #btnCh does double duty once Worgish is unlocked, applying
   the format AND opening this menu off the same click.
   click-to-open, not hover, matching the selection toolbars rather than .spells-flyout's
   hover mechanics. a deliberate one-off for the current heading, not a navigation menu. */
.ch-btn-wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.ch-style-menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  flex-direction: column;
  min-width: 220px;
  margin-top: 4px;
  background: rgba(20, 15, 10, 0.97);
  border: 1px solid rgba(180, 30, 30, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
  padding: 4px;
}
.ch-style-menu.open { display: flex; }
.ch-style-item {
  background: transparent;
  color: #c8b8a0;
  border: none;
  border-radius: 3px;
  text-align: left;
  padding: 8px 10px;
  font-family: Consolas, monospace;
  font-size: 12.5px;
  text-transform: none;
  cursor: pointer;
}
.ch-style-item:hover { background: rgba(180,30,30,0.35); color: #e0d0c0; }
.ch-style-item[aria-checked="true"] { color: #e6be5a; }
.ch-style-item:disabled {
  opacity: 0.4;
  cursor: default;
  pointer-events: none;
}

/* fonts/worgish.otf, generated by scripts/build-worgish-font.js rather than
   hand-authored. a self-hosted static asset like any sprite here.
   .otf, not .woff2: the glyph set is 43 glyphs and the file is already a few KB, and
   browsers load .otf via @font-face natively, so compression would save negligible bytes
   for real added tooling. */
@font-face {
  font-family: 'Worgish';
  src: url('fonts/worgish.otf') format('opentype');
  font-weight: normal;
  font-style: normal;
}

/* Worgish chapter-heading rendering. text-transform does the all-caps enforcement, and
   glyph selection happens AFTER the transform, so the stored text keeps whatever case the
   writer typed. nothing here mutates document content.
   no lowercase glyphs exist in the font at all, so the transform guarantees they are
   never requested.
   the color matches the toolbar wordmark's own pixel-title red exactly. */
#editor h1[data-worgish] {
  font-family: 'Worgish', Consolas, monospace;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: #B02010;
}

.feature-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 2px;
  gap: 0;
}
.feature-emoji {
  font-size: 14px;
  line-height: 1;
  color: #8a8a7a;
  background: transparent;
  pointer-events: none;
}
/* hidden, box preserved via visibility rather than display, until applyMenuIcons() marks
   body.icons-ready, whether it swapped this span's emoji for pixel art or left the emoji
   in place. module scripts run after first paint, so without this the static emoji
   briefly flashes before the swap lands. */
body:not(.icons-ready) .feature-emoji {
  visibility: hidden;
}
/* owner-set pixel-art menu icons, replacing a button's leading emoji. fixed px, not 1em,
   so they scale independently of the button's font-size. */
.menu-icon {
  height: 32px;
  width: auto;
  image-rendering: pixelated;
  vertical-align: middle;
}
/* the format sprites sit next to plain-text siblings until designed, so they are sized
   down from the 32px menu icons and don't tower over whichever is still text. */
.format-icon {
  height: 15px;
}
/* these icons swap to separately-drawn hover art whose pixel-grid aspect ratio isn't
   always the base icon's. with width:auto that swap changed the rendered width and pushed
   every button after it sideways on hover. a fixed square box plus object-fit:contain
   means the box never changes size, only what is drawn inside it. */
.spells-flyout-btn .menu-icon {
  width: 32px;
  object-fit: contain;
}
/* Warp is landscape and, unlike the case above, its base icon and both hover frames all
   share one canvas size, so there is no cross-frame mismatch to guard against. freed from
   the square box it fills its full height instead of being letterboxed. */
#btnWarp .menu-icon {
  width: auto;
}
.feature-btn {
  background: transparent;
  color: #8a8a7a;
  border: none;
  border-radius: 4px;
  padding: 4px 10px 5px 10px;
  font-family: Consolas, monospace;
  font-size: 13px;
  font-style: normal;
  font-weight: normal;
  text-transform: uppercase;
  cursor: pointer;
  position: relative;
}
.feature-btn:hover { background: rgba(180,30,30,0.35); color: #e0d0c0; }
.feature-btn.active { background: rgba(180,30,30,0.55); color: #f0e0d0; }
.feature-btn:disabled { opacity: 0.4; cursor: default; pointer-events: none; }
/* these heads render icon-only, so this sits empty below the icon in the column stack.
   its padding still added height there, pulling those icons' vertical center up relative
   to the single-row format buttons. collapsing when empty fixes that, and it un-collapses
   automatically the moment a label is added back. */
.feature-btn:empty { padding: 0; min-height: 0; }

/* spells submenus, inline expand */
.spells-wrap {
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: center;
  margin: 0 14px;
}
.spells-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  border-radius: 4px;
}
/* click-to-pin target. .feature-emoji has pointer-events:none and .feature-btn collapses
   to 0x0 when empty, so .spells-head is the only element in this stack with the icon's
   actual clickable box. the hover and pinned highlight live here rather than on either. */
.spells-head:hover { background: rgba(180,30,30,0.35); }
.spells-head.active { background: rgba(180,30,30,0.55); }
/* the Codex icon's "new item waiting" glow targets .feature-emoji, not .spells-head:
   the head div has no direct text content of its own, and buildOutGlow's text-shadow needs
   an actual glyph to render against. */
#codexWrap .spells-head.glow .feature-emoji {
  animation: buildOutGlow 1.6s ease-in-out infinite;
}
/* the submenu lives on the same bar, collapsed to zero width until hovered. animating the
   grid track from 0fr to 1fr makes the revealed width equal the real content width every
   frame, so the items and the toolbar contents they push move in lockstep. */
.spells-flyout {
  display: grid;
  grid-template-columns: 0fr;
  transition: grid-template-columns 0.4s ease-out;
  /* without this, the leftmost item's highlight sits flush against .spells-head's icon
       highlight with no seam, reading as one smeared highlight rather than two controls. */
  margin-left: 6px;
}
.spells-flyout-inner {
  min-width: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  white-space: nowrap;
}
/* header label identifying which flyout this is, since the parent icon carries no text.
   gold rather than this bar's usual red so it reads as a title, not another clickable
   item, and static so it isn't mistaken for buildOutGlow's own pulse. */
.spells-flyout-header {
  margin-left: 12px;
  padding: 4px 12px 1px 0;
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #d8b34a;
  text-shadow: 0 0 6px rgba(216,179,74,0.5);
  border-bottom: 1px solid rgba(216,179,74,0.35);
  pointer-events: none;
  user-select: none;
}
.spells-flyout-items {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  padding-top: 2px;
  white-space: nowrap;
}
.spells-flyout-items > * { flex: 0 0 auto; }
.spells-wrap:hover .spells-flyout,
/* :has(:focus-visible), not :focus-within. a flyout item that opens a modal gets its
   focus explicitly restored by closeDialogFocus() when that modal closes, and plain
   :focus-within would match that restored focus regardless of how the dialog was opened,
   leaving the flyout stuck open after a mouse click.
   :focus-visible only matches when the browser's input-modality heuristic says focus
   should be visibly indicated: true for a real keyboard flow, false for a click's
   restored focus. */
.spells-wrap:has(:focus-visible) .spells-flyout,
.spells-wrap.flyout-pinned .spells-flyout,
/* short close-only grace period after the pointer leaves. see main.js's
   wireSpellsFlyoutHoverGrace() for why plain :hover retracts too eagerly on a fast exit. */
.spells-wrap.flyout-hover-grace .spells-flyout {
  grid-template-columns: 1fr;
}
/* keep the inner clipped throughout the reveal so items are uncovered progressively by
   the growing track, in lockstep with the items they push. only the pinned state needs
   overflow:visible, for the Warp dropdown escaping downward, and by then it is fully open. */
.spells-wrap.flyout-pinned .spells-flyout-inner {
  overflow: visible;
}
.spells-flyout-btn {
  background: transparent;
  border: none;
  color: #8a8a7a;
  padding: 6px 12px;
  font-family: Consolas, monospace;
  font-size: 11px;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
}
.spells-flyout-btn:hover { background: rgba(180,30,30,0.35); color: #e0d0c0; }
.spells-flyout-btn.active { background: rgba(180,30,30,0.55); color: #f0e0d0; }
.spells-flyout-btn:disabled { opacity: 0.4; cursor: default; pointer-events: none; }
/* the Codex flyout items mirror their modal tab's own "something new" glow. */
.spells-flyout-btn.glow { animation: buildOutGlow 1.6s ease-in-out infinite; }
/* need-skulls arcade flash */
#needSkullsFlash {
  display: none;
  font-family: Consolas, monospace;
  font-size: 12px;
  font-weight: bold;
  letter-spacing: 1px;
  color: #ff3020;
  text-shadow: 0 0 6px rgba(255,40,10,0.8), 0 0 14px rgba(180,0,0,0.5);
  text-transform: uppercase;
  padding: 0 10px;
  white-space: nowrap;
  user-select: none;
  pointer-events: none;
}
#needSkullsFlash.flashing {
  display: block;
  animation: needSkullsBlink 0.3s steps(1) 6;
}
@keyframes needSkullsBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.1; }
}

/* gold reward treatment, deliberately a different color AND a different motion shape
   from the red blink above, not just a text swap: the two were once visually identical,
   which is why a genuine reward barely registered next to a routine rejection.
   gold reuses this file's own buildOutGlow palette rather than inventing a color. motion
   is a quick stepped flicker-in that then holds steady at full glow for the rest of the
   banner's duration: an arrival, not a repeating alarm.
   duration and removal are JS-driven; this owns only the flicker-in. */
#needSkullsFlash.reward-flashing, .modal-need-skulls-flash.reward-flashing {
  display: block;
  color: #e6be5a;
  text-shadow: 0 0 6px rgba(230,190,90,0.8), 0 0 14px rgba(230,190,90,0.5);
  animation: rewardFlicker 0.3s steps(1) 2;
}
@keyframes rewardFlicker {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.15; }
}

/* gold mini-skull inside the reward banner, drawn by hearth.js and shown only while the
   banner is in its reward state. inline next to the text, not positioned separately.
   only the toolbar copy gets this nested canvas; the in-modal copy gets the gold text
   treatment without the icon. */
.reward-flash-skull {
  display: none;
  width: 14px;
  height: 14px;
  vertical-align: middle;
  margin-left: 6px;
  image-rendering: pixelated;
  pointer-events: none;
}
#needSkullsFlash.reward-flashing .reward-flash-skull {
  display: inline-block;
}

/* same red arcade-flash treatment for the copy inside an already-open modal's header.
   the toolbar's copy is blurred and dimmed behind an open modal, so an in-panel spend
   needs its own legible copy rather than relying on the backgrounded one. */
.modal-need-skulls-flash {
  display: none;
  font-family: Consolas, monospace;
  font-size: 12px;
  font-weight: bold;
  letter-spacing: 1px;
  color: #ff3020;
  text-shadow: 0 0 6px rgba(255,40,10,0.8), 0 0 14px rgba(180,0,0,0.5);
  text-transform: uppercase;
  white-space: nowrap;
  user-select: none;
  pointer-events: none;
}
.modal-need-skulls-flash.flashing {
  display: block;
  animation: needSkullsBlink 0.3s steps(1) 6;
}

.tb-spacer { flex: 1; }

#saveLabel {
  color: #6a5a4a;
  font-family: Consolas, monospace;
  font-size: 11px;
  min-width: 60px;
  text-align: center;
}

/* at-a-glance "is this document actually safe" dot, deliberately binary, unlike
   #saveLabel's text which distinguishes Drive from device. .risk reuses the app's existing
   danger red; .safe is the one new color, since nothing in the palette reads as success. */
#saveBadge {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 2px;
}
#saveBadge.risk { background: #B02010; }
#saveBadge.safe { background: #5a8f5a; }

/* the dark square lives on the wrapper's CSS background, not painted inside the canvas,
   so it inverts with the rest of the chrome under Solstice while the canvas itself, which
   is excluded from that inversion, keeps the skull's colors unchanged. */
#skullLogoWrap {
  width: 32px;
  height: 32px;
  margin-right: 4px;
  flex-shrink: 0;
  background: var(--workspace-slate);
  cursor: pointer;
}
#skullLogo {
  width: 100%;
  height: 100%;
  display: block;
  user-select: none;
  image-rendering: pixelated;
  transition: transform 0.15s ease;
}
/* subtle sink on hover and focus, matching .spells-flyout-btn's own :active transform
   rather than inventing a new affordance.
   :focus-visible, not :focus-within: the canvas is tabindex="0", so a plain click leaves
   it focused, and :focus-within matches ANY focus however acquired, which left the sink
   stuck until focus moved elsewhere instead of releasing on mouseup like a real button. */
#skullLogoWrap:hover #skullLogo,
#skullLogo:focus-visible {
  transform: translateY(2px);
}
#skullLogoWrap:active #skullLogo {
  transform: translateY(3px) scale(0.94);
}

/* the not-enough-skulls reflex, toggled alongside the banner.
   on the wrapper, not the canvas: the canvas already owns `transform` for the sink above,
   and animating a second transform source on one element would have them clobber. */
@keyframes skullShake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-3px) rotate(-6deg); }
  40% { transform: translateX(3px) rotate(6deg); }
  60% { transform: translateX(-2px) rotate(-3deg); }
  80% { transform: translateX(2px) rotate(3deg); }
}
#skullLogoWrap.skull-shake {
  animation: skullShake 0.4s ease;
}

#worgLabel {
  color: #B02010;
  font-family: Consolas, monospace;
  font-size: 28px;
  font-style: italic;
  font-weight: bold;
  letter-spacing: 2px;
  text-transform: uppercase;
  user-select: none;
  margin-right: 10px;
}

/* warp menu */
#warpMenu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  background: #151515;
  border: 1px solid rgba(180,30,30,0.20);
  padding: 4px 0;
  min-width: 200px;
  max-height: 400px;
  overflow-y: auto;
  z-index: 1000;
}
#warpMenu .warp-item {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  color: #b0a890;
  padding: 5px 20px;
  text-align: left;
  font-family: Consolas, monospace;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
}
#warpMenu .warp-item:hover { background: rgba(180,30,30,0.35); }
#warpMenu .warp-item.disabled {
  color: #6a5a4a;
  cursor: default;
  pointer-events: none;
}

/* warp teleport / the real scroll jump is instant and imperceptible under this overlay,
   which plays a brief hyperspace streak in the travel direction.
   kept well under half a second: Warp is a quick mid-writing utility, not a cutscene. */
#warpFx {
  display: none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 20;
  /* screen-blend so the amber streaks glow over the dark page, drawn on transparent
       canvas. the buffer size is set in JS for devicePixelRatio sharpness. */
  mix-blend-mode: screen;
}
@media (prefers-reduced-motion: reduce) {
  /* JS already skips the effect under reduced motion; this is the safety net. */
  #warpFx { display: none !important; }
}

/* main layout */
#app {
  display: flex;
  flex-direction: column;
  /* containing block for whatever is absolutely positioned against the app shell. kept
       even where nothing depends on it, since removing it would silently repoint any future
       absolutely-positioned direct child of #app at the viewport instead. */
  position: relative;
  height: 100vh;
  height: 100lvh;
  /* --app-height is kept in sync with visualViewport.height by viewport.js, but ONLY while
       a real on-screen keyboard is open.
       at rest this falls back to 100lvh, the true full screen height. 100dvh under-reports
       by env(safe-area-inset-top) in iOS standalone with a black-translucent status bar. */
  height: var(--app-height, 100lvh);
}

/* solstice */
/* flips every background and text color at once via a filter instead of a second
   hand-maintained palette: there are ~1500 lines of one-off hex colors here and no theme
   variable system to swap.
   invert(1) hue-rotate(180deg) is the standard light-mode-via-filter trick, where the
   hue-rotate corrects the shift plain invert() leaves on saturated colors so reds come
   back out as reds.
   applied to `body`, not `#app`: #app and its descendants are transparent, so the dark
   backdrop behind the editor is actually body's, and filtering only #app left it showing
   through unfiltered. body also covers the Skullmage iframe, which sits outside #app.
   applying the SAME filter again to a descendant cancels the ancestor's inversion, since
   two inverts are identity. that is how every hand-drawn pixel asset stays untouched. */
body.solstice-active { filter: invert(1) hue-rotate(180deg); }
body.solstice-active .menu-icon,
body.solstice-active #skullCounterCanvas,
body.solstice-active #skullEscapeCanvas,
body.solstice-active #skullEscapeStartScreen,
body.solstice-active #skullLogo,
body.solstice-active #toolbarWorg,
body.solstice-active #typingSprite,
body.solstice-active #mobSkullIcon,
body.solstice-active #wordCountSkull,
body.solstice-active #frameModalBorder,
body.solstice-active #frameModalFrame {
  filter: invert(1) hue-rotate(180deg);
}

/* warm glow overlay / invert plus hue-rotate alone cannot add color to Solstice's bright
   side, because the dark theme's backgrounds are perfectly neutral gray with zero chroma
   to rotate, so a plain invert reads as stark colorless white rather than daylight. this
   layer paints a literal warm gradient on top instead.
   it uses the same cancel-the-ancestor's-inversion trick as the pixel-art exclusions, so
   the rgba stops below are the actual displayed colors rather than a pre-inverted guess.
   that is provably exact: invert plus hue-rotate is an affine per-pixel map, and an
   affine map distributes exactly over alpha compositing.
   deliberately NOT a blend mode even though one could look nice: blend modes are
   non-linear, that identity breaks, and the result could only be tuned by eye. */
body.solstice-active::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 30;
  pointer-events: none;
  background: radial-gradient(ellipse 105% 105% at 85% -19%,
    rgba(239, 149, 82, 0.43) 0%,
    rgba(231, 131, 69, 0.31) 25%,
    rgba(223, 114, 57, 0.20) 55%,
    rgba(216, 98, 50, 0.12) 100%);
  filter: invert(1) hue-rotate(180deg);
}

/* document tabs */
/* one tab per open document. only the active tab's document is live in #editor;
   switching content-swaps the editor rather than keeping N documents rendered. */
/* a real header INSIDE #editorUnit's panel, its first child, not a separately-aligned
   floating bar above it. no background of its own, so the panel's surface shows through
   uniformly.
   being a normal DOM child means it inherits the panel's marginLeft, width, and transform
   for free, so tabs.js has no alignment code of its own. */
#docTabs {
  display: flex;
  align-items: stretch;
  gap: 0;
  flex-shrink: 0;
  user-select: none;
  /* transparent, NOT a literal matching --canvas-surface. an opaque background here
       occludes #editorUnit's LEFT rim light for this row's full height, and the left rim is
       the one that must run uninterrupted. don't simplify this back to a literal. */
  background: transparent;
  /* top only. without this the formatting buttons' own content is the tallest thing in the
       row, so it exactly fills the bar's height with no breathing room and the highlight
       touches the top edge flush.
       it must live HERE, on the container, not as a padding-top on #docTabsFormat: the icons
       are centered against this row's full height, so taking the space out of that element's
       own content box leaves them visibly cramped.
       no padding-bottom: that breathing room is each .doctab's own border-bottom instead,
       per-tab rather than shared, which a shared container padding can't override per-child.
       #docTabsFormat needs no matching border-bottom, since .doctab is already the tallest
       thing in the row and align-items:stretch grows it to match either way. */
  padding-top: 3px;
}
/* fills #docTabs' own width, which is #editorUnit's width, inherited. the children lay
   out side by side across it exactly as before; only the OUTER positioning is gone, since
   nesting makes it redundant. */
#docTabsUnit {
  display: flex;
  align-items: stretch;
  flex: 1 1 auto;
  min-width: 0;
}
/* the scrollable tab group, content-sized rather than flex:1, so it takes only as much
   width as the open tabs need and #docTabsFormat sits snug against it instead of being
   shoved to the far end.
   min-width:0 is what still lets it SHRINK below that natural width once the tabs exceed
   the available budget.
   tabs are fixed-width and don't compress: MAX_TABS caps the strip at 5, so its worst case
   is a fixed, known quantity that always fits. long titles truncate via ellipsis instead. */
#docTabsList {
  display: flex;
  /* flex-end, not stretch. anchoring to the bottom keeps every tab's border-bottom, the
       seam against the panel below, on one line, and it is the right default for any future
       control in this row that isn't tab-height. */
  align-items: flex-end;
  gap: 2px;
  /* left is deliberately 0, not symmetric with the right: the first tab's own box needs to
       sit flush against #editorUnit's left edge, the same edge the wall lines bracket. its
       text keeps its own inset via .doctab's padding. a left padding here would visibly
       offset the strip from that edge. */
  padding: 0 10px 0 0;
  flex: 0 1 auto;
  min-width: 0;
}
/* that padding-right gives the "+" button room before the strip's right edge. once open
   tabs hit MAX_TABS renderTabStrip() stops rendering "+", so the padding has nothing left
   to buffer and would just be a bare sliver of empty space past the last tab. */
#docTabsList:not(:has(.doctab-new)) { padding-right: 0; }
#docTabsList:empty { display: none; }
/* only the "+" shows when a single document is open, since renderTabStrip skips the
   redundant one-tab row. trim the row to fit it snugly rather than leaving tab-height
   empty space around it. */
#docTabsList.single { padding: 0 6px; gap: 0; }
/* the text formatting controls, relocated here from the global #toolbar: they only make
   sense with an open document, so they live on the canvas's own bar rather than the
   app-wide navigation row. flex-shrink:0 is the other half of #docTabsList's min-width:0,
   so this group is never squeezed. */
#docTabsFormat {
  display: flex;
  align-items: center;
  gap: 2px;
  /* flex-grow:1 claims whatever width #docTabsList doesn't need, so this box and its
       background reach all the way to #editorUnit's right edge instead of stopping at the
       last icon. the icons themselves stay packed left within that wider box. */
  flex: 1 0 auto;
  /* containing block for the ::after rim segment below. safe on a flex item: it changes
       nothing about this element's layout, and the only absolutely-positioned descendant
       already has its own positioned wrapper as its containing block. */
  position: relative;
  padding: 0 10px;
  /* cancels #docTabs's own padding-top, which this sibling doesn't need but inherits as a
       side effect of sharing the flex row. left alone it pushed this element's slate
       background down, leaving a thin strip of panel surface exposed above the icons at the
       panel's top-right corner.
       this must stay a negative MARGIN here rather than a padding-top with #docTabs's own
       padding removed: these icons are centered against the row's full height, and a
       padding-top takes that space out of their content box and cramps the strip. */
  margin-top: -3px;
  /* paints the app shell's black, the same token #toolbar and #statusbar use, so the
       formatting icons read as part of the app-wide toolbar system they were relocated from,
       as the black background continuing THROUGH a cut in the panel. this must stay the
       exact same value as those elements.
       being opaque, this element also occludes #editorUnit's RIGHT rim light for exactly its
       own height, with no measurement and no hardcoded constant. that is required, not
       incidental. */
  background: var(--workspace-slate);
  /* scoped to this element's own box, and since #docTabsList sits beside it in the same
       flex row this never touches the tab strip.
       inset plus a negative y-offset hugs the bottom inner edge and blurs upward into the
       toolbar rather than casting onto the canvas below.
       the negative spread matching the blur radius is load-bearing: with spread:0 an inset
       shadow's blur still wraps all four inner edges, since only the offset pulls it toward
       the bottom. shrinking the shadow rectangle by the blur amount pulls it fully off the
       other three, leaving only the bottom edge with any visible shadow. */
  box-shadow: inset 0 -4px 4px -4px rgba(0, 0, 0, 0.45);
}
/* carries #editorUnit's rim light across the bottom of the black cut this element makes
   in the panel. the right rim stops dead at this element and picks up again below it, so
   without this the panel's boundary vanished for the height of the tab row. this closes
   that gap into an L.

   it ends at the doctabs for free, with no measurement: left/right:0 span this element's
   own box, whose left edge IS where #docTabsList stops and whose right edge is
   #editorUnit's. nothing to keep in sync if the tab count changes.

   a pseudo-element, not a background layer on the element itself: the inset shadow above
   hugs this exact edge and paints ABOVE backgrounds but BELOW content, so a
   background-image rim would be muted by the shadow. this paints over it instead.

   THIS ALPHA IS DERIVED, NOT CHOSEN. it is matched on ABSOLUTE LUMINANCE, not on the
   vertical rim's own alpha: this segment meets that rim at a corner, so what must match is
   the grey they land on, not the light applied. reusing the ramp's own top alpha puts this
   at RGB ~35 over the slate's 13, a lift at or under the visibility floor that reads as
   simply absent. the current value lands it flush with the rim it continues.
   it tracks --canvas-surface and must be recomputed whenever that moves: solve
   rim_grey = a*212 + (1-a)*13 for the new rim grey. leaving it stale doesn't fail loudly,
   it just makes the two halves of the corner miss each other by a few RGB points. */
#docTabsFormat::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  pointer-events: none;
  background: rgba(var(--canvas-rim-hue), 0.17);
}

.doctab {
  display: flex;
  align-items: center;
  gap: 8px;
  /* transparent, so the panel's own surface shows through and the tab reads as part of one
       continuous slab rather than a chip sitting on it. also keeps the left rim unoccluded,
       the reason that outlives every other one here. */
  background: transparent;
  color: #666666;
  border: none;
  border-right: 1px solid rgba(255, 255, 255, 0.06);
  /* 1px, matching #toolbar's border-bottom exactly: .doctab.active paints this edge with
       the same --accent-lit token, and the two are meant to be the same line.
       declared here rather than only on .active so every tab reserves the same 1px and the
       active one doesn't sit a pixel lower than its neighbors.
       changing this width alone re-breaks the match; a test pins the pair. */
  border-top: 1px solid transparent;
  /* bottom 3px stands in for a shared container padding, moved onto each tab individually
       so the active one could override just its own strip. transparent: showing the panel
       through is strictly more robust than a literal and costs nothing.
       nothing overrides the color anymore, but the 3px itself is still load-bearing spacing.
       keep this fully inside the border box: a sibling's overflow-x once computed overflow-y
       to auto as well, so anything escaping a tab's own border box never painted. */
  border-bottom: 3px solid transparent;
  /* the row is FLAT IN HEIGHT, on purpose. every tab is the same height and carries no fill
       or rail of its own. */
  padding: 8px 8px 8px 12px;
  font-family: Consolas, monospace;
  font-size: 12.5px;
  cursor: pointer;
  /* fixed, not individually shrinkable: every tab is the same width regardless of how many
       are open or how long their titles are. no scrolling, no shrinking. long titles
       truncate via .doctab-label's ellipsis instead. */
  width: 100px;
  flex-shrink: 0;
}
/* inactive tabs paint NOTHING: no wash, no rail, no edge of their own. an inactive tab is
   just the panel's own surface with a dimmer label on it, and the active tab carries the
   entire distinction.
   before adding anything here, know the hazard: tab 1 sits flush at x=0 directly over
   #editorUnit's left rim, and that rim runs the panel's full height, so the tab row's
   slice of it is shared, not the tab's to paint. a treatment applied in only ONE of a
   tab's two states dims and re-brightens that column on every switch, a visible flicker
   with nothing on screen to explain it. either hold the treatment off the first tab's
   leftmost 1px, or paint SOMETHING in both states so there is no swing. */
.doctab:hover { background: rgba(180,30,30,0.16); color: #d8c8b0; }
/* the active tab is marked by a red top edge and a brighter label, nothing else. it has
   no fill of its own: the strip is a real header inside the panel, not a color-matched row
   above it, so there is no seam to match.

   the color is var(--accent-lit), literally the token #toolbar's divider paints, not a
   hand-matched red. that is the point of the token: the two lit edges cannot drift, and a
   change to one is a change to both. the 1px weight is declared on .doctab itself so every
   tab reserves the same pixel; only the COLOR is set here.

   no halo. if one is ever reinstated it must copy #toolbar's numbers per SIDE rather than
   per mechanism, since this tab is the other way up, and it must hug the LINE: a wide red
   falloff over the tab's face lights the PLATE instead.

   `background: transparent` is NOT redundant with .doctab's own: it re-overrides
   .doctab:hover at the same specificity declared earlier, so the active tab keeps not
   taking the red hover tint. removing it silently starts tinting the active tab on hover. */
.doctab.active {
  background: transparent;
  color: #d4c4a8;
  border-top-color: var(--accent-lit);
}

.doctab-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* flex items default to min-width:auto, which floors them at their content's intrinsic
       width. for nowrap text that is the full unwrapped label, silently defeating the
       ellipsis and letting a long title overflow the tab's fixed width. this override is
       what actually lets the ellipsis engage. */
  min-width: 0;
}

.doctab-close {
  width: 14px;
  height: 14px;
  line-height: 14px;
  text-align: center;
  border-radius: 3px;
  font-size: 12px;
  flex-shrink: 0;
  opacity: 0;
}
/* the :focus-within/:focus-visible twins are the keyboard equivalents of these two hover
   rules. .doctab-close starts at opacity 0 but stays in the tab order, so without them a
   keyboard writer can focus, and activate, an invisible close button. enforced by
   test/unit/a11y-baseline.test.js. */
.doctab:hover .doctab-close,
.doctab:focus-within .doctab-close { opacity: 0.7; }
.doctab-close:hover,
.doctab-close:focus-visible { opacity: 1 !important; background: rgba(180,30,30,0.4); color: #f0e0d0; }

.doctab-new {
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: 3px 0;
  padding: 0;
  width: 24px;
  height: 24px;
  border-radius: 4px;
  border: none;
  background: transparent;
  color: #8a8a7a;
  font-family: Consolas, monospace;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
}
.doctab-new:hover { background: rgba(180,30,30,0.35); color: #e0d0c0; }
/* multi-tab state only. #docTabsList's own gap already separates this from the last tab
   on the left, but its padding-right is the only thing separating it from the right edge,
   an uneven frame. this adds enough margin-left to bring the left side up to the same
   total, so the button sits in a symmetric slot.
   the single-tab state's padding is already symmetric with no preceding tab. */
.doctab + .doctab-new { margin-left: 8px; }

#middle {
  display: flex;
  flex: 1;
  overflow: hidden;
}

#editorCol {
  flex: 1;
  display: flex;
  flex-direction: column;
  /* no top or bottom padding, deliberately: #editorUnit must stretch flush from #toolbar's
       bottom edge down to #statusbar's top edge with zero gap either side.
       horizontal padding stays, being the gutter the wall lines and side panels dock
       against, unrelated to this vertical seam. */
  padding: 0 12px;
  overflow: hidden;
  /* workspace slate, the same token body and #toolbar paint, so the whole shell reads as
       one continuous dark room. #editorUnit is the thing that reads as a lit surface; this
       element is the floor it rests on. */
  background: var(--workspace-slate);
}

/* editor unit / title, author, and canvas grouped as one movable box */
/* applyEditorUnitLayout() sets marginLeft, width, and transform HERE once, rather than on
   each child. the wall lines are deliberately NOT nested inside.
   flex column reproduces the vertical stacking #editorCol gave these children directly
   before this wrapper existed; min-height:0 keeps #editorScroll free to actually shrink
   and scroll instead of being blocked by a nested flex column's default auto min-height. */
#editorUnit {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  box-sizing: border-box;
  /* single source of truth for the left/right text inset shared by the title, author row,
       and editor. a custom property rather than a shared ancestor padding, so each element's
       own box is unaffected. declared here, and inherited down, rather than on :root, since
       it is specific to this panel's layout. */
  --canvas-text-inset: 9px;
  /* the panel slab / painted first-to-last as left rim, right rim, over a FLAT surface.
       together these are the ENTIRE depth treatment; there is deliberately no box-shadow.
  
       THE RIMS ARE THE PANEL'S ONLY EDGE. the wall lines carry no fill of their own and
       #editorScrollThumb only appears once the document overflows, so without these the
       panel's sole boundary is a flat color step.
  
       they live HERE, on this element's background layer, rather than on the wall lines, for
       one reason: a child's opaque background always paints above its parent's, so
       #docTabsFormat occludes the RIGHT rim for exactly its own height, automatically, with
       no measurement and no constant to drift out of sync.
  
       that interruption is required, not incidental. the formatting group is a cut THROUGH
       the slab, so there is no material there for an edge light to belong to: a rim running
       past it would have black on both sides, fencing the black in and announcing it as a
       bounded object instead of the page continuing through. the LEFT rim passes through the
       tab row uninterrupted, because material genuinely is there on that side. left
       continuous, right interrupted, is what makes the top-right corner read as a notch
       rather than an asymmetry bug. do not "fix" it by matching the two.
  
       both rims are clipped from the same full-height gradient, so the right one emerges
       already partway down its own falloff. restarting it below the toolbar would make the
       right edge brighter than the left at the same height.
  
       no padding or overflow here deliberately: padding would double up with the children's
       own text inset, and overflow:hidden would risk clipping the fields' focus ring, which
       sits flush against this element's edges. */
  background-color: var(--canvas-surface);
  background-image: var(--canvas-rim), var(--canvas-rim);
  /* 1px columns pinned to each edge, inside the box. both composite over the surface, so
       the two rims render identically. */
  background-size: 1px 100%, 1px 100%;
  background-position: left top, right top;
  background-repeat: no-repeat;
  /* pre-JS FOUC guard: applyEditorUnitLayout() is a module script running after first
       paint, and without a matching default here this flex column child has no width of its
       own, so align-items:stretch fills it to #editorCol's full content width for that first
       frame, a visible full-bleed flash that snaps narrow once the script runs.
       matching the same centered shape by CSS alone removes that frame; the script overwrites
       both with precise px values immediately after. */
  width: 60%;
  margin: 0 auto;
  padding: 0;
  border-radius: 0;
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
  /* the opaque slab sits BELOW the wall lines' z-index, not above: those lines are meant to
       read as a border sitting ON this panel's edge pixel, fully lit on both sides of it,
       not a backlight half-swallowed by this panel's own opaque background.
       #editorScrollThumb sits one level higher still, being the one part of the line meant
       to stay visibly on top and draggable where it overlaps the edge. */
  z-index: 2;
}

/* title */
#titleEdit {
  background: transparent;
  border: none;
  outline: none;
  color: #d4c4a8;
  font-family: Consolas, monospace;
  font-size: clamp(28px, 2vw, 42px);
  font-weight: bold;
  text-transform: uppercase;
  box-sizing: border-box;
  /* --canvas-text-inset, matching the author row and editor, so the title sits the same
       distance in from the boundary lines on both sides. the lines are positioned flush
       against this element's own box edges, so this padding IS the gap. */
  padding: 0 var(--canvas-text-inset);
  /* fills #editorUnit's own width. that element, not this one, is what
       applyEditorUnitLayout() sizes against the viewport. */
  width: 100%;
  /* the gap between the doctab strip directly above and this field's own top edge. */
  margin: 6px 0 4px 0;
}
#titleEdit::placeholder { color: #4a4a3a; }
#titleEdit::selection { background: rgba(180,30,30,0.35); }

#authorRow {
  display: flex;
  align-items: baseline;
  gap: 0.4em;
  box-sizing: border-box;
  /* --canvas-text-inset, see #titleEdit above. */
  padding: 0 var(--canvas-text-inset);
  /* fills #editorUnit's own width, see #titleEdit above. */
  width: 100%;
  margin: 0 0 10px 0;
  max-height: 2em;
  overflow: hidden;
  transition: opacity 0.25s ease, max-height 0.25s ease, margin-bottom 0.25s ease;
}
#authorRow.scroll-hidden {
  opacity: 0;
  max-height: 0;
  margin-bottom: 0;
}
#authorBy {
  color: #d4c4a8;
  font-family: Consolas, monospace;
  font-size: clamp(11px, 0.75vw, 16px);
  font-weight: normal;
  font-style: italic;
  flex-shrink: 0;
  user-select: none;
}
#authorEdit {
  background: transparent;
  border: none;
  outline: none;
  color: #d4c4a8;
  font-family: Consolas, monospace;
  font-size: clamp(11px, 0.75vw, 16px);
  font-weight: normal;
  text-transform: uppercase;
  padding: 0;
  flex: 1;
}
#authorEdit:focus-visible { box-shadow: 0 0 0 2px rgba(200, 165, 80, 0.5); border-radius: 2px; }
#authorEdit::placeholder { color: #4a4a3a; }
#authorEdit::selection { background: rgba(180,30,30,0.35); }

/* editor */
/* scrolling lives on this non-editable wrapper, not on #editor itself: Chromium shows the
   text-beam cursor, and misbehaves on drag, over a custom ::-webkit-scrollbar belonging to
   a contenteditable element.
   sized to the canvas's own width and left, not #editorCol's full width, so the scroll
   range lines up with the canvas.
   the native scrollbar is hidden entirely below; editorscrollbar.js substitutes a segment
   of #editorWallRight itself, so there is one line on the canvas's right edge rather than
   a scrollbar sitting redundantly beside that boundary line. */
#editorScroll {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  padding: 4px 0 3.5em 0;
  box-sizing: border-box;
  /* fills #editorUnit's own width, see #titleEdit above. #editorUnit carries the transform
       now, a GPU-composited shift without reflowing this element's potentially large
       contenteditable subtree. */
  width: 100%;
  margin: 0;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* legacy Edge */
}
#editorScroll::-webkit-scrollbar { width: 0; height: 0; display: none; }
#editor {
  outline: none;
  color: #c8b898;
  font-family: Consolas, monospace;
  font-size: 16px;
  line-height: 1.75;
  width: 100%;
  margin: 0;
  box-sizing: border-box;
  /* --canvas-text-inset, see #titleEdit above. */
  padding: 0 var(--canvas-text-inset);
}
#editor::selection { background: rgba(180,30,30,0.30); }

/* boundary-line markers bracketing the canvas's left and right edges, invisible
   themselves apart from the outward glow their own rules carry.
   #editor's own padding keeps the text from touching either line. these lines, not
   #editor's box edges, are the canvas's true left and right boundary, and main.js aligns
   the widget menus against them rather than #editor directly.
   direct children of #editorCol, running the full height of #editorUnit so the glow reads
   as one continuous edge rather than cutting off above the author row.
   both `top` and `bottom` are set inline by applyEditorUnitLayout(). leaving CSS's plain
   `bottom: 0` here would let these lines dangle past the panel's bottom edge into the
   slate. the values below are the pre-JS fallback. */
/* the two wall lines are deliberately declared with IDENTICAL width, pointer-events,
   z-index, and transition values. only `left` differs, and their box-shadow's horizontal
   offset sign is a second deliberate mirror rather than a divergence: each casts its glow
   outward, away from the canvas, so the signs must be opposite for the two to be the same
   treatment.
   keep any future edit mirrored across both rules. tuning one side alone breaks the
   identical-border guarantee this pair exists for, silently. */
#editorWallLeft {
  position: absolute;
  left: 20%;
  top: 0;
  bottom: 0;
  pointer-events: none;
  z-index: 3;
  /* still no border or background: this element is a pure layout marker apart from the
       outward glow, which needs no fill since box-shadow paints around the box regardless.
       that glow is added LIGHT, using the same warm tone as the panel's edge rims, not a
       dark drop-shadow: a dark shadow is below the perceptual floor against this background
       and would be invisible. only added light shows up here.
       the only visible RED on this edge is #editorScrollThumb, a brighter segment at this
       line's own coordinates.
       editorlayout.js positions this every layout pass and editorscrollbar.js reads its
       twin's position off it, so removing the element would break both.
       offset, blur, and alpha are tuned together, not independent: #editorCol has only a
       12px gutter and clips at it, so offset plus blur must stay under that or the glow's
       soft tail is cut short instead of fading out on its own. the alpha compensates for the
       blur diluting it across its own spread. */
  box-shadow: -4px 0 7px rgba(var(--canvas-rim-hue), 0.4);
  /* transform: the side-panel-open shift, applied in lockstep with #editorUnit's own since
       this is a deliberate sibling, not a child. left, top, and bottom are only ever set once
       per layout pass and never transitioned, so there is no initial-mount sweep risk. */
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
#editorWallRight {
  position: absolute;
  left: 80%;
  top: 0;
  bottom: 0;
  pointer-events: none;
  z-index: 3;
  /* see #editorWallLeft above: every declaration here is byte-identical to it except the
       box-shadow's offset sign, mirrored so the glow casts right instead of left. */
  box-shadow: 4px 0 7px rgba(var(--canvas-rim-hue), 0.4);
  /* transform: the side-panel-open shift, see #editorWallLeft above. */
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

/* the editor's scrollbar: not a separate track beside #editorWallRight, this IS a segment
   of that same line, just brighter, sized and positioned by editorscrollbar.js to match
   the scroll position. same left and same resting width, so at a glance it reads as one
   continuous line with a lit segment on it rather than two lines side by side.
   js-hidden whenever the document doesn't overflow: nothing to scroll, nothing to show.
   pointer-events:auto, while the wall line stays none, since this is the one part of the
   line meant to be grabbed. */
#editorScrollThumb {
  position: absolute;
  width: 2px;
  pointer-events: auto;
  /* above both the panel's z-index and the wall lines': this segment must stay visible and
       draggable at the canvas edge, on top of the plain line it is a brighter segment of. */
  z-index: 4;
  cursor: grab;
  border-radius: 1px;
  background: rgba(255, 90, 60, 1);
  box-shadow: 0 0 6px rgba(255, 60, 30, 0.85), 0 0 14px rgba(200, 20, 10, 0.5);
  /* transform: the side-panel-open shift, copied from #editorWallRight on every
       refreshEditorScrollbar() call, with the same duration and easing so the two glide
       together. */
  transition: width 0.12s ease, margin-left 0.12s ease, box-shadow 0.12s ease,
    background-color 0.12s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
#editorScrollThumb:hover,
#editorScrollThumb.dragging {
  width: 6px;
  margin-left: -2px; /* widen from the track's own centerline, not just rightward */
  background: rgba(255, 150, 110, 1);
  box-shadow: 0 0 10px rgba(255, 110, 70, 1), 0 0 26px rgba(255, 40, 10, 0.75);
}
#editorScrollThumb.dragging { cursor: grabbing; }
#editorScrollThumb::before {
  content: '';
  position: absolute;
  width: 14px;
  height: 100%;
  left: -6px;
  top: 0;
  pointer-events: auto;
}

/* ghost placeholder for a blank document. NOT #editor:empty: ensureTrailingSpace() always
   pads the doc with empty paragraphs as editable breathing room, so #editor is never
   actually :empty even on a brand-new document.
   .is-empty is a JS-maintained class instead, gone the instant real content exists and
   back the instant the document is genuinely empty again.
   anchored on the first <p>, not #editor itself: pseudo-elements on the container would
   land either side of ALL its children. the first paragraph's own <br>, needed to give a
   truly empty <p> its line height, would then sit between the two and force a break,
   splitting the placeholder from the dots. it is hidden here since the pseudo text
   supplies that line height itself while showing. */
#editor.is-empty p:first-child > br {
  display: none;
}
#editor.is-empty p:first-child::before {
  content: 'begin writing';
  font-style: italic;
  color: rgba(200, 184, 152, 0.32);
  pointer-events: none;
}
/* the animated ellipsis is a separate ::after, since only two pseudo-elements exist per
   element, revealed via a widening clip box rather than animating `content` itself:
   animatable `content` needs Houdini, not supported widely enough for a zero-dependency
   app to lean on. `ch` is exact here since #editor is monospace.
   jump-start, not the default jump-end: with jump-end the final step only exists for the
   instant the loop wraps, so it never visually renders and the reveal caps one dot short.
   jump-start front-loads each jump so the LAST interval is the one that actually holds.
   the trade-off is that the empty state is never rendered instead, which is the right side
   to lose. */
#editor.is-empty p:first-child::after {
  content: '.....';
  display: inline-block;
  width: 0;
  overflow: hidden;
  white-space: nowrap;
  vertical-align: bottom;
  font-style: italic;
  color: rgba(200, 184, 152, 0.32);
  animation: ghostEllipsis 2.5s steps(5, jump-start) infinite;
  pointer-events: none;
}
@keyframes ghostEllipsis {
  to { width: 5ch; }
}

#editor p {
  margin: 0 0 22px 0;
  font-family: Consolas, monospace;
  font-size: 16px;
  border-bottom: 1px solid transparent;
}
/* single ghost ruled line marking the first paragraph, shown only while the document is
   still in its brand-new state, the same gate as the ghost placeholder above.
   updateGhostLine() strips .line-current from every other paragraph first, then adds it
   back only to the first child. it never lights on any other paragraph, and disappears for
   good once real content exists anywhere. */
#editor p.line-current {
  border-bottom-color: rgba(200, 60, 50, 0.35);
}

#editor h1 {
  font-family: Consolas, monospace;
  font-size: 19px;
  font-weight: bold;
  font-style: normal;
  text-align: center;
  color: #d4c4a8;
  margin: 16px 0 6px 0;
  line-height: 1.75;
}

#editor h2 {
  font-family: Consolas, monospace;
  font-size: 16px;
  font-weight: bold;
  font-style: italic;
  text-align: left;
  color: #d4c4a8;
  margin: 12px 0 4px 0;
  line-height: 1.75;
}

/* lists / bulleted lists get exactly one marker style everywhere, a small solid red square
   at every nesting depth, not the browser's default alternation and not a user choice.
   numbered lists keep native decimal numbering, needing nothing beyond matching the
   editor's own margin and font rhythm. */
#editor ul, #editor ol {
  margin: 0 0 22px 0;
  padding-left: 28px;
  font-family: Consolas, monospace;
  font-size: 16px;
}
#editor li {
  margin: 0 0 6px 0;
}
#editor li > ul, #editor li > ol {
  margin: 6px 0 0 0;
}
#editor ul > li {
  list-style: none;
  position: relative;
}
#editor ul > li::before {
  content: '';
  position: absolute;
  left: -20px;
  top: 0.5em;
  width: 8px;
  height: 8px;
  background: rgba(200, 60, 50, 0.9);
  border: 1px solid rgba(255, 90, 90, 0.5);
}

/* tables / no header-row distinction: every cell reads the same. cell content is always
   paragraph-wrapped by insertTable(), and the cell's own padding supplies breathing room,
   so a paragraph's usual bottom margin is zeroed to avoid stacking on top of it. */
#editor table {
  border-collapse: collapse;
  margin: 0 0 22px 0;
  font-family: Consolas, monospace;
  font-size: 16px;
}
#editor td, #editor th {
  border: 1px solid rgba(180, 30, 30, 0.3);
  padding: 6px 10px;
  vertical-align: top;
}
#editor td p, #editor th p {
  margin: 0;
}

/* skull counter modal */
/* skull-pile readout, an on-demand modal using the same fade lifecycle every
   createFadeModal()-based panel uses. */
#skullCounterBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.48);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#skullCounterBackdrop.visible { display: block; }
#skullCounterBackdrop.shown { opacity: 1; }
#skullCounterBackdrop.fading { opacity: 0; transition: opacity 2s ease; }

/* right-docked side panel shared shape. the editor canvas glides left to make room and
   the panel slides in from the right edge to fill it.
   these four were byte-identical blocks, combined into one shared rule rather than a new
   class, so ID-selector specificity stays exactly as it was.
   #skullEscapePanel and #turnModePanel share the same box-shadow formula with different
   accent colors but differ enough elsewhere to stay separate. the two-column layout below
   is sized to fit this width, not the other way around. */
#codexPanel,
#outlinePanel,
#skullCounterPanel,
#skillCheckPanel {
  position: fixed;
  top: 0;
  left: auto;
  right: 0;
  width: 35vw;
  flex-shrink: 0;
  height: 100vh;
  max-height: 100vh;
  background: rgba(8, 5, 18, 0.94);
  border-width: 0 0 0 1px;
  border-style: solid;
  border-color: rgba(180, 140, 60, 0.35);
  border-radius: 0;
  box-shadow: 0 0 22px rgba(180, 140, 60, 0.16), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3000;
  font-family: Consolas, monospace;
  display: none;
  flex-direction: column;
  opacity: 0;
  transform: translateX(100%);
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease-out;
}
#codexPanel.visible, #outlinePanel.visible, #skullCounterPanel.visible, #skillCheckPanel.visible { display: flex; }
#codexPanel.shown, #outlinePanel.shown, #skullCounterPanel.shown, #skillCheckPanel.shown { opacity: 1; transform: translateX(0); }
#codexPanel.fading, #outlinePanel.fading, #skullCounterPanel.fading, #skillCheckPanel.fading { opacity: 0; transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4,0,0.2,1), opacity 0.3s ease; pointer-events: none; }

#skullCounterHeader {
  flex-shrink: 0;
  padding: 10px 14px;
  color: rgba(200, 165, 80, 0.75);
  font-size: 15px;
  letter-spacing: 1.5px;
  display: flex;
  align-items: center;
  gap: 8px;
  user-select: none;
}
#skullCounterCloseBtn {
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
  margin-left: auto;
}
#skullCounterCloseBtn:hover { color: rgba(220, 190, 110, 0.9); }

/* two-column body: the visual skull-pile canvas on the left, the numeric metrics
   dashboard on the right, split by a divider border on the visual column. */
#skullCounterBody {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: row;
}

#skullCounterVisualCol {
  flex: 1 1 50%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  border-right: 1px solid rgba(180, 140, 60, 0.35);
}

#skullCounterCanvas {
  display: block;
  flex: 1;
  min-height: 0;
  width: 100%;
}

#skullCounterMetricsCol {
  flex: 1 1 50%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-end;
  gap: 22px;
  padding: 16px 12px;
  overflow: hidden;
}

.skullCounterMetric {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
}

/* underlined, matching .spells-flyout-header's "this is a title, not a clickable item"
   treatment, but red, the same hex the toolbar wordmark is drawn in, rather than gold. */
.skullCounterMetricLabel {
  font-family: Consolas, monospace;
  font-size: 16.5px;
  font-weight: 700;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: #B02010;
  text-shadow: 0 0 6px rgba(176, 32, 16, 0.5);
  border-bottom: 1px solid rgba(176, 32, 16, 0.35);
  padding-bottom: 2px;
}

.skullCounterMetricValue {
  margin-top: 4px;
  font-family: Consolas, monospace;
  font-size: 90px;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: 1px;
  font-variant-numeric: tabular-nums;
  color: #B02010;
  text-shadow: 2px 2px 0 rgba(176, 32, 16, 0.45);
}

.skullCounterMetricValue.skullCounterMetricValueBank {
  font-size: 72px;
  color: #B02010;
  text-shadow: 2px 2px 0 rgba(176, 32, 16, 0.3);
}

/* plain, matching .spells-flyout-btn's un-highlighted item shape, but red per the
   metric-text color pass above. */
.skullCounterMetricSub {
  margin-top: 4px;
  font-family: Consolas, monospace;
  font-size: 14.25px;
  letter-spacing: 0.3px;
  color: #B02010;
}

.skull-counter-active #toolbar,
.skull-counter-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

/* skull escape modal */
/* the arcade minigame, its own modal reached only via the revealed cheat code, with no
   toolbar trigger.
   the panel is the same fixed size every right-docked panel uses, but the canvas's own
   width and height are NOT fixed: skull-escape-modal.js sizes it to this panel's actual
   rendered box right before the game starts, and the level's bounds derive from that, so
   the game always fills whatever the panel comes out to on the current viewport. */
#skullEscapeBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.48);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#skullEscapeBackdrop.visible { display: block; }
#skullEscapeBackdrop.shown { opacity: 1; }
#skullEscapeBackdrop.fading { opacity: 0; transition: opacity 2s ease; }

/* right-docked side panel, see the shared rule above for the general mechanism. kept as
   its own block rather than folded in: same box-shadow formula with a red accent instead
   of gold, but differing background, border width, and flex alignment. */
#skullEscapePanel {
  position: fixed;
  top: 0;
  left: auto;
  right: 0;
  width: 35vw;
  flex-shrink: 0;
  height: 100vh;
  background: #000;
  border-width: 0 0 0 3px;
  border-style: solid;
  border-color: #B02010;
  border-radius: 0;
  box-shadow: 0 0 22px rgba(176, 32, 16, 0.3), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3000;
  font-family: Consolas, monospace;
  display: none;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transform: translateX(100%);
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease-out;
  overflow: hidden;
}
#skullEscapePanel.visible { display: flex; }
#skullEscapePanel.shown { opacity: 1; transform: translateX(0); }
#skullEscapePanel.fading { opacity: 0; transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4,0,0.2,1), opacity 0.3s ease; pointer-events: none; }

.skull-escape-active #toolbar,
.skull-escape-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

#skullEscapeCloseBtn {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 11;
  background: rgba(0,0,0,0.55);
  color: #e0d0b0;
  border: 1px solid #e0d0b0;
  width: 20px;
  height: 20px;
  line-height: 18px;
  padding: 0;
  cursor: pointer;
  font-family: Consolas, monospace;
  font-size: 12px;
}
#skullEscapeCloseBtn:hover { opacity: 0.8; }

#skullEscapeStartScreen {
  text-align: center;
}

/* canvas-drawn with PIXEL_FONT, see drawSkullEscapeTitle. */
#skullEscapeTitle {
  display: block;
  margin: 0 auto 16px auto;
  image-rendering: pixelated;
}

#skullEscapeSubtitle {
  display: none;
}

/* deliberately smaller than the pixel-font title above it, a secondary call-to-action not
   competing for the same visual weight as the game's own name.
   blinks hard on and off, matching the arcade-cabinet convention, to draw the eye once the
   screen appears. hover and active override it with an immediate, non-animated opacity so
   interacting doesn't fight the blink cycle. */
@keyframes hearth-start-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}
#skullEscapeStartBtn {
  background: transparent;
  border: none;
  color: #B02010;
  font-family: Consolas, monospace;
  font-size: 14px;
  font-weight: bold;
  padding: 0;
  margin: 0;
  cursor: pointer;
  letter-spacing: 1px;
  text-transform: uppercase;
  animation: hearth-start-blink 1s steps(1, end) infinite;
}
#skullEscapeStartBtn:hover {
  animation: none;
  opacity: 0.8;
}
#skullEscapeStartBtn:active {
  animation: none;
  transform: scale(0.98);
}

/* disabled only while the fetch of level and song content is in flight, a real failure
   mode lazy-loading introduced that a static import could never hit at runtime. stops the
   blink so "disabled" doesn't also look like "still asking for input". */
#skullEscapeStartBtn:disabled {
  animation: none;
  opacity: 0.4;
  cursor: default;
}

/* shown only on a load failure: a plain-text, low-emphasis line under the button rather
   than a modal on a modal. */
#skullEscapeLoadStatus {
  color: #8a5a4a;
  font-size: 11px;
  text-align: center;
  margin-top: 8px;
  max-width: 90%;
  margin-left: auto;
  margin-right: auto;
}

#skullEscapeControls {
  display: none;
}

#skullEscapeCanvas {
  display: block;
  outline: none; /* focused programmatically, not a real tab stop */
  image-rendering: pixelated;
  background: #1a1a1a;
}

/* statusbar */
#statusbar {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  background: var(--workspace-slate);
  padding: 3px 14px;
  gap: 20px;
  flex-shrink: 0;
  user-select: none;
}
#statusbar span {
  color: #6a5a4a;
  font-family: Consolas, monospace;
  font-size: 11px;
}
#statusbar .legal-link {
  color: #6a5a4a;
  font-family: Consolas, monospace;
  font-size: 11px;
  text-decoration: none;
}
#statusbar .legal-link:hover { color: #d4b86a; }
#wordCountSkull { display: none; vertical-align: middle; margin-right: 3px; }
#typingSprite {
  /* turned off for now, and it never gets a ctx or draws anything, so it is already blank.
       it still has to stay in flex layout as the spacer between the legal group and the
       skull group, or #statusbar's flex-end packs both together against the right edge.
       height:0, not display:none, is what lets the bar shrink to its text height without
       losing that spacer. restore the height to re-enable. */
  flex: 1 1 auto;
  min-width: 60px;
  height: 0;
  image-rendering: pixelated;
}

/* drive document list modal */
#driveOverlay {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.7);
  z-index: 2000;
  align-items: center;
  justify-content: center;
}
#driveOverlay.visible { display: flex; }
/* background, border, and box-shadow match the right-docked side-panel family. kept as
   its own block rather than joining that selector: this modal is centered with its own
   backdrop rather than a right-docked sliding panel, so the layout half of that rule
   doesn't apply here, only the visual language. */
#driveModal {
  background: rgba(8, 5, 18, 0.94);
  border: 1px solid rgba(180, 140, 60, 0.35);
  border-radius: 6px;
  width: 420px;
  /* dvh, not vh: vh counts space still hidden behind mobile browser chrome, which let the
       footer link render below the actually-visible viewport. falls back to vh where dvh
       isn't supported. */
  max-height: 70vh;
  max-height: 70dvh;
  display: flex;
  flex-direction: column;
  font-family: Consolas, monospace;
  box-shadow: 0 0 22px rgba(180, 140, 60, 0.16), 0 4px 28px rgba(0,0,0,0.55);
}
#driveModalHeader {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px 10px 18px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
}
#driveModalHeader h3 {
  color: #d4c4a8;
  font-size: 16px;
  font-weight: bold;
  margin: 0;
}
#driveModalClose {
  background: transparent;
  border: none;
  color: #8a8a7a;
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}
#driveModalClose:hover { color: #e0d0c0; }
#driveNewBtn {
  margin: 12px 18px 0 18px;
  background: rgba(180,30,30,0.55);
  color: #f0e0d0;
  border: none;
  border-radius: 4px;
  padding: 8px 14px;
  font-family: Consolas, monospace;
  font-size: 14px;
  font-weight: bold;
  cursor: pointer;
}
#driveNewBtn:hover { background: rgba(180,30,30,0.75); color: #fffbe8; }
#driveFileList {
  flex: 1;
  overflow-y: auto;
  padding: 8px 0;
  min-height: 0; /* allow flex: 1 to shrink below content height on tall modals */
}
/* a row, not a single button: it hosts two independent actions, so the row itself is just
   a flex container and each action is its own real <button>. */
.drive-file-item {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid rgba(180,30,30,0.08);
}
.drive-file-open {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  color: #b0a890;
  padding: 10px 18px;
  text-align: left;
  font-family: Consolas, monospace;
  font-size: 16px;
  cursor: pointer;
}
.drive-file-open:hover { background: rgba(180,30,30,0.20); }
.drive-file-date {
  display: block;
  color: #6a5a4a;
  font-size: 12px;
  margin-top: 2px;
}
/* same visual recipe as .doctab-new for a consistent "this adds a tab" affordance
   wherever it appears. */
.drive-file-newtab {
  align-self: center;
  flex-shrink: 0;
  margin-right: 12px;
  width: 22px;
  height: 22px;
  border-radius: 4px;
  border: none;
  background: transparent;
  color: #8a8a7a;
  font-family: Consolas, monospace;
  font-size: 14px;
  cursor: pointer;
}
.drive-file-newtab:hover { background: rgba(180,30,30,0.35); color: #e0d0c0; }
#driveFileList .drive-status {
  color: #6a5a4a;
  padding: 20px 18px;
  font-size: 13px;
  text-align: center;
}
#driveFileList .drive-status.error { color: #b03018; }
/* grouped, two-column action rows. the eyebrow style is copied exactly from
   .build-out-section h4 below, so this reads as the same pattern the app already uses for
   "this modal's items fall into real categories" rather than a new one.
   each group is a border-top-divided block containing a 2-column grid, half the row count
   of a flat single-column list, giving the file list more of the modal's fixed budget back
   before it needs to scroll. */
.tomes-group {
  padding: 2px 18px 6px;
  border-top: 1px solid rgba(180,30,30,0.20);
}
.tomes-group h4 {
  color: rgba(160, 130, 60, 0.55);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 6px 0 4px 0;
}
.tomes-group-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
}
.tomes-action-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: none;
  width: 100%;
  color: #6a5a4a;
  padding: 7px 6px;
  font-family: Consolas, monospace;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
  border-radius: 3px;
}
.tomes-action-row:hover, .tomes-action-row:focus-visible { color: #d4c4a8; }
.tomes-action-row .tomes-icon { font-size: 14px; width: 16px; text-align: center; flex-shrink: 0; }
/* Reading View is the most distinctive destination in this menu; everything else here is
   a plain utility action. one deliberate touch of extra weight, nowhere else in this list. */
/* compound selector, not bare: .tomes-action-row's own color rule is a single-class
   selector too, and a same-specificity tie between two bare single-class selectors on one
   element is exactly the fragile shape test/unit/css-specificity-ties.test.js exists to
   catch. compounding guarantees this always wins regardless of source order. */
.tomes-action-row.tomes-action-featured { color: #d4c4a8; }
.tomes-action-featured .tomes-icon { filter: drop-shadow(0 0 3px rgba(176,32,16,0.6)); }

/* Alchemize's format choice, revealed by clicking Alchemize itself rather than exporting
   immediately. sits inside the Convert group below its row, with no second modal, matching
   the one-modal-at-a-time convention. no horizontal padding of its own: it inherits the
   group's. */
#alchemizeChoice {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px dashed rgba(180,30,30,0.35);
}
#alchemizeChoice button {
  flex: 1;
  display: block;
  background: rgba(180,30,30,0.08);
  border: 1px dashed rgba(180,30,30,0.35);
  border-radius: 3px;
  color: #6a5a4a;
  padding: 8px 10px;
  font-family: Consolas, monospace;
  font-size: 11px;
  cursor: pointer;
}
#alchemizeChoice button:hover:not(:disabled) { color: #d4c4a8; border-color: rgba(180,30,30,0.6); }
#alchemizeChoice button:disabled { opacity: 0.35; cursor: default; }
/* footer link section, always visible at the bottom of the modal, not scrollable away */
#driveModal > div:last-child {
  flex-shrink: 0;
  border-top: 1px solid rgba(180, 140, 60, 0.2);
  margin-top: 12px;
  padding: 12px 18px;
  text-align: center;
}
#driveModal > div:last-child a,
.tomes-footer-link {
  color: rgba(200, 165, 80, 0.6);
  font-size: 12px;
  text-decoration: none;
}
/* button reset: this opens the How Worg Saves Your Work modal in place of the plain <a>
   the footer link used to be, now that its target is in-app rather than a page nav. */
.tomes-footer-link {
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  cursor: pointer;
}
.tomes-footer-link:hover { color: #e0d0c0; text-decoration: underline; }

/* how Worg saves your work / the same plain centered-overlay recipe as #driveModal just
   above, instant show and hide with no fade lifecycle, reusing that visual language under
   its own ids since it is opened FROM Tomes and replaces it in the same screen space. */
#dataStorageOverlay {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.7);
  z-index: 2000;
  align-items: center;
  justify-content: center;
}
#dataStorageOverlay.visible { display: flex; }
#dataStorageModal {
  background: rgba(8, 5, 18, 0.94);
  border: 1px solid rgba(180, 140, 60, 0.35);
  border-radius: 6px;
  width: 420px;
  max-height: 70vh;
  max-height: 70dvh;
  display: flex;
  flex-direction: column;
  font-family: Consolas, monospace;
  box-shadow: 0 0 22px rgba(180, 140, 60, 0.16), 0 4px 28px rgba(0,0,0,0.55);
}
#dataStorageModalHeader {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px 10px 18px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
  flex-shrink: 0;
}
#dataStorageModalHeader h3 {
  color: #d4c4a8;
  font-size: 16px;
  font-weight: bold;
  margin: 0;
}
#dataStorageModalClose {
  background: transparent;
  border: none;
  color: #8a8a7a;
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}
#dataStorageModalClose:hover { color: #e0d0c0; }
#dataStorageBody {
  overflow-y: auto;
  min-height: 0;
  padding: 14px 18px 18px 18px;
  color: rgba(220, 190, 110, 0.82);
}
.data-storage-warning {
  border: 1px solid rgba(180, 30, 30, 0.35);
  background: rgba(180, 30, 30, 0.08);
  border-radius: 4px;
  padding: 10px 14px;
  font-size: 13px;
  color: rgba(220, 160, 130, 0.85);
  margin-bottom: 16px;
}
#dataStorageBody h4 {
  color: #d4b86a;
  font-size: 14px;
  letter-spacing: 0.5px;
  margin: 18px 0 8px 0;
}
#dataStorageBody h4:first-of-type { margin-top: 0; }
#dataStorageBody p {
  font-size: 13.5px;
  line-height: 1.6;
  margin-bottom: 8px;
}
@media (max-width: 480px) {
  #dataStorageModal { width: 90vw; max-height: 70vh; max-height: 70dvh; }
}

/* build out panel / the same frame-modal language as Skeleton and Skill Check, with the
   same fade lifecycle via createFadeModal. */
#codexBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.48);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#codexBackdrop.visible { display: block; }
#codexBackdrop.shown { opacity: 1; }
#codexBackdrop.fading { opacity: 0; transition: opacity 2s ease; }

/* #codexPanel's right-docked shape lives in the shared four-panel rule near
   #skullCounterPanel above; see that comment for why those four are combined. */

.codex-active #toolbar,
.codex-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

#codexHeader {
  flex-shrink: 0;
  padding: 10px 14px;
  color: rgba(200, 165, 80, 0.75);
  font-size: 15px;
  letter-spacing: 1.5px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
/* the same pixel-art icon the toolbar's Codex head uses. .menu-icon's own default is
   sized for a bare toolbar button, shrunk here to sit beside the header's text rather than
   dominating it. */
.codex-header-icon {
  height: 20px;
}
#codexCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}
#codexCloseBtn:hover { color: rgba(220, 190, 110, 0.9); }

/* tab bar, codex.js's small generic tab switcher. .glow reuses buildOutGlow directly,
   having real text content for text-shadow to render against, unlike .spells-head. */
.codex-tabs {
  flex-shrink: 0;
  display: flex;
  gap: 4px;
  padding: 0 14px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
}
.codex-tab {
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-family: Consolas, monospace;
  font-size: 14px;
  letter-spacing: 0.5px;
  padding: 8px 10px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
}
.codex-tab:hover { color: rgba(220, 190, 110, 0.85); }
.codex-tab.active {
  color: rgba(220, 190, 110, 0.95);
  border-bottom-color: rgba(180, 140, 60, 0.6);
}
.codex-tab.glow { animation: buildOutGlow 1.6s ease-in-out infinite; }
.codex-tab-panel {
  flex: 1;
  overflow-y: auto;
  padding: 8px 0 16px 0;
  min-height: 0;
}
/* a revealed cheat-code phrase: the same row and description shell as a Build Out item,
   without the move arrows, checkbox, or price a curatable item has, since these are a
   read-only reference list rather than something you opt in or out of. */
.codex-cheat-phrase {
  font-style: italic;
  color: rgba(220, 190, 110, 0.9);
}
.build-out-section {
  padding: 4px 18px 12px 18px;
}
/* flex row so an optional price column label can sit on the SAME line as the group's own
   name, right-aligned, instead of the two reading as stacked rows. */
.build-out-section h4 {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  color: rgba(160, 130, 60, 0.55);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 8px 0 6px 0;
}
/* right edge padded further than a bare row's section inset would give it: the price span
   sits inset by the section's own amount PLUS .build-out-row's right padding, which this
   heading doesn't have, so matching only the section's inset leaves it off from the column
   it labels. */
.build-out-price-header {
  padding-right: 4px;
}
.build-out-row {
  padding: 8px 4px;
  color: rgba(220, 190, 110, 0.78);
  font-size: 15px;
}
.build-out-row:hover { color: rgba(220, 190, 110, 0.95); }
.build-out-row input[type="checkbox"] { cursor: pointer; }
.build-out-row-main {
  display: flex;
  align-items: center;
  gap: 10px;
}
/* one-sentence "what does this actually do", reusing the exact copy already in the
   button's own title tooltip. indented to line up under the checkbox label above it,
   not the row's left edge. */
.build-out-description {
  margin: 3px 0 0 46px;
  color: rgba(200, 165, 80, 0.5);
  font-size: 13px;
  font-style: italic;
}
/* Charms' gutter-visibility sub-option, rendered directly under Charms' row. the same
   inset as .build-out-description above, so it lines up under Charms' own label rather
   than reading as a new top-level catalog row. */
.build-out-subrow {
  margin-left: 46px;
  padding: 4px 4px 8px 0;
}
.build-out-subrow .build-out-label {
  font-size: 13px;
  color: rgba(200, 165, 80, 0.75);
}
.build-out-move-wrap {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.build-out-move {
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-size: 11px;
  line-height: 1;
  padding: 2px 4px;
  cursor: pointer;
}
.build-out-move:hover:not(:disabled) { color: rgba(220, 190, 110, 0.95); }
.build-out-move:disabled { color: rgba(160, 130, 60, 0.22); cursor: default; }
.build-out-checkbox-label {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  cursor: pointer;
}
.build-out-label { flex: 1; }
/* per-row "just unlocked" glow, the same seen/unseen check and keyframe as the rest of
   the Codex reveal chain. real text content, so text-shadow renders against it directly. */
.build-out-label.glow { animation: buildOutGlow 1.6s ease-in-out infinite; }
.build-out-price {
  color: rgba(200, 165, 80, 0.6);
  font-size: 13px;
  white-space: nowrap;
}
.build-out-empty {
  color: rgba(160, 130, 60, 0.4);
  font-size: 14px;
  font-style: italic;
  padding: 6px 4px;
}

/* "new item waiting" glow keyframe, shared by every glow indicator the Codex hub uses:
   the icon itself, its two tabs, and the mobile hamburger. a bare keyframe now that
   several different selectors reuse it. */
@keyframes buildOutGlow {
  0%, 100% { text-shadow: 0 0 2px rgba(230,190,90,0.3); opacity: 0.75; }
  50% { text-shadow: 0 0 10px rgba(230,190,90,0.95), 0 0 18px rgba(230,190,90,0.6); opacity: 1; }
}

/* devour modal / the same overlay and panel recipe as the Drive modal above */
#devourOverlay {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.7);
  z-index: 2000;
  align-items: center;
  justify-content: center;
}
#devourOverlay.visible { display: flex; }
#devourModal {
  background: #151515;
  border-radius: 6px;
  width: 420px;
  max-height: 70vh;
  max-height: 70dvh;
  display: flex;
  flex-direction: column;
  font-family: Consolas, monospace;
  box-shadow:
    0 16px 50px rgba(0,0,0,0.70),
    inset 0 0 30px rgba(0,0,0,0.55);
}
#devourModalHeader {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px 10px 18px;
  border-bottom: 1px solid rgba(180,30,30,0.20);
}
#devourModalHeader h3 {
  color: #d4c4a8;
  font-size: 16px;
  font-weight: bold;
  margin: 0;
}
#devourModalClose {
  background: transparent;
  border: none;
  color: #8a8a7a;
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}
#devourModalClose:hover { color: #e0d0c0; }
#devourSearch {
  margin: 12px 18px 0 18px;
  background: #0e0e0e;
  border: 1px solid rgba(180,30,30,0.25);
  border-radius: 4px;
  color: #d4c4a8;
  padding: 8px 10px;
  font-family: Consolas, monospace;
  font-size: 13px;
}
#devourSearch::placeholder { color: #5a5045; }
#devourSearch:focus { outline: none; border-color: rgba(180,30,30,0.55); }
#devourFileList {
  flex: 1;
  overflow-y: auto;
  padding: 8px 0;
  min-height: 0;
}
#devourLocalOpen {
  display: block;
  flex-shrink: 0;
  background: transparent;
  border: none;
  border-top: 1px solid rgba(180,30,30,0.20);
  color: #6a5a4a;
  padding: 12px 18px;
  font-family: Consolas, monospace;
  font-size: 12px;
  cursor: pointer;
  text-align: center;
  width: 100%;
}
#devourLocalOpen:hover { color: #d4c4a8; }

.worg-quest-row {
  position: relative;
  border-bottom: 2px solid rgba(180,30,30,0.25);
}
.worg-quest-item {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  color: #c8a84a;
  padding: 10px 34px 10px 18px;
  text-align: left;
  font-family: Consolas, monospace;
  font-size: 14px;
  cursor: pointer;
}
.worg-quest-item:hover { background: rgba(180,30,30,0.20); color: #e0c870; }
.worg-quest-sub {
  display: block;
  font-size: 11px;
  color: #7a6a3a;
  margin-top: 2px;
}
.worg-quest-remove {
  position: absolute;
  top: 6px;
  right: 6px;
  background: transparent;
  border: none;
  color: #7a6a3a;
  font-size: 16px;
  line-height: 1;
  padding: 4px 6px;
  cursor: pointer;
}
.worg-quest-remove:hover { color: #e0c870; }
/* unsaved-changes dialog */
#unsavedOverlay {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.7);
  z-index: 2100;
  align-items: center;
  justify-content: center;
}
#unsavedOverlay.visible { display: flex; }
#unsavedModal {
  background: #151515;
  border-radius: 6px;
  width: 360px;
  max-width: 90vw;
  padding: 20px 22px 22px 22px;
  font-family: Consolas, monospace;
  box-shadow:
    0 16px 50px rgba(0,0,0,0.70),
    inset 0 0 30px rgba(0,0,0,0.55);
}
#unsavedTitle {
  color: #d4c4a8;
  font-size: 15px;
  font-weight: bold;
  margin-bottom: 8px;
}
#unsavedMsg {
  color: #a8a090;
  font-size: 13px;
  line-height: 1.5;
  margin-bottom: 18px;
}
#unsavedBtns {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
#unsavedBtns button {
  border: none;
  border-radius: 4px;
  padding: 9px 14px;
  font-family: Consolas, monospace;
  font-size: 13px;
  cursor: pointer;
}
.unsaved-btn-primary { background: rgba(180,30,30,0.55); color: #f0e0d0; font-weight: bold; }
.unsaved-btn-primary:hover { background: rgba(180,30,30,0.75); color: #fffbe8; }
.unsaved-btn { background: #2a2a2a; color: #c0b8a8; }
.unsaved-btn:hover { background: #333333; color: #e0d0c0; }

/* sync concierge / a non-blocking stack of notice and decision cards anchored above the
   statusbar. the container ignores pointer events so it never blocks the editor beneath
   it; only the cards themselves are interactive. column-reverse keeps the newest card on
   top of the stack. */
#syncConcierge {
  position: fixed;
  right: 14px;
  bottom: 46px;
  z-index: 2300;
  display: flex;
  flex-direction: column-reverse;
  gap: 10px;
  width: 340px;
  max-width: calc(100vw - 28px);
  pointer-events: none;
}
.concierge-card {
  pointer-events: auto;
  background: #151515;
  border-radius: 6px;
  border-left: 3px solid rgba(180,30,30,0.55);
  padding: 12px 14px 13px 14px;
  font-family: Consolas, monospace;
  box-shadow:
    0 12px 40px rgba(0,0,0,0.65),
    inset 0 0 24px rgba(0,0,0,0.45);
  animation: concierge-in 160ms ease-out;
}
.concierge-card.info { border-left-color: rgba(120,120,160,0.55); }
.concierge-card.conflict { border-left-color: rgba(180,30,30,0.75); }
@keyframes concierge-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.concierge-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.concierge-icon { color: #c8a85a; font-size: 14px; }
.concierge-title {
  flex: 1;
  color: #d4c4a8;
  font-size: 13px;
  font-weight: bold;
}
.concierge-close {
  background: transparent;
  border: none;
  color: #8a8a7a;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}
.concierge-close:hover { color: #e0d0c0; }
.concierge-card-body {
  color: #a8a090;
  font-size: 12px;
  line-height: 1.5;
  margin-bottom: 11px;
}
.concierge-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}
.concierge-btn {
  border: none;
  border-radius: 4px;
  padding: 6px 11px;
  font-family: Consolas, monospace;
  font-size: 12px;
  cursor: pointer;
  background: #2a2a2a;
  color: #c0b8a8;
}
.concierge-btn:hover { background: #333333; color: #e0d0c0; }
/* compound, not bare: .concierge-btn is ALSO always present on a primary button, so a
   bare .concierge-btn-primary would tie with it on background and color at the same
   specificity, resolved only by source order. exactly the tie shape
   test/unit/css-specificity-ties.test.js exists to catch project-wide. compound wins
   deterministically regardless of order. */
.concierge-btn.concierge-btn-primary { background: rgba(180,30,30,0.55); color: #f0e0d0; font-weight: bold; }
.concierge-btn.concierge-btn-primary:hover { background: rgba(180,30,30,0.75); color: #fffbe8; }

/* mandatory Drive conflict resolution / deliberately NOT a fade-modal: no click-to-close,
   no Escape, no × control anywhere.
   the z-index sits inside the sync-messaging neighborhood rather than the general app-modal
   tier, specifically so Compare, opened FROM this modal, stacks correctly on top of it.
   a deliberate, called-out exception to one-modal-at-a-time.
   also above the mobile toolbar's own z-index: a backdrop meant to be non-dismissable
   cannot sit under a still-clickable toolbar. */
#conflictBackdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.72);
  z-index: 2650;
}
#conflictBackdrop.visible { display: block; }
#conflictModal {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2651;
  width: 420px;
  max-width: calc(100vw - 36px);
  background: #151515;
  border-radius: 6px;
  border-left: 3px solid rgba(180,30,30,0.75);
  padding: 20px 22px 18px 22px;
  font-family: Consolas, monospace;
  box-shadow:
    0 16px 50px rgba(0,0,0,0.70),
    inset 0 0 30px rgba(0,0,0,0.55);
}
#conflictModal.visible { display: block; }
#conflictTitle { color: #d4c4a8; font-size: 15px; font-weight: bold; margin: 0 0 10px 0; }
#conflictMessage { color: #a8a090; font-size: 12.5px; line-height: 1.5; margin: 0 0 14px 0; }
#conflictActions { display: flex; flex-wrap: wrap; gap: 7px; }

/* mandatory local-tab conflict resolution / the same shape and rationale as the Drive
   conflict modal above, own DOM.
   z-index one notch above the Drive modal's, in case both are ever pending for the same
   doc at once, so the more narrowly-scoped decision sits visually on top. still below
   #compareOverlay so Compare stacks on top of either, and above the mobile toolbar for the
   same reason the Drive modal is. */
#localConflictBackdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.72);
  z-index: 2660;
}
#localConflictBackdrop.visible { display: block; }
#localConflictModal {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2661;
  width: 420px;
  max-width: calc(100vw - 36px);
  background: #151515;
  border-radius: 6px;
  border-left: 3px solid rgba(180,30,30,0.75);
  padding: 20px 22px 18px 22px;
  font-family: Consolas, monospace;
  box-shadow:
    0 16px 50px rgba(0,0,0,0.70),
    inset 0 0 30px rgba(0,0,0,0.55);
}
#localConflictModal.visible { display: block; }
#localConflictTitle { color: #d4c4a8; font-size: 15px; font-weight: bold; margin: 0 0 10px 0; }
#localConflictMessage { color: #a8a090; font-size: 12.5px; line-height: 1.5; margin: 0 0 14px 0; }
#localConflictActions { display: flex; flex-wrap: wrap; gap: 7px; }

/* compare view / word-level highlighted diff */
/* z-index kept above both conflict modals, and above the mobile toolbar, since Compare
   can be opened from either conflict modal on mobile too. */
#compareOverlay {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.7);
  z-index: 2700;
  align-items: center;
  justify-content: center;
}
#compareOverlay.visible { display: flex; }
#compareModal {
  background: #151515;
  border-radius: 6px;
  width: 900px;
  max-width: 94vw;
  max-height: 82vh;
  display: flex;
  flex-direction: column;
  font-family: Consolas, monospace;
  box-shadow:
    0 16px 50px rgba(0,0,0,0.70),
    inset 0 0 30px rgba(0,0,0,0.55);
}
#compareHeader {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px 10px 18px;
  border-bottom: 1px solid rgba(180,30,30,0.20);
}
#compareHeader h3 { color: #d4c4a8; font-size: 16px; font-weight: bold; margin: 0; }
#compareClose {
  background: transparent;
  border: none;
  color: #8a8a7a;
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}
#compareClose:hover { color: #e0d0c0; }
#compareNote {
  color: #8a7a5a;
  font-size: 12px;
  padding: 8px 18px 0 18px;
}
#compareNote:empty { display: none; }
#compareColumns {
  display: flex;
  gap: 1px;
  flex: 1;
  min-height: 0;
  padding: 12px 14px 16px 14px;
}
/* only present when openCompare() is called with an onRestore callback. absent for the
   conflict-resolution calls, which have their own action buttons elsewhere and never want
   a Restore action here. */
#compareFooter {
  flex-shrink: 0;
  padding: 0 18px 16px 18px;
  text-align: right;
}
#compareRestoreBtn {
  background: rgba(180,30,30,0.20);
  border: 1px solid rgba(180,30,30,0.45);
  border-radius: 4px;
  color: #e0d0c0;
  font-family: Consolas, monospace;
  font-size: 13px;
  padding: 8px 16px;
  cursor: pointer;
}
#compareRestoreBtn:hover { background: rgba(180,30,30,0.32); }
.compare-col {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.compare-col-head {
  color: #b0a890;
  font-size: 12px;
  font-weight: bold;
  padding: 4px 10px 8px 10px;
}
.compare-col-body {
  flex: 1;
  overflow-y: auto;
  background: #0e0e0e;
  border-radius: 4px;
  padding: 12px 14px;
  color: #a8a090;
  font-size: 13px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}
.compare-col-body ins {
  text-decoration: none;
  background: rgba(70,150,70,0.28);
  color: #cfe8c0;
  border-radius: 2px;
}
.compare-col-body del {
  text-decoration: line-through;
  background: rgba(180,30,30,0.28);
  color: #e8c0b8;
  border-radius: 2px;
}
.compare-para { margin-bottom: 8px; }
.compare-empty { color: #6a5a4a; font-style: italic; }

/* mobile hamburger and panel */
.mob-hamburger {
  display: none;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: #B02010;
  padding: 6px 12px;
  cursor: pointer;
  flex-shrink: 0;
  position: relative;
}
.mob-hamburger::before {
  content: '';
  display: block;
  width: 22px;
  height: 19px;
  background-image:
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor);
  background-size: 100% 3px;
  background-position: 0 0, 0 8px, 0 16px;
  background-repeat: no-repeat;
}
.mob-hamburger:hover { color: #e05030; }
.mob-hamburger.open::before {
  background-image:
    linear-gradient(to bottom, #B02010 1px, #000 1px, #000 2px, #B02010 2px),
    linear-gradient(to bottom, #B02010 1px, #000 1px, #000 2px, #B02010 2px),
    linear-gradient(to bottom, #B02010 1px, #000 1px, #000 2px, #B02010 2px);
}
/* glow indicator signalling "something new is in the menu" on mobile, where the Codex
   icon itself lives inside the closed drawer and can't glow directly.
   ::before is fully consumed drawing the 3-bar icon, so this uses ::after, given actual
   text content rather than an empty background box: buildOutGlow animates text-shadow,
   which has nothing to render against without a glyph.
   positioned as a small corner badge over the bars, which needs the parent's own
   position:relative. */
.mob-hamburger.glow::after {
  content: '\25CF';
  position: absolute;
  top: 2px;
  right: 4px;
  font-size: 10px;
  line-height: 1;
  color: #e6be5a;
  animation: buildOutGlow 1.6s ease-in-out infinite;
}

/* pixel-font wordmark, the toolbar's far-left brand mark on every viewport. the first
   visible child of #toolbar: the hamburger sits before it in the DOM but is display:none
   outside the mobile breakpoint, so this ends up flush against the toolbar's left edge on
   desktop. */
#toolbarWorg {
  display: block;
  flex-shrink: 0;
  image-rendering: pixelated;
}

#mobPanel {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  /* about two thirds of the screen, not edge-to-edge: dominant but not literally
       full-width, with the backdrop dimming and blocking the rest behind it. */
  width: 66vw;
  min-width: 220px;
  max-width: 420px;
  background: #151515;
  border-bottom: 1px solid rgba(180,30,30,0.18);
  border-right: 1px solid rgba(180,30,30,0.18);
  box-shadow: 16px 0 50px rgba(0,0,0,0.6);
  padding: 6px 0 8px 0;
  /* a real height, not just a cap: the drawer's own background fills the full column all
       the way down even when the menu content is short, so the footer's margin-top:auto has
       empty flex space to push into instead of leaving a gap that shows the backdrop's
       dimmed color underneath. content past this height still scrolls.
       the 100% term resolves against #toolbar's own rendered height, since it is the
       containing block, not a hardcoded px guess, so it is exact regardless of safe-area
       insets or font metrics on the real device.
       100lvh, not 100dvh: dvh under-reports by env(safe-area-inset-top) in iOS standalone
       with a black-translucent status bar. */
  height: calc(100lvh - 100%);
  flex-direction: column;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  z-index: 1000;
}
#mobPanel.open { display: flex; }

/* selection-triggered formatting toolbar, mirroring #editorContextMenu's fixed-position
   popup treatment but positioned from the selection's own bounding rect rather than a
   click point. */
#mobSelToolbar {
  display: none;
  position: fixed;
  z-index: 3050;
  background: rgba(20, 15, 10, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
}
.mob-sel-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: transparent;
  border: none;
  color: #b0a890;
  font-family: Consolas, monospace;
  font-size: 16px;
  cursor: pointer;
}
.mob-sel-btn:hover, .mob-sel-btn:active { background: rgba(180,30,30,0.35); color: #e0d0c0; }
.mob-sel-btn.active { background: rgba(180,30,30,0.55); color: #f0e0d0; }
/* divides the inline-formatting buttons from the block-structure ones sharing this popup,
   the same red-on-dark language as .mob-sep, just a vertical rule since this is one row. */
.mob-sel-sep {
  align-self: stretch;
  width: 1px;
  margin: 6px 0;
  background: rgba(180,30,30,0.25);
}

.mob-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  background: transparent;
  border: none;
  color: #b0a890;
  padding: 6px 16px;
  text-align: left;
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: 1px;
  cursor: pointer;
}
.mob-item:hover, .mob-item:active { background: rgba(180,30,30,0.35); color: #e0d0c0; }
.mob-item.active { background: rgba(180,30,30,0.55); color: #f0e0d0; }
/* #mobCodex's own "new item waiting" glow once the drawer is open. direct text content,
   so text-shadow renders against it directly, unlike .spells-head. */
#mobCodex.glow { animation: buildOutGlow 1.6s ease-in-out infinite; }
/* the same fixed, centered icon slot as .mob-group-hdr, keeping every leaf item's icon and
   label pair aligned the same way. */
.mob-item .feature-emoji {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
}

.mob-sep {
  height: 1px;
  background: rgba(180,30,30,0.20);
  margin: 4px 0;
}

/* accordion group: a header button toggling .expanded on its parent, revealing the items.
   .mob-group-gated auto-hides itself if every item inside is individually gated js-hidden,
   so a fully-locked group never shows an empty header. that keeps buildout.js's per-item
   gating as the only source of truth rather than teaching it about these wrappers too.
   Codex isn't one of these groups at all: it is a flat top-level item, since it only ever
   opens the modal directly. */
.mob-group-gated:not(:has(.mob-item:not(.js-hidden))) {
  display: none;
}
.mob-group-hdr {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: transparent;
  border: none;
  color: #6a5a4a;
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: 1px;
  padding: 8px 16px 8px 16px;
  text-align: left;
  cursor: pointer;
  user-select: none;
}
.mob-group-hdr:hover, .mob-group-hdr:active { color: #b0a890; }
/* fixed, centered icon slot: the owner-drawn pixel art isn't uniformly framed within its
   own canvas, so left at natural size the icons throw off both the vertical centering AND
   the horizontal start of the label text beside them. pinning every icon to the same box
   and centering the art inside it is the layout-level fix; any remaining per-icon offset
   is baked into that icon's artwork, not fixable here. */
.mob-group-hdr .feature-emoji {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
}
.mob-group-hdr::after {
  content: '\203A'; /* › */
  margin-left: auto;
  transform: rotate(90deg);
  transition: transform 0.15s ease;
}
.mob-group.expanded .mob-group-hdr::after { transform: rotate(-90deg); }
.mob-group-items {
  display: none;
}
.mob-group.expanded .mob-group-items {
  display: block;
}

.mob-warp-item {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  color: #b0a890;
  padding: 8px 16px 8px 24px;
  text-align: left;
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: 1px;
  cursor: pointer;
}
.mob-warp-item:hover, .mob-warp-item:active { background: rgba(180,30,30,0.35); color: #e0d0c0; }

.mob-warp-empty {
  color: #4a4a3a;
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: 1px;
  padding: 8px 16px 8px 24px;
}

/* shared by the statusbar readout and the hamburger drawer's footer: the same icon, count,
   and title, two different homes, driven by the same updateMobSkullStatus() targets loop. */
.skull-status {
  display: none;
  align-items: center;
  gap: 5px;
}
#mobSkullStatus {
  margin-right: auto;
}
.skull-status-icon {
  width: 12px;
  height: 13px;
  vertical-align: middle;
  image-rendering: pixelated;
  flex-shrink: 0;
}
.skull-status-count {
  color: #B02010;
  font-family: Consolas, monospace;
  font-size: 11px;
}
.skull-status-title {
  color: #B02010;
  font-family: Consolas, monospace;
  font-size: 11px;
  font-style: italic;
  letter-spacing: 0.5px;
}

/* mobile and tablet */
/* the second condition catches phones in landscape, capped so an actual tablet or a
   resized desktop window doesn't get pulled in, and scoped to phone-sized screens. */
@media (max-width: 480px), (orientation: landscape) and (max-height: 480px) and (max-width: 930px) {
  #toolbar {
    position: relative;
    /* above #mobPanelBackdrop so the toolbar itself stays undimmed and clickable while the
           drawer is open. #mobPanel, nested inside #toolbar, isn't affected either way: a
           positioned descendant's z-index ranks it within its ancestor's stacking context,
           not against the ancestor's own z-index. */
    z-index: 2550;
    flex-wrap: nowrap;
    /* iOS Add to Home Screen runs the app edge-to-edge under a translucent status bar.
           without safe-area padding the toolbar renders behind the clock and battery and its
           buttons become untappable. padding the top by the inset fills the dark bar under the
           status bar and drops the controls below it; the horizontal insets keep buttons clear
           of the notch in landscape.
           env(safe-area-inset-*) is 0 everywhere it doesn't apply, so this is inert off iOS. */
    padding: calc(8px + env(safe-area-inset-top)) max(10px, env(safe-area-inset-right)) 6px max(10px, env(safe-area-inset-left));
    gap: 2px;
    flex-shrink: 0;
  }
  .mob-hamburger { display: flex; }
  #mobSelToolbar.open { display: flex; }
  #toolbarWorg { margin-left: 4px; }
  /* the divider's downward glow is desktop-only. #mobPanel is nested INSIDE #toolbar and
       also starts at top:100%, and at this breakpoint #toolbar IS a stacking context, so the
       pseudo-element would be trapped in it and paint its red wash across the drawer's top
       edge. the divider's core line and upward bloom both still apply; only the half falling
       past the bar is dropped. */
  #toolbar::after { display: none; }
  /* dims and blocks the rest of the screen while the drawer is open.
       this is a sibling of #toolbar itself, not of #mobPanel: #mobPanel is nested INSIDE
       #toolbar, which establishes its own stacking context, so a backdrop nested in there too
       would compete directly with the toolbar's own controls for z-index instead of sitting
       behind the whole toolbar as one unit.
       visibility uses :has() to reach across that sibling boundary and still avoid any JS,
       reusing #mobPanel's own .open class with no separate state to sync.
       z-index sits below #toolbar, kept undimmed and clickable since the hamburger's trigger
       lives there and should stay usable while its drawer is open, and below #mobPanel, which
       despite being nested inside #toolbar still renders above this automatically since the
       whole subtree already outranks it. */
  #mobPanelBackdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    z-index: 400;
  }
  #toolbar:has(#mobPanel.open) ~ #mobPanelBackdrop { display: block; }
  /* skull tally, relocated here from the statusbar. a footer row INSIDE #mobPanel, its last
       child rather than a separate overlay behind it, with margin-top:auto pushing it to the
       bottom of the drawer's flex column.
       being a normal child means it shares the drawer's background and width automatically,
       with no separate position, sizing, or background to keep in sync, and no gap of
       mismatched backdrop color ever shows beneath a short menu.
       visibility is just a live count; being inside #mobPanel already gates it on the drawer
       being open. */
  #mobPanelSkullBar {
    margin-top: auto;
    flex-shrink: 0;
    border-top: 1px solid rgba(180,30,30,0.18);
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom)) 16px;
    gap: 6px;
  }
  #mobPanelSkullBar.active { display: flex; }
  #btnB, #btnI, #btnU, #btnCh, #btnSc, #btnPar { display: none; }
  /* hides the wrapper itself, not just its now-hidden children: without this its own
       padding still reserves a visible empty gutter at the tab row's right edge. */
  #docTabsFormat { display: none; }
  .feature-wrap { display: none; }
  .spells-wrap { display: none; }
  .tb-sep { display: none; }
  #btnSignIn, #btnDocs, #btnSignOut { display: none; }
  #editorCol { padding: 0 10px; }
  #editorScroll { width: 100%; margin: 0; }
  #editorWallLeft { display: none; }
  #editorWallRight { display: none; }
  #editorScrollThumb { display: none; }
  /* edge-to-edge on mobile, no floating panel slab on a screen this tight, the same
       reasoning as the wall lines and scrollbar going display:none just above.
       width and margin override the desktop default's centered fallback back to full-bleed.
       without this, mobile would show, and PERMANENTLY keep, the desktop-centered width,
       since JS clears its inline override here rather than setting one. */
  #editorUnit { background: transparent; border-radius: 0; width: 100%; margin: 0; }
  /* #editorUnit goes fully transparent just above, and the `background` shorthand clears
       both rim layers along with the fill, which is correct for full-bleed mobile. but
       #docTabs and .doctab are transparent on desktop precisely SO that panel surface can
       show through them, and with it gone they would paint nothing and the strip would vanish
       into the slate. restore an explicit surface on the row: the tabs stay transparent and
       show it through, so the row reads as one flat field with only the active tab's border
       on it, the same structure the desktop layout has. */
  #docTabs { background: var(--canvas-surface); }
  #titleEdit { font-size: 22px; width: 100%; margin: 0 0 4px; }
  #authorRow { width: 100%; margin: 0 0 10px; }
  /* kept at 16px even though it visually crowds the byline: iOS Safari auto-zooms the whole
       page on focus for any text field under 16px, and won't zoom back out on its own. */
  #authorBy, #authorEdit { font-size: 16px; }
  #editorScroll { -webkit-overflow-scrolling: touch; }
  /* bottom safe-area inset so the dark statusbar fills the home-indicator area instead of
       leaving a gap below it. */
  #statusbar {
    padding: 3px max(10px, env(safe-area-inset-right)) calc(3px + env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left));
    gap: 10px;
    /* pin the bar to a single row on mobile. without this the wordy save status wraps onto
           several lines and the statusbar balloons, stealing scarce typable height exactly when
           the on-screen keyboard is already crushing the editor. #saveLabel is the one item
           allowed to shrink and ellipsize so the row always fits. */
    flex-wrap: nowrap;
    white-space: nowrap;
  }
  #charCount { display: none; }
  #copyrightName { display: none; }
  /* dropped from the statusbar on mobile: the skull tally is one tap away on the toolbar
       skull, so a persistent copy here only crowded the bar and pushed the save status into
       wrapping. */
  #mobSkullStatus.active { display: none; }
  /* the lone flexible item: shrinks and ellipsizes when the row is tight, so a long backup
       message truncates instead of wrapping the whole bar. the safe/risk dot beside it still
       conveys the at-a-glance state. */
  #saveLabel {
    flex: 0 1 auto;
    min-width: 0;
    text-align: left;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* let the invisible typing-sprite spacer collapse to 0 on a tight row instead of holding
       its desktop minimum. */
  #typingSprite { min-width: 0; }
  #skullLogoWrap { margin-right: 0; }
  #driveModal { width: 90vw; max-height: 60vh; max-height: 60dvh; }
  #syncConcierge { right: 8px; left: 8px; bottom: 40px; width: auto; }
  #compareColumns { flex-direction: column; overflow-y: auto; }
  .compare-col-body { max-height: 40vh; }

  /* full-screen writing mode: editor.js adds this while the editor holds focus, i.e. the
       keyboard is up. hand the space above the keyboard to the draft by dropping the
       statusbar and folding the title and byline away.
       #toolbar deliberately stays put, its hamburger being the always-there way back out.
       the title gets the byline's animated-collapse affordance so both slide shut together
       rather than snapping. */
  #titleEdit {
    max-height: 1.4em;
    overflow: hidden;
    transition: opacity 0.25s ease, max-height 0.25s ease, margin-bottom 0.25s ease;
  }
  #app.writing-focus #statusbar { display: none; }
  #app.writing-focus #titleEdit,
  #app.writing-focus #authorRow {
    opacity: 0;
    max-height: 0;
    margin-bottom: 0;
    overflow: hidden;
  }
}

@supports (-webkit-touch-callout: none) {
  html, body { position: fixed; width: 100%; }
}

/* dungeon mode */
#editorCol { position: relative; }

.dungeon-active #toolbar,
.dungeon-active #statusbar { display: none !important; }

.dungeon-active #titleEdit,
.dungeon-active #authorRow,
.dungeon-active #docTabs { display: none; }

/* black out the whole workspace, not just #editorCol, so no gray frame shows anywhere
   around the editor, side panels included. */
.dungeon-active #middle { background: #000; }
.dungeon-active #editorCol { background: #000; }

/* #editorUnit normally carries its own panel surface and rim-light layers. without this
   override that lit slab and its two edge hairlines would show through and around #editor
   once #editorCol goes pure black, breaking the no-frame full-bleed intent.
   the `background` shorthand is load-bearing rather than `background-color`: it clears the
   rim layers too, which is why the wall lines need no separate dungeon override, still
   carrying no fill of their own. */
.dungeon-active #editorUnit {
  background: transparent;
  border-radius: 0;
}

.dungeon-active #editorScroll {
  overflow: hidden;
  /* #dungeonFade's gradient is only fully transparent past roughly a third of the way down.
       with the title and author row hidden above, a short document's first lines would
       otherwise render right at the top of #editorCol, inside the fade's darkest band. this
       pushes them below it regardless of document length. */
  padding-top: 140px;
}

.dungeon-active #editor {
  background: #000;
}

.dungeon-active #editor,
.dungeon-active #editor * {
  color: #ff1a1a !important;
  text-shadow: 0 0 8px rgba(255,40,10,0.8), 0 0 18px rgba(180,0,0,0.4);
}

#dungeonFade {
  display: none;
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 100%;
  background: linear-gradient(to bottom,
    rgba(0,0,0,1)    0%,
    rgba(0,0,0,0.88) 9%,
    rgba(0,0,0,0.6)  19%,
    rgba(0,0,0,0.18) 28%,
    transparent      35%
  );
  pointer-events: none;
  z-index: 5;
}

.dungeon-active #dungeonFade { display: block; }

/* free to use, always active, with no locked or unlocked state to distinguish, so unlike
   most gated buttons here there is just one resting look plus a hover glow. */
#dungeonEscapeBtn {
  display: none;
  position: fixed;
  top: 14px;
  left: 18px;
  background: transparent;
  border: 1px solid rgba(180,30,30,0.55);
  border-radius: 4px;
  color: #902010;
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 5px 12px;
  cursor: pointer;
  z-index: 100;
  letter-spacing: 0.5px;
  user-select: none;
  transition: color 0.2s, border-color 0.2s, text-shadow 0.2s, background 0.2s;
}

#dungeonEscapeBtn:hover {
  color: #ff3020;
  border-color: rgba(255,50,30,0.8);
  background: rgba(80,5,0,0.2);
  text-shadow: 0 0 8px rgba(255,40,10,0.85), 0 0 16px rgba(180,0,0,0.5);
}

.dungeon-active #dungeonEscapeBtn { display: block; }

/* strike */
/* sits inside #editorScroll, not #editor, since it must stay outside the contenteditable
   region, so it scrolls natively with the document rather than needing scroll bookkeeping
   in JS. */
#strikeOverlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 4;
}
.strike-line {
  stroke: #ff2a1a;
  stroke-width: 1.5;
  opacity: 0.8;
  filter: drop-shadow(0 0 4px rgba(255,40,10,0.85)) drop-shadow(0 0 10px rgba(180,0,0,0.5));
}
/* pointer-events re-enabled per-dot. the overlay itself stays none: lines and empty SVG
   space must keep passing clicks through to the editor underneath, and only the dots are
   real click targets. */
.strike-dot {
  fill: #ff2a1a;
  filter: drop-shadow(0 0 6px rgba(255,40,10,0.9));
  pointer-events: auto;
  cursor: pointer;
  transition: r 0.1s ease, fill 0.1s ease;
}
.strike-dot:hover {
  fill: #ff6040;
  r: 6;
}

#strikeBadge {
  display: none;
  position: fixed;
  top: 14px;
  right: 18px;
  background: rgba(10,0,0,0.75);
  border: 1px solid rgba(180,30,30,0.55);
  border-radius: 4px;
  color: #ff3020;
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 5px 12px;
  z-index: 100;
  letter-spacing: 0.5px;
  user-select: none;
  text-shadow: 0 0 6px rgba(255,40,10,0.7);
}
#strikeBadge.visible { display: block; }

/* auto-capitalize undo mark / the same measure-a-Range-and-draw-a-box technique as
   #strikeOverlay above, sitting inside #editorScroll so it scrolls natively.
   only ever tracks the single most recent fix, positioned over that one character.
   invisible until hovered, and a click opens the popover instead of undoing immediately,
   so a stray click can't silently revert a fix mid-typing. */
#autocapFixMark {
  position: absolute;
  pointer-events: auto;
  cursor: pointer;
  z-index: 5;
  border-bottom: 2px solid transparent;
}
#autocapFixMark:hover {
  border-bottom-color: rgba(220, 190, 110, 0.85);
}

#autocapFixPopover {
  position: fixed;
  z-index: 3050;
  background: rgba(20, 15, 10, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
  font-family: Consolas, monospace;
  font-size: 12px;
}
#autocapFixUndoBtn {
  display: block;
  font: inherit;
  background: transparent;
  border: none;
  color: rgba(240, 224, 208, 0.95);
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  text-align: left;
}
#autocapFixUndoBtn:hover { background: rgba(180, 30, 30, 0.35); }

/* Charms gutter's click popover, the same floating-at-a-point chrome as the context menu
   above, positioned next to the clicked gutter icon.
   z-index matches those, below the shared modal tier, since this is a lightweight anchored
   popover rather than a modal. */
#charmGutterPopover {
  position: fixed;
  z-index: 3050;
  width: 260px;
  max-width: 260px;
  background: rgba(20, 15, 10, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 10px 12px;
}
#charmGutterPopoverHeader {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
#charmGutterPopoverIcon {
  font-size: 16px;
  line-height: 1.4;
}
/* dismiss-only, with the same neutral gold hover as the list panel's close button and
   deliberately NOT the red delete-hover color below, so the two controls never read as
   visually interchangeable. */
#charmGutterPopoverCloseBtn {
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.55);
  font-size: 16px;
  cursor: pointer;
  padding: 0 2px;
}
#charmGutterPopoverCloseBtn:hover { color: rgba(240, 224, 208, 0.9); }
#charmGutterPopoverText {
  margin-top: 8px;
  color: rgba(240, 224, 208, 0.95);
  font-size: 13px;
  white-space: pre-wrap;
  word-break: break-word;
}
/* the copy button, hidden unless this charm has a customIcon, sits at the left while the
   delete button stays pinned right via margin-left:auto. that still works whether or not
   the copy button is present, since js-hidden removes it from flex layout entirely rather
   than just hiding it visually. */
#charmGutterPopoverActions {
  display: flex;
  align-items: center;
  margin-top: 10px;
}
#charmGutterPopoverCopyIconBtn {
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.4);
  cursor: pointer;
  font-size: 12px;
  padding: 0;
}
#charmGutterPopoverCopyIconBtn:hover { color: rgba(220, 190, 110, 0.75); }
#charmGutterPopoverDeleteBtn {
  display: block;
  margin: 0 0 0 auto;
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.4);
  cursor: pointer;
  font-size: 12px;
  padding: 0;
}
#charmGutterPopoverDeleteBtn:hover { color: rgba(220, 60, 60, 0.9); }
/* a custom-drawn icon renders as a small canvas in place of the emoji textContent
   paintPixelIcon() targets. canvas defaults to display:inline, which leaves a baseline gap
   under it; block avoids that in every one of these icon slots. */
#charmGutterPopoverIcon canvas { display: block; }

/* Search and Destroy's match highlight / the layer is position:fixed with inset:0, so its
   box is exactly the viewport, and a child positioned with plain px equal to a Range's
   getClientRects() values, also viewport-relative, lands in the right spot with zero
   coordinate translation. unlike #strikeOverlay's SVG-local math, that overlay scrolls WITH
   the document while this one re-measures and redraws on scroll instead. */
#findReplaceHighlightLayer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 90;
}
.findReplace-highlight-rect {
  position: absolute;
  background: rgba(220, 190, 110, 0.35);
  outline: 1px solid rgba(220, 190, 110, 0.7);
  border-radius: 2px;
}

/* Charms' resolved-anchor highlight, the same layer technique as above with its own red
   accent so it reads as visually distinct from a Search and Destroy match. */
#charmHighlightLayer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 90;
}
.charm-highlight-rect {
  position: absolute;
  background: rgba(200, 40, 40, 0.30);
  outline: 1px solid rgba(220, 60, 60, 0.75);
  border-radius: 2px;
}

/* charms gutter / the same fixed inset:0 layer technique as the highlight layer above,
   but this one holds real buttons rather than decorative boxes, so pointer-events stays
   none on the layer and is opened back up per-icon below.
   deliberately no background, border, or shadow anywhere on the layer or at rest on an
   icon: it must have zero visual footprint beyond the glyphs themselves, and even those
   only appear once the player opts in via Build Out. */
#charmsGutterLayer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 90;
}
.charm-gutter-icon {
  position: absolute;
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  background: transparent;
  border: none;
  border-radius: 4px;
  font-size: 15px;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}
.charm-gutter-icon:hover,
.charm-gutter-icon:focus-visible {
  background: rgba(180, 30, 30, 0.15);
}
.charm-gutter-icon canvas { display: block; }

/* outline */
#outlineBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.48);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#outlineBackdrop.visible { display: block; }
#outlineBackdrop.shown { opacity: 1; }
#outlineBackdrop.fading { opacity: 0; transition: opacity 2s ease; }

/* #outlinePanel's right-docked shape lives in the shared four-panel rule near
   #skullCounterPanel above. the editor canvas glides left to make room, sized from this
   panel's own rendered width. */

#outlineHeader {
  flex-shrink: 0;
  padding: 10px 14px;
  color: rgba(200, 165, 80, 0.75);
  font-size: 15px;
  letter-spacing: 1.5px;
  display: flex;
  align-items: center;
  gap: 8px;
  user-select: none;
}
/* the equipped skin's own tiny tracker icon, shared with the toolbar logo and redrawn
   together in redrawForSkin() so a skin change updates both. */
#outlineSkullIcon { flex-shrink: 0; width: 18px; height: 18px; }
/* dedicated row for the panel's action buttons, directly under the title bar, just placed
   next to each other with no special grouping. */
#outlineActionBar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 14px 10px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
}
#outlineBonesBtn {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.35);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.65);
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: normal;
  padding: 4px 9px;
  cursor: pointer;
}
#outlineBonesBtn:hover { color: rgba(220, 190, 110, 0.9); border-color: rgba(180, 140, 60, 0.55); }
#outlineBonesBtn.active {
  color: rgba(220, 190, 110, 0.95);
  border-style: solid;
  border-color: rgba(180, 140, 60, 0.6);
  background: rgba(180, 140, 60, 0.12);
}
#outlineSelectBtn {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.35);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.65);
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: normal;
  padding: 4px 9px;
  cursor: pointer;
}
#outlineSelectBtn:hover { color: rgba(220, 190, 110, 0.9); border-color: rgba(180, 140, 60, 0.55); }
#outlineSelectBtn.active {
  color: rgba(220, 190, 110, 0.95);
  border-style: solid;
  border-color: rgba(180, 140, 60, 0.6);
  background: rgba(180, 140, 60, 0.12);
}
/* select mode's action bar, shown only while select mode is active. sits between the
   header and the scrollable tree. */
#outlineSelectBar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 14px;
  background: rgba(180, 140, 60, 0.06);
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
  font-family: Consolas, monospace;
  font-size: 11.5px;
  color: rgba(200, 165, 80, 0.75);
}
#outlineAddToPartBtn {
  margin-left: auto;
  background: transparent;
  border: 1px solid rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 11px;
  padding: 4px 9px;
  cursor: pointer;
}
#outlineAddToPartBtn:hover:not(:disabled) { background: rgba(180, 140, 60, 0.12); }
#outlineAddToPartBtn:disabled { opacity: 0.4; cursor: default; }
#outlineRemoveFromPartBtn {
  background: transparent;
  border: 1px solid rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 11px;
  padding: 4px 9px;
  cursor: pointer;
}
#outlineRemoveFromPartBtn:hover:not(:disabled) { background: rgba(180, 140, 60, 0.12); }
#outlineRemoveFromPartBtn:disabled { opacity: 0.4; cursor: default; }
#outlineSelectDoneBtn {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.35);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.65);
  font-family: Consolas, monospace;
  font-size: 11px;
  padding: 4px 9px;
  cursor: pointer;
}
#outlineSelectDoneBtn:hover { color: rgba(220, 190, 110, 0.9); }
/* selection checkboxes, only rendered into rows while select mode is on. */
.outline-select-checkbox {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  accent-color: rgba(200, 165, 80, 0.85);
  cursor: pointer;
}
/* the Part popover, the same floating-at-a-point mechanism as #editorContextMenu, just
   anchored under the button rather than a click point. */
#outlinePartMenu {
  position: fixed;
  z-index: 3010;
  min-width: 180px;
  max-width: 260px;
  background: #1a1611;
  border: 1px solid rgba(180, 140, 60, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
  padding: 6px;
  font-family: Consolas, monospace;
  font-size: 12px;
}
#outlinePartMenuList { display: flex; flex-direction: column; gap: 2px; max-height: 180px; overflow-y: auto; }
.outline-part-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-radius: 3px;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 6px 8px;
  cursor: pointer;
}
.outline-part-menu-item:hover { background: rgba(180, 140, 60, 0.15); }
.outline-part-menu-empty {
  padding: 8px;
  color: rgba(200, 165, 80, 0.5);
  font-family: Consolas, monospace;
  font-size: 11.5px;
  font-style: italic;
}
#outlinePartMenuNew {
  display: flex;
  gap: 4px;
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px solid rgba(180, 140, 60, 0.2);
}
#outlinePartMenuInput {
  flex: 1;
  min-width: 0;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(180, 140, 60, 0.3);
  border-radius: 3px;
  color: rgba(240, 224, 208, 0.9);
  font-family: Consolas, monospace;
  font-size: 11.5px;
  padding: 5px 7px;
}
#outlinePartMenuCreateBtn {
  flex-shrink: 0;
  background: rgba(180, 140, 60, 0.15);
  border: 1px solid rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(220, 190, 110, 0.9);
  font-family: Consolas, monospace;
  font-size: 11px;
  padding: 5px 8px;
  cursor: pointer;
}
#outlinePartMenuCreateBtn:hover { background: rgba(180, 140, 60, 0.25); }
/* parts / inline, collapsible clusters woven into the primary tree rather than a separate
   tab. the same expand and collapse language as .outline-scene, with its own visual
   identity so a cluster reads as a drawer, not another chapter. */
.outline-part-cluster {
  border: 1px solid rgba(180, 140, 60, 0.3);
  border-radius: 4px;
  margin: 6px 10px;
  background: rgba(180, 140, 60, 0.04);
}
.outline-part-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 14px;
  cursor: pointer;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 15px;
  font-weight: bold;
}
.outline-part-header:hover { background: rgba(180, 140, 60, 0.08); }
.outline-part-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.outline-part-count { flex-shrink: 0; font-weight: normal; font-size: 11px; opacity: 0.55; }
.outline-part-body { display: none; flex-direction: column; padding: 0 0 6px; }
.outline-part-cluster.expanded .outline-part-body { display: flex; }
.outline-part-beat-wrap {
  margin: 4px 10px;
  padding: 6px 8px;
  border-left: 2px solid rgba(180, 140, 60, 0.3);
  background: rgba(0, 0, 0, 0.12);
}
.outline-part-beat-source {
  font-size: 10.5px;
  opacity: 0.55;
  margin-bottom: 3px;
  font-family: Consolas, monospace;
}

/* insert dividers / a thin line between every item at every level. desktop reveals the
   "+" on hover; touch has no hover, so outline.js's click handler toggles .revealed on tap
   instead, and a second tap on the now-visible "+" opens the menu. */
.outline-divider {
  position: relative;
  height: 9px;
  margin: 0 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.outline-divider::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: rgba(180, 140, 60, 0.16);
}
.outline-divider-plus {
  position: relative;
  z-index: 1;
  width: 16px;
  height: 16px;
  line-height: 14px;
  border-radius: 50%;
  background: #1a1611;
  border: 1px solid rgba(180, 140, 60, 0.55);
  color: rgba(220, 190, 110, 0.9);
  font-size: 12px;
  text-align: center;
  padding: 0;
  cursor: pointer;
  opacity: 0;
  transform: scale(0.85);
  transition: opacity 0.12s, transform 0.12s;
}
/* :focus-within is the keyboard twin of the :hover reveal below it. without it the "+"
   stays at opacity 0 while focused, so a keyboard writer tabbing through the outline lands
   on a control they cannot see. the .revealed class is driven from a CLICK, so it does not
   cover the keyboard path. enforced by test/unit/a11y-baseline.test.js. */
.outline-divider:hover .outline-divider-plus,
.outline-divider:focus-within .outline-divider-plus,
.outline-divider.revealed .outline-divider-plus {
  opacity: 1;
  transform: scale(1);
}
.outline-divider-plus:hover { background: rgba(180, 140, 60, 0.25); }

/* insert menu, the same floating-at-a-point mechanism as #editorContextMenu. */
#outlineInsertMenu {
  position: fixed;
  z-index: 3010;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 120px;
  background: #1a1611;
  border: 1px solid rgba(180, 140, 60, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
  padding: 5px;
}
#outlineInsertMenu button {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-radius: 3px;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 6px 9px;
  cursor: pointer;
}
#outlineInsertMenu button:hover { background: rgba(180, 140, 60, 0.15); }

#outlineAddCharacterBtn {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.35);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.65);
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: normal;
  padding: 4px 9px;
  cursor: pointer;
}
#outlineAddCharacterBtn:hover { color: rgba(220, 190, 110, 0.9); border-color: rgba(180, 140, 60, 0.55); }

/* tag picker / chips for the current tags plus a "+" that opens the menu to add more.
   generic: any future entity plus xIds tag category reuses this unchanged. */
.outline-tag-picker { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.outline-tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: rgba(180, 140, 60, 0.12);
  border: 1px solid rgba(180, 140, 60, 0.35);
  border-radius: 10px;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 10.5px;
  padding: 2px 4px 2px 8px;
}
.outline-tag-chip-remove {
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.6);
  font-size: 11px;
  line-height: 1;
  padding: 0 3px;
  cursor: pointer;
}
.outline-tag-chip-remove:hover { color: rgba(240, 200, 180, 0.95); }
.outline-tag-add {
  width: 16px;
  height: 16px;
  line-height: 14px;
  border-radius: 50%;
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.4);
  color: rgba(200, 165, 80, 0.75);
  font-size: 11px;
  text-align: center;
  padding: 0;
  cursor: pointer;
}
.outline-tag-add:hover { border-style: solid; color: rgba(220, 190, 110, 0.9); }
#outlineTagMenu {
  position: fixed;
  z-index: 3010;
  min-width: 160px;
  max-width: 240px;
  background: #1a1611;
  border: 1px solid rgba(180, 140, 60, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5);
  padding: 5px;
  font-family: Consolas, monospace;
  font-size: 12px;
}
#outlineTagMenuList { display: flex; flex-direction: column; gap: 2px; max-height: 160px; overflow-y: auto; }
.outline-tag-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-radius: 3px;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 6px 8px;
  cursor: pointer;
}
.outline-tag-menu-item:hover { background: rgba(180, 140, 60, 0.15); }
#outlineTagMenuNewBtn {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-top: 1px solid rgba(180, 140, 60, 0.2);
  margin-top: 3px;
  padding-top: 6px;
  color: rgba(200, 165, 80, 0.8);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding-left: 8px;
  padding-bottom: 4px;
  cursor: pointer;
}
#outlineTagMenuNewBtn:hover { color: rgba(220, 190, 110, 1); }

/* add/edit character dialog / a lightweight floating panel, not a second
   createFadeModal(): Skeleton is already the one open modal, and this repo's convention is
   exactly one at a time. */
#outlineCharacterDialog {
  position: fixed;
  z-index: 3010;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 260px;
  background: #1a1611;
  border: 1px solid rgba(180, 140, 60, 0.45);
  border-radius: 6px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.6);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
#outlineCharacterDialogTitle {
  color: rgba(220, 190, 110, 0.9);
  font-family: Consolas, monospace;
  font-size: 13px;
  font-weight: bold;
  margin-bottom: 2px;
}
#outlineCharacterNameInput, #outlineCharacterAliasInput {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(180, 140, 60, 0.3);
  border-radius: 3px;
  color: rgba(240, 224, 208, 0.9);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 6px 8px;
}
#outlineCharacterAliasChips { display: flex; flex-wrap: wrap; gap: 4px; min-height: 4px; }
#outlineCharacterDialogActions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
#outlineCharacterCancelBtn, #outlineCharacterSaveBtn {
  background: transparent;
  border: 1px solid rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 5px 12px;
  cursor: pointer;
}
#outlineCharacterSaveBtn { background: rgba(180, 140, 60, 0.18); }
#outlineCharacterCancelBtn:hover, #outlineCharacterSaveBtn:hover { background: rgba(180, 140, 60, 0.28); }

/* beat status, tagging, and confirm checkmark */
.outline-beat-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 4px 0; }
.outline-beat-text-wrap { flex: 1; min-width: 0; }
/* positioned against the whole card, not the textarea: on the textarea's own corner it
     sat right on top of the native resize grip. */
.outline-beat-confirm {
  position: absolute;
  right: 6px;
  bottom: 6px;
  width: 18px;
  height: 18px;
  line-height: 16px;
  border-radius: 50%;
  background: rgba(180, 30, 30, 0.15);
  border: 1px solid rgba(180, 30, 30, 0.5);
  color: rgba(220, 140, 120, 0.9);
  font-size: 11px;
  text-align: center;
  padding: 0;
  cursor: pointer;
}
.outline-beat-confirm:hover { background: rgba(180, 30, 30, 0.3); }
#outlineCloseBtn {
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
  margin-left: auto;
}
#outlineCloseBtn:hover { color: rgba(220, 190, 110, 0.9); }

#outlineTreeScroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* generous bottom padding: a long draft's Skeleton is always worked at the bottom of this
       list, and without room to scroll the last rows up away from the panel's bottom edge,
       any menu they open has nowhere to land but off it. paired with clampMenuToViewport(),
       which catches whatever this padding doesn't. */
  padding: 6px 0 22vh 0;
  /* the same look as #editorScroll's, which relies on the bare global scrollbar rule near
       the top of this file. scoped explicitly here at the same values so this panel's
       scrollbar is guaranteed rather than assumed via inheritance, and given the Firefox
       equivalent the bare global rule never had. */
  scrollbar-width: thin;
  scrollbar-color: #6b1a1a #1a1a1a;
}
#outlineTreeScroll::-webkit-scrollbar { width: 8px; }
#outlineTreeScroll::-webkit-scrollbar-track { background: #1a1a1a; }
#outlineTreeScroll::-webkit-scrollbar-thumb { background: #6b1a1a; border-radius: 4px; }
#outlineEmptyState {
  padding: 30px 18px;
  text-align: center;
  color: rgba(160, 130, 60, 0.5);
  font-size: 13px;
}

.outline-scene { border-bottom: 1px solid rgba(180, 140, 60, 0.10); }
.outline-scene[data-level="chapter"] > .outline-scene-header .outline-scene-prefix,
.outline-scene[data-level="chapter"] > .outline-scene-header .outline-scene-title-input { font-weight: bold; }
.outline-scene-header {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.78);
  font-family: Consolas, monospace;
  font-size: 15px;
  text-align: left;
  padding: 9px 14px;
  cursor: pointer;
}
.outline-scene-header:hover { background: rgba(180, 140, 60, 0.08); }
.outline-scene-prefix { flex-shrink: 0; opacity: 0.7; }
/* an <input>, not a <span>: see buildSceneRow()'s comment for why the header had to become
   a plain div to host it. styled to blend in like a label until focused, the same treatment
   as .outline-beat-text below. */
.outline-scene-title-input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  outline: none;
  color: inherit;
  font: inherit;
  padding: 0;
  /* a text input forces its own text-caret cursor regardless of the header's cursor:pointer,
       which read as "you're doing something wrong" while hovering a COLLAPSED row, where
       clicking opens the row rather than editing. match the cursor to what a click actually
       does: pointer while collapsed, text once expanded and a real edit target. */
  cursor: pointer;
}
.outline-scene.expanded .outline-scene-title-input { cursor: text; }
.outline-scene-title-input:focus { text-decoration: underline; text-decoration-color: rgba(180, 140, 60, 0.5); }
.outline-scene-title-input::placeholder { color: rgba(160, 130, 60, 0.4); }
.outline-status-pill {
  flex-shrink: 0;
  font-size: 10px;
  letter-spacing: 0.5px;
  padding: 2px 7px;
  border-radius: 3px;
  color: rgba(220, 190, 110, 0.6);
  background: rgba(180, 140, 60, 0.10);
  border: 1px solid rgba(180, 140, 60, 0.25);
}
.outline-status-pill[data-status="drafted"] { color: rgba(120, 170, 220, 0.85); border-color: rgba(120, 170, 220, 0.35); background: rgba(120, 170, 220, 0.10); }
.outline-status-pill[data-status="needs-revision"] { color: rgba(220, 140, 60, 0.9); border-color: rgba(220, 140, 60, 0.4); background: rgba(220, 140, 60, 0.12); }
.outline-status-pill[data-status="done"] { color: rgba(120, 200, 120, 0.9); border-color: rgba(120, 200, 120, 0.4); background: rgba(120, 200, 120, 0.12); }
/* beat status: raw falls back to the base pill styling, beaten reuses the same green as
   done and fixed. */
.outline-status-pill[data-status="beaten"] { color: rgba(120, 200, 120, 0.9); border-color: rgba(120, 200, 120, 0.4); background: rgba(120, 200, 120, 0.12); }
/* Skill Check's review list reuses this same pill class rather than defining its own,
   just three more data-status variants. */
.outline-status-pill[data-status="fixed"] { color: rgba(120, 200, 120, 0.9); border-color: rgba(120, 200, 120, 0.4); background: rgba(120, 200, 120, 0.12); }
.outline-status-pill[data-status="skipped"] { color: rgba(160, 130, 60, 0.7); border-color: rgba(180, 140, 60, 0.3); background: rgba(180, 140, 60, 0.08); }
.outline-status-pill[data-status="missed"] { color: rgba(220, 90, 70, 0.9); border-color: rgba(220, 90, 70, 0.4); background: rgba(220, 90, 70, 0.12); }
.outline-status-pill[data-status="ignored"] { color: rgba(120, 170, 220, 0.85); border-color: rgba(120, 170, 220, 0.35); background: rgba(120, 170, 220, 0.10); }
.outline-status-pill[data-status="unreached"] { color: rgba(140, 120, 100, 0.55); border-color: rgba(140, 120, 100, 0.25); background: rgba(140, 120, 100, 0.06); }
.outline-word-count {
  flex-shrink: 0;
  font-size: 11px;
  color: rgba(160, 130, 60, 0.55);
}
/* chapter and scene reorder, the same stacked treatment as the beat move group below,
   scoped to the row header instead of a beat card's gutter. */
.outline-scene-move-group {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex-shrink: 0;
}
.outline-scene-move {
  background: transparent;
  border: none;
  color: rgba(160, 130, 60, 0.55);
  font-size: 9px;
  line-height: 1;
  cursor: pointer;
  padding: 1px 4px;
}
.outline-scene-move:hover:not(:disabled) { color: rgba(220, 190, 110, 0.9); }
.outline-scene-move:disabled { color: rgba(140, 110, 60, 0.18); cursor: default; }

/* drag handle, outline-drag.js's long-press target, with markup and styling shared
   between scene rows and beat rows. touch-action:none stops the browser's own touch-scroll
   gesture from competing with a long-press-then-drag on a touchscreen. */
.outline-drag-handle {
  flex-shrink: 0;
  cursor: grab;
  color: rgba(160, 130, 60, 0.4);
  font-size: 13px;
  touch-action: none;
  user-select: none;
}
.outline-drag-handle:hover { color: rgba(220, 190, 110, 0.7); }
#outlineTree.bones-mode .outline-drag-handle { display: none; }

/* the floating clone that follows the pointer during a drag, positioned at the pointer via
   left and top set directly in JS, the same measure-then-draw technique the highlight
   layers already use. */
.outline-drag-ghost {
  position: fixed;
  top: 0; left: 0;
  transform: translate(-12px, -12px);
  pointer-events: none;
  z-index: 3100;
  opacity: 0.85;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  border-radius: 4px;
  background: #1a1408;
}
/* the clone drags its own full subtree along, but only the collapsed header is meaningful
   to show while it follows the pointer. */
.outline-drag-ghost .outline-scene-body,
.outline-drag-ghost .outline-beat-meta { display: none; }

.outline-drag-indicator {
  position: fixed;
  height: 2px;
  background: rgba(220, 190, 110, 0.9);
  z-index: 3100;
  pointer-events: none;
}

#outlineTree.outline-drag-active { cursor: grabbing; }

.outline-scene-body {
  display: none;
  flex-direction: column;
  gap: 8px;
  padding: 4px 14px 14px 14px;
}
.outline-scene.expanded > .outline-scene-body { display: flex; }

.outline-meta-row { display: flex; gap: 8px; }
/* status picker / replaces the old native <select>. a native dropdown's highlighted row is
   controlled by the browser and OS and is not reliably overridable with CSS, so its default
   couldn't be made to match the app's palette.
   this is a plain button that opens a fully custom popover, the same floating-menu
   mechanism as the tag, Part, and insert menus, so every color in it is ours. */
.outline-status-picker {
  font-family: Consolas, monospace;
  cursor: pointer;
  transition: box-shadow 0.15s;
}
.outline-status-picker:hover, .outline-status-picker:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px rgba(220, 70, 60, 0.6), 0 0 8px rgba(220, 70, 60, 0.45);
}
#outlineStatusMenu {
  position: fixed;
  z-index: 3010;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 130px;
  background: #1a1611;
  border: 1px solid rgba(220, 70, 60, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5), 0 0 14px rgba(220, 70, 60, 0.12);
  padding: 5px;
}
.outline-status-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  font-family: Consolas, monospace;
  font-size: 11.5px;
  cursor: pointer;
  transition: box-shadow 0.1s, background 0.1s;
}
/* the glowing highlight the whole thing was built for: a saturated red glow on hover and
   keyboard focus, replacing the browser's native blue. */
.outline-status-menu-item:hover, .outline-status-menu-item:focus-visible {
  outline: none;
  background: rgba(220, 70, 60, 0.18);
  box-shadow: 0 0 0 1px rgba(220, 70, 60, 0.65), 0 0 10px rgba(220, 70, 60, 0.5);
}

.outline-beats { display: flex; flex-direction: column; gap: 8px; }
.outline-beat {
  position: relative;
  background: rgba(180, 140, 60, 0.04);
  border: 1px solid rgba(180, 140, 60, 0.14);
  border-radius: 3px;
  transition: border-color 0.15s, padding 0.15s;
  /* extra right and bottom room keeps the confirm checkmark, in the bottom-right corner of
       the whole card, from sitting flush against the meta row's own content. */
  padding: 7px 26px 24px 9px;
}
/* confirmed means done, visually: the outline disappears via border-color rather than
   `border: none`, keeping the same 1px box so nothing shifts, along with the checkmark
   that is no longer there, so the card reads as complete rather than mid-edit.
   editing it again brings both back. outlined is editing, not outlined is done. */
.outline-beat.outline-beat-confirmed { padding: 7px 9px; border-color: transparent; }
.outline-beat-header { display: flex; align-items: flex-start; gap: 6px; }
/* the wrap is the actual flex item here, not the textarea directly: `flex: 1` on the
   textarea would be a no-op since its parent isn't a flex container, so it needs its own
   width:100% to fill the wrap instead. */
.outline-beat-text {
  display: block;
  width: 100%;
  box-sizing: border-box;
  background: transparent;
  border: none;
  outline: none;
  color: rgba(220, 190, 110, 0.78);
  font-family: Consolas, monospace;
  font-size: 14.5px;
  line-height: 1.4;
  resize: vertical;
  min-height: 20px;
  padding: 0;
}
.outline-beat-text:focus-visible { box-shadow: 0 0 0 1px rgba(220, 190, 110, 0.5); border-radius: 2px; }
.outline-beat-text::placeholder { color: rgba(160, 130, 60, 0.32); }
.outline-beat-remove, .outline-link-remove {
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: rgba(160, 130, 60, 0.5);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}
.outline-beat-remove:hover, .outline-link-remove:hover { color: rgba(220, 80, 60, 0.85); }

/* beat reordering, stacked rather than side-by-side so the pair fits the same slim gutter
   as the remove button without widening the row. */
.outline-beat-move-group {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex-shrink: 0;
}
.outline-beat-move {
  background: transparent;
  border: none;
  color: rgba(160, 130, 60, 0.55);
  font-size: 9px;
  line-height: 1;
  cursor: pointer;
  padding: 1px 4px;
}
.outline-beat-move:hover:not(:disabled) { color: rgba(220, 190, 110, 0.9); }
.outline-beat-move:disabled { color: rgba(140, 110, 60, 0.18); cursor: default; }

.outline-attach-here {
  display: block;
  width: 100%;
  margin-top: 6px;
  background: rgba(180, 30, 30, 0.35);
  border: 1px solid rgba(180, 30, 30, 0.5);
  border-radius: 3px;
  color: rgba(240, 224, 208, 0.95);
  font-family: Consolas, monospace;
  font-size: 11.5px;
  text-align: left;
  padding: 6px 9px;
  cursor: pointer;
}
.outline-attach-here:hover { background: rgba(180, 30, 30, 0.55); }

.outline-links { display: flex; flex-direction: column; gap: 5px; margin-top: 6px; }
.outline-link-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  background: rgba(180, 140, 60, 0.06);
  border-left: 2px solid rgba(180, 140, 60, 0.4);
  border-radius: 2px;
  padding: 4px 8px;
  font-size: 11.5px;
}
.outline-link-chip.removed {
  border-left-color: rgba(160, 80, 60, 0.4);
  font-style: italic;
  opacity: 0.6;
}
.outline-link-excerpt {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: rgba(220, 190, 110, 0.65);
}
/* bones view / a skeleton pass over the same tree: every scene body is forced open, but
   only the structural spine shows. everything that is detail or attached material is
   hidden, so the manuscript's shape reads at a glance.
   purely a display filter toggled by a class on #outlineTree. the DOM is unchanged, so
   flipping it back off restores the full editable outline instantly. */
#outlineTree.bones-mode .outline-scene-body { display: flex; }
#outlineTree.bones-mode .outline-meta-row,
#outlineTree.bones-mode .outline-beat-meta,
#outlineTree.bones-mode .outline-beat-confirm,
#outlineTree.bones-mode .outline-divider,
#outlineTree.bones-mode .outline-attach-here,
#outlineTree.bones-mode .outline-links,
#outlineTree.bones-mode .outline-beat-remove,
#outlineTree.bones-mode .outline-beat-move-group,
#outlineTree.bones-mode .outline-scene-move-group,
#outlineTree.bones-mode .outline-status-pill,
#outlineTree.bones-mode .outline-word-count { display: none; }
/* tighten the surviving beats into a plain indented list with a marker, instead of the
   boxed, editable-looking rows of the full view. */
#outlineTree.bones-mode .outline-scene-body { gap: 3px; padding-top: 0; }
#outlineTree.bones-mode .outline-beats { gap: 3px; }
#outlineTree.bones-mode .outline-beat {
  background: transparent;
  border: none;
  border-radius: 0;
  padding: 1px 0 1px 14px;
}
#outlineTree.bones-mode .outline-beat-header::before {
  content: "\203A"; /* › */
  flex-shrink: 0;
  color: rgba(180, 140, 60, 0.5);
  margin-right: 2px;
}
#outlineTree.bones-mode .outline-beat-text {
  min-height: 0;
  resize: none;
  line-height: 1.35;
}

/* positioned popup for editorcontextmenu.js's dispatcher, a right-click-on-selection menu
   shared by every feature that registers an action into it.
   one or more item buttons render per open; the notice shows instead when every applicable
   action is blocked. */
#editorContextMenu {
  position: fixed;
  z-index: 3050;
  background: rgba(20, 15, 10, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
  font-family: Consolas, monospace;
  font-size: 12px;
}
.editor-context-menu-item {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  color: rgba(240, 224, 208, 0.95);
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  text-align: left;
}
.editor-context-menu-item:hover { background: rgba(180, 30, 30, 0.35); }
/* visibleWhenBlocked actions render as a real disabled <button> rather than vanishing,
   with the block reason as the tooltip. the :hover override stops the row from looking
   clickable on mouseover. */
.editor-context-menu-item:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.editor-context-menu-item:disabled:hover { background: transparent; }

/* non-icon actions fill a 2-column grid instead of stacking full-width: the icon row above
   already spans the menu's full width, so a single-column list left a lot of unused
   horizontal space.
   one grid per contiguous run between group-change dividers; a run with an odd item count
   spans its last button across both columns instead of leaving an empty cell.
   the even 50/50 split below is only the FALLBACK for a menu with no icon row. whenever an
   icon row IS present, alignGridColumnsToIconRow() overrides this with an inline value
   measured off the icon row's own separator, so the vertical rule lines up with the one
   above instead of independently splitting its own width in half. */
.editor-context-menu-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}
.editor-context-menu-grid .editor-context-menu-item:nth-child(odd):not(.span-full) {
  border-right: 1px solid rgba(180, 30, 30, 0.2);
}
.editor-context-menu-grid .editor-context-menu-item.span-full {
  grid-column: 1 / -1;
}

/* "List" collapses the two list kinds into one button that opens the submenu below rather
   than acting immediately. a pure CSS hint marking which grid buttons behave this way. */
.editor-context-menu-item.has-submenu {
  position: relative;
  padding-right: 24px;
}
.editor-context-menu-item.has-submenu::after {
  content: '\25B8';
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  opacity: 0.6;
}

/* nested flyout for a submenu-bearing action, the same chrome as #editorContextMenu
   itself, positioned off the clicked button rather than the original click point, with the
   same viewport clamp. a DOM child of the outer menu, so its outside-click handling already
   covers this. */
#editorContextSubmenu {
  position: fixed;
  z-index: 3060;
  background: rgba(20, 15, 10, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.4);
  border-radius: 4px;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
  font-family: Consolas, monospace;
  font-size: 12px;
  min-width: 140px;
}

/* icon-only actions share one horizontal row instead of stacking as full-width text rows,
   reusing the toolbar's own pixel sprites: the same icon language the persistent toolbar
   shows, just relocated. */
.editor-context-menu-icon-row {
  display: flex;
  align-items: center;
  padding: 8px 10px;
  gap: 10px;
}
.editor-context-menu-icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  background: transparent;
  border: none;
  border-radius: 3px;
  cursor: pointer;
  padding: 0;
}
.editor-context-menu-icon-btn:hover { background: rgba(180, 30, 30, 0.35); }
/* mirrors the toolbar's own .active treatment. the menu reads the same live selection
   state, so Bold shows pressed here exactly when the toolbar button would. */
.editor-context-menu-icon-btn.pressed { background: rgba(180, 30, 30, 0.55); }
.editor-context-menu-icon-btn img {
  image-rendering: pixelated;
  height: 15px;
  width: auto;
}
/* wider multi-letter monograms need a wider hitbox than the single-letter glyphs but stay
   the same height, matching the persistent toolbar's own format icons, which apply one
   fixed height to all six and let width scale with each glyph's aspect ratio.
   main.js opts an action in via `iconWide` rather than this file matching on label text,
   which is display copy free to be renamed independently. */
.editor-context-menu-icon-btn.wide {
  width: 34px;
}
/* divides the inline-formatting icons from the block-type icons within the same row.
   without it the two heading monograms run together illegibly, reading as one smeared
   cluster of letters rather than two distinct controls. */
.editor-context-menu-icon-sep {
  width: 1px;
  align-self: stretch;
  margin: 2px 4px;
  background: rgba(180, 30, 30, 0.3);
}
/* full-width divider between the icon row and the text actions, and between those and any
   feature-specific actions below. editorcontextmenu.js inserts one wherever `group`
   changes. */
.editor-context-menu-hr {
  border: none;
  border-top: 1px solid rgba(180, 30, 30, 0.3);
  margin: 0;
}

#editorContextMenuNotice {
  padding: 8px 12px;
  color: rgba(220, 190, 110, 0.8);
  max-width: 240px;
  white-space: normal;
}

#toolbar, #statusbar {
  transition: opacity 0.7s ease, filter 0.7s ease;
}
.outline-active #toolbar,
.outline-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

/* skill check / a dice-driven spellcheck review in the same frame-modal language as the
   Skeleton panel above.
   the play and review states are empty containers in index.html: skillcheck.js builds their
   contents the same way outline.js builds its tree, a fresh element tree per render with no
   persistent inner markup to keep in sync here. */
#skillCheckBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.48);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#skillCheckBackdrop.visible { display: block; }
#skillCheckBackdrop.shown { opacity: 1; }
#skillCheckBackdrop.fading { opacity: 0; transition: opacity 2s ease; }

/* #skillCheckPanel's right-docked shape lives in the shared four-panel rule near
   #skullCounterPanel above. */

.skillcheck-active #toolbar,
.skillcheck-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

#skillCheckHeader {
  flex-shrink: 0;
  padding: 10px 14px;
  color: rgba(200, 165, 80, 0.75);
  font-size: 15px;
  letter-spacing: 1.5px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
#skillCheckCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}
#skillCheckCloseBtn:hover { color: rgba(220, 190, 110, 0.9); }

#skillCheckBodyScroll {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}
#skillCheckEmptyState {
  padding: 30px 18px;
  text-align: center;
  color: rgba(160, 130, 60, 0.5);
  font-size: 15px;
}

/* roll-for-steps stage, shown between turns while items remain unresolved. */
.skillcheck-progress {
  font-size: 14px;
  color: rgba(200, 165, 80, 0.7);
  margin-bottom: 12px;
  text-align: center;
}
.skillcheck-roll-btn {
  display: block;
  width: 100%;
  background: rgba(180, 140, 60, 0.10);
  border: 1px dashed rgba(180, 140, 60, 0.5);
  border-radius: 4px;
  color: rgba(220, 190, 110, 0.9);
  font-family: Consolas, monospace;
  font-size: 14px;
  letter-spacing: 0.5px;
  padding: 12px;
  cursor: pointer;
}
.skillcheck-roll-btn:hover { background: rgba(180, 140, 60, 0.18); border-color: rgba(180, 140, 60, 0.75); }

/* the optional-powerup recipe menu, offered on both the empty-draft screen and the roll
   stage. framed as a recipe card: a heading over a Name/Effect/Cost table, one row per
   entry, sitting between the progress text and the roll button. */
.skillcheck-elixir-menu {
  margin-bottom: 12px;
  padding: 10px 12px;
  border: 1px dashed rgba(180, 140, 60, 0.35);
  border-radius: 4px;
}
.skillcheck-elixir-heading {
  font-size: 12px;
  letter-spacing: 0.5px;
  color: rgba(220, 190, 110, 0.85);
  margin-bottom: 8px;
  text-align: center;
}
.skillcheck-elixir-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 11.5px;
}
.skillcheck-elixir-table th {
  text-align: left;
  font-weight: normal;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-size: 10px;
  color: rgba(160, 130, 60, 0.6);
  padding: 0 8px 6px 0;
  border-bottom: 1px solid rgba(180, 140, 60, 0.25);
}
.skillcheck-elixir-table td {
  padding: 6px 8px 6px 0;
  color: rgba(200, 165, 80, 0.85);
  border-bottom: 1px solid rgba(180, 140, 60, 0.12);
  vertical-align: middle;
}
.skillcheck-elixir-table tr:last-child td { border-bottom: none; }
.skillcheck-elixir-table td:first-child { color: rgba(220, 190, 110, 0.95); }
.skillcheck-elixir-buy-btn {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.5);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.85);
  font-family: Consolas, monospace;
  font-size: 11px;
  padding: 4px 10px;
  cursor: pointer;
}
.skillcheck-elixir-buy-btn:hover { color: rgba(220, 190, 110, 0.95); border-color: rgba(180, 140, 60, 0.8); }
.skillcheck-elixir-buy-btn:disabled { opacity: 0.4; cursor: default; }

/* per-item stage: the flagged snippet, the roll-vs-difficulty result, and whatever action
   buttons that result unlocks. */
.skillcheck-item-context {
  font-size: 15px;
  line-height: 1.5;
  color: rgba(200, 180, 150, 0.7);
  background: rgba(180, 140, 60, 0.05);
  border-left: 2px solid rgba(180, 140, 60, 0.35);
  border-radius: 2px;
  padding: 10px 12px;
  margin-bottom: 14px;
}
.skillcheck-item-context mark {
  background: rgba(220, 90, 70, 0.25);
  color: rgba(240, 200, 190, 0.95);
  border-radius: 2px;
  padding: 0 1px;
}
.skillcheck-item-label {
  display: block;
  font-size: 12.5px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: rgba(160, 130, 60, 0.55);
  margin-bottom: 10px;
}

.skillcheck-dice-row {
  display: flex;
  justify-content: center;
  gap: 18px;
  margin-bottom: 14px;
}
.skillcheck-die {
  min-width: 64px;
  text-align: center;
  border: 1px solid rgba(180, 140, 60, 0.3);
  border-radius: 4px;
  padding: 8px 10px;
}
.skillcheck-die-label {
  display: block;
  font-size: 10px;
  letter-spacing: 0.5px;
  color: rgba(160, 130, 60, 0.6);
  margin-bottom: 3px;
}
.skillcheck-die-value {
  display: block;
  font-size: 20px;
  color: rgba(220, 190, 110, 0.9);
}
.skillcheck-die.success .skillcheck-die-value { color: rgba(120, 200, 120, 0.95); }
.skillcheck-die.fail .skillcheck-die-value { color: rgba(220, 90, 70, 0.95); }

.skillcheck-outcome {
  text-align: center;
  font-size: 15px;
  margin-bottom: 12px;
}
.skillcheck-outcome.success { color: rgba(120, 200, 120, 0.9); }
.skillcheck-outcome.fail { color: rgba(220, 90, 70, 0.9); }

.skillcheck-suggestion {
  text-align: center;
  font-size: 15px;
  color: rgba(220, 190, 110, 0.85);
  margin-bottom: 14px;
}
.skillcheck-suggestion .arrow { color: rgba(160, 130, 60, 0.6); margin: 0 6px; }

.skillcheck-actions {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
}
.skillcheck-btn {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.75);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 7px 14px;
  cursor: pointer;
}
.skillcheck-btn:hover { color: rgba(220, 190, 110, 0.95); border-color: rgba(180, 140, 60, 0.7); }
.skillcheck-btn-primary {
  background: rgba(120, 200, 120, 0.12);
  border-style: solid;
  border-color: rgba(120, 200, 120, 0.4);
  color: rgba(150, 220, 150, 0.95);
}
.skillcheck-btn-primary:hover { background: rgba(120, 200, 120, 0.2); border-color: rgba(120, 200, 120, 0.6); }
.skillcheck-btn-override {
  border-color: rgba(220, 140, 60, 0.5);
  color: rgba(230, 180, 110, 0.9);
}
.skillcheck-btn-override:hover { background: rgba(220, 140, 60, 0.12); border-color: rgba(220, 140, 60, 0.75); }
.skillcheck-btn:disabled { opacity: 0.4; cursor: default; }

/* "fix it myself", an inline text input swapped in over the normal action row so a
   mis-suggested or missing auto-fix can be corrected by hand without leaving the roll flow.
   offered whenever the item can be auto-fixed, success or fail, with the same
   free-on-success and spend-on-fail gating as the normal buttons. */
.skillcheck-custom-fix { margin-bottom: 4px; }
.skillcheck-custom-fix-input {
  display: block;
  width: 100%;
  box-sizing: border-box;
  background: rgba(180, 140, 60, 0.06);
  border: 1px solid rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(230, 210, 170, 0.95);
  font-family: Consolas, monospace;
  font-size: 13px;
  padding: 7px 10px;
  margin-bottom: 10px;
}
.skillcheck-custom-fix-input:focus { outline: none; border-color: rgba(220, 190, 110, 0.8); }

/* footer help icon and its popup, pinned outside the scrollable body so it stays reachable
   in every stage. the same toggle and outside-click mechanism as outline.js's popovers,
   anchored to a fixed corner instead of a clicked row. */
#skillCheckFooter {
  flex-shrink: 0;
  padding: 8px 14px;
  border-top: 1px solid rgba(180, 140, 60, 0.20);
  display: flex;
  justify-content: flex-end;
}
#skillCheckHelpBtn {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: transparent;
  border: 1px solid rgba(180, 140, 60, 0.45);
  color: rgba(200, 165, 80, 0.7);
  font-family: Consolas, monospace;
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
#skillCheckHelpBtn:hover, #skillCheckHelpBtn[aria-expanded="true"] {
  color: rgba(220, 190, 110, 0.95);
  border-color: rgba(180, 140, 60, 0.8);
}
#skillCheckHelpPopup {
  position: fixed;
  z-index: 3001;
  width: min(300px, 80vw);
  max-height: calc(100vh - 16px);
  overflow-y: auto;
  background: rgba(8, 5, 18, 0.97);
  border: 1px solid rgba(180, 140, 60, 0.45);
  border-radius: 5px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.5);
  padding: 12px 14px;
  font-family: Consolas, monospace;
  font-size: 12px;
  line-height: 1.5;
  color: rgba(210, 190, 150, 0.9);
}
#skillCheckHelpPopup ul { margin: 0; padding-left: 16px; }
#skillCheckHelpPopup li { margin-bottom: 8px; }
#skillCheckHelpPopup li:last-child { margin-bottom: 0; }

/* search and destroy / the same fade lifecycle and language as Skill Check above, but a
   much smaller panel pinned near the top: a find bar, not a full-screen review, since it
   must stay out of the way of the manuscript text it is mutating in place. */
#findReplaceBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.24);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#findReplaceBackdrop.visible { display: block; }
#findReplaceBackdrop.shown { opacity: 1; }
#findReplaceBackdrop.fading { opacity: 0; transition: opacity 0.6s ease; }

/* flush against the viewport's right edge, like the other right-docked panels, rather than
   floating in with a right margin, which read as a leftover gap next to panels that all sit
   flush.
   width is capped to whatever is left beside the never-shifting centered canvas: the canvas
   right edge rests at 80vw, so 20vw is free, and 16px comes off that for a gap, so the
   panel's left edge never crosses the canvas boundary. */
#findReplacePanel {
  position: fixed;
  top: 64px;
  right: 0;
  width: min(320px, calc(20vw - 16px), 90vw);
  max-height: calc(100vh - 96px);
  overflow-y: auto;
  background: rgba(8, 5, 18, 0.96);
  border: 1px solid rgba(180, 140, 60, 0.35);
  border-radius: 5px;
  box-shadow: 0 0 22px rgba(180, 140, 60, 0.16), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3000;
  font-family: Consolas, monospace;
  display: none;
  flex-direction: column;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#findReplacePanel.visible { display: flex; }
#findReplacePanel.shown { opacity: 1; }
#findReplacePanel.fading { opacity: 0; transition: opacity 0.6s ease; pointer-events: none; }

/* below this breakpoint the canvas is flush-left and full-width, so there is no side gap
   to fit into. fall back to the plain viewport-relative cap the desktop rule above only
   uses as its own fallback. */
@media (max-width: 480px), (orientation: landscape) and (max-height: 480px) and (max-width: 930px) {
  #findReplacePanel { width: min(320px, 90vw); }
}

.findreplace-active #toolbar,
.findreplace-active #statusbar {
  opacity: 0.55;
}

#findReplaceHeader {
  flex-shrink: 0;
  padding: 8px 12px;
  color: rgba(200, 165, 80, 0.75);
  font-size: 11px;
  letter-spacing: 1.2px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
#findReplaceCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-size: 16px;
  cursor: pointer;
  padding: 0 4px;
}
#findReplaceCloseBtn:hover { color: rgba(220, 190, 110, 0.9); }

#findReplaceBody {
  padding: 10px 12px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.findReplace-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
#findReplaceSearchInput,
#findReplaceReplaceInput {
  flex: 1;
  background: rgba(180, 140, 60, 0.06);
  border: 1px solid rgba(180, 140, 60, 0.3);
  border-radius: 3px;
  color: rgba(230, 210, 170, 0.95);
  font-family: Consolas, monospace;
  font-size: 13px;
  padding: 6px 8px;
}
#findReplaceSearchInput:focus,
#findReplaceReplaceInput:focus {
  outline: none;
  border-color: rgba(180, 140, 60, 0.7);
}
#findReplaceCaseBtn {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.6);
  font-size: 11px;
  padding: 5px 8px;
  cursor: pointer;
}
#findReplaceCaseBtn[aria-pressed="true"] {
  background: rgba(180, 140, 60, 0.18);
  border-style: solid;
  color: rgba(220, 190, 110, 0.95);
}
.findReplace-actions {
  justify-content: flex-end;
  flex-wrap: wrap;
}
#findReplaceCount {
  margin-right: auto;
  font-size: 11px;
  color: rgba(160, 130, 60, 0.6);
}
.findReplace-actions button {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.75);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 5px 10px;
  cursor: pointer;
}
.findReplace-actions button:hover:not(:disabled) { color: rgba(220, 190, 110, 0.95); border-color: rgba(180, 140, 60, 0.7); }
.findReplace-actions button:disabled { opacity: 0.35; cursor: default; }

/* history / the same frame-modal language and right-docked sizing formula as Search and
   Destroy and Charms, kept as its own block rather than joining their selectors since its
   accent and content differ. */
#historyBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.24);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#historyBackdrop.visible { display: block; }
#historyBackdrop.shown { opacity: 1; }
#historyBackdrop.fading { opacity: 0; transition: opacity 0.6s ease; }

/* flush against the viewport's right edge, see #findReplacePanel above for why and for the
   width math. */
#historyPanel {
  position: fixed;
  top: 64px;
  right: 0;
  width: min(340px, calc(20vw - 16px), 90vw);
  max-height: calc(100vh - 96px);
  background: rgba(8, 5, 18, 0.96);
  border: 1px solid rgba(180, 140, 60, 0.35);
  border-radius: 5px;
  box-shadow: 0 0 22px rgba(180, 140, 60, 0.16), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3000;
  font-family: Consolas, monospace;
  display: none;
  flex-direction: column;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#historyPanel.visible { display: flex; }
#historyPanel.shown { opacity: 1; }
#historyPanel.fading { opacity: 0; transition: opacity 0.6s ease; pointer-events: none; }

/* below this breakpoint the canvas is flush-left and full-width, so there is no side gap
   to fit into. fall back to the plain viewport-relative cap. */
@media (max-width: 480px), (orientation: landscape) and (max-height: 480px) and (max-width: 930px) {
  #historyPanel { width: min(340px, 90vw); max-height: 70vh; }
}

.history-active #toolbar,
.history-active #statusbar {
  opacity: 0.55;
}

#historyHeader {
  flex-shrink: 0;
  padding: 8px 12px;
  color: rgba(200, 165, 80, 0.75);
  font-size: 11px;
  letter-spacing: 1.2px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
#historyCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-size: 16px;
  cursor: pointer;
  padding: 0 4px;
}
#historyCloseBtn:hover { color: rgba(220, 190, 110, 0.9); }

#historyList {
  overflow-y: auto;
  padding: 8px 0;
  min-height: 0;
}
/* the same row recipe as the Tomes list: a plain button per row, since each has only the
   one action. */
.history-item {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid rgba(180, 140, 60, 0.12);
  color: rgba(230, 210, 170, 0.95);
  padding: 10px 14px;
  text-align: left;
  font-family: Consolas, monospace;
  font-size: 13px;
  cursor: pointer;
}
.history-item:hover { background: rgba(180, 140, 60, 0.10); }
.history-item-sub {
  display: block;
  color: rgba(200, 165, 80, 0.6);
  font-size: 11px;
  margin-top: 2px;
}
.history-status {
  color: rgba(200, 165, 80, 0.6);
  padding: 20px 14px;
  font-size: 13px;
  text-align: center;
}

/* charms create modal / the same frame-modal language as History above, centered rather
   than corner-docked since this is a short-lived form to fill and dismiss, not a persistent
   browsing list. */
#charmCreateBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.24);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#charmCreateBackdrop.visible { display: block; }
#charmCreateBackdrop.shown { opacity: 1; }
#charmCreateBackdrop.fading { opacity: 0; transition: opacity 0.6s ease; }

#charmCreatePanel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(320px, 90vw);
  background: rgba(8, 5, 18, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.35);
  border-radius: 5px;
  box-shadow: 0 0 22px rgba(180, 30, 30, 0.16), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3000;
  font-family: Consolas, monospace;
  display: none;
  flex-direction: column;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#charmCreatePanel.visible { display: flex; }
#charmCreatePanel.shown { opacity: 1; }
#charmCreatePanel.fading { opacity: 0; transition: opacity 0.6s ease; pointer-events: none; }

.charm-create-active #toolbar,
.charm-create-active #statusbar {
  opacity: 0.55;
}

#charmCreateHeader {
  flex-shrink: 0;
  padding: 8px 12px;
  color: rgba(220, 190, 110, 0.75);
  font-size: 11px;
  letter-spacing: 1.2px;
  border-bottom: 1px solid rgba(180, 30, 30, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
#charmCreateCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.55);
  font-size: 16px;
  cursor: pointer;
  padding: 0 4px;
}
#charmCreateCloseBtn:hover { color: rgba(240, 224, 208, 0.9); }

/* exactly the 12 stock icons, an unchanged 6-column grid so it never looks unevenly
   filled: an odd tile count in a fixed-column grid leaves a visible gap in the last row.
   the custom tiles live in their own row instead of a 13th cell. */
#charmIconGrid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 4px;
  padding: 10px 12px 0;
}
.charm-icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid rgba(180, 30, 30, 0.2);
  border-radius: 4px;
  font-size: 18px;
  padding: 6px 0;
  cursor: pointer;
}
.charm-icon-btn:hover { background: rgba(180, 30, 30, 0.15); }
.charm-icon-btn.selected {
  border-color: rgba(220, 60, 60, 0.85);
  background: rgba(180, 30, 30, 0.32);
}
/* the custom-icon tiles, plus any already-drawn pending preview. the same tile chrome as
   the stock buttons above, with a dashed border so the draw and paste actions read as
   distinct from picking a fixed icon, and a fixed width since they are flex items here, not
   grid cells. once a custom icon is pending this tile instead holds a small canvas. */
.charm-icon-btn-custom {
  border-style: dashed;
  font-size: 15px;
  width: 46px;
  flex-shrink: 0;
}
.charm-icon-btn canvas { display: block; }

/* deliberately NOT part of #charmIconGrid's 6-column grid: a 13th or 14th cell would
   sometimes leave the grid's last row visibly short one slot. flex plus its own padding
   echoes the grid's spacing without sharing its column math. */
#charmCustomIconRow {
  display: flex;
  gap: 4px;
  padding: 0 12px 10px;
}

/* a <textarea> is a replaced element for CSS sizing, so even at display:block, width:auto
   still sizes it from its `cols` attribute rather than filling the container the way an
   ordinary block would. display:block alone does NOT fix this; it needs an explicit width.
   calc() subtracts this rule's own horizontal margin so the result lines up with the icon
   grid's side padding above and below, instead of overflowing past the panel's edge the way
   a bare width:100% plus margin would. */
#charmTextInput {
  display: block;
  width: calc(100% - 24px);
  margin: 0 12px 10px;
  padding: 8px;
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(180, 30, 30, 0.25);
  border-radius: 4px;
  color: rgba(240, 224, 208, 0.95);
  font-family: Consolas, monospace;
  font-size: 12px;
  resize: vertical;
  min-height: 60px;
}

#charmCreateActions {
  padding: 0 12px 12px;
  display: flex;
  justify-content: flex-end;
}
#charmSaveBtn {
  background: rgba(180, 30, 30, 0.4);
  border: 1px solid rgba(220, 60, 60, 0.5);
  border-radius: 4px;
  color: rgba(240, 224, 208, 0.95);
  padding: 6px 16px;
  cursor: pointer;
  font-family: Consolas, monospace;
  font-size: 12px;
}
#charmSaveBtn:hover { background: rgba(180, 30, 30, 0.6); }

/* charms pixel-art icon editor / a second state INSIDE the same #charmCreatePanel dialog,
   js-hidden-toggled, not a second createFadeModal(), per the one-modal-at-a-time rule.
   a fixed 16x16 grid at 16px cells plus 1px gaps, comfortably inside the panel's own width
   with its side padding. */
#charmCreatePixelEditorState {
  padding: 10px 12px 12px;
}
#charmPixelPalette {
  display: flex;
  gap: 6px;
  margin-bottom: 10px;
}
.charm-pixel-swatch {
  width: 24px;
  height: 24px;
  border: 1px solid rgba(180, 30, 30, 0.3);
  border-radius: 3px;
  padding: 0;
  cursor: pointer;
}
.charm-pixel-swatch.selected {
  box-shadow: inset 0 0 0 2px rgba(240, 224, 208, 0.9);
}
/* the eraser slot has no color of its own to show, so a small checkerboard stands in for
   it, the same idea as any image editor's transparency swatch. */
.charm-pixel-swatch-eraser {
  background-image:
    linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.15) 75%),
    linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.15) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
  background-color: rgba(0, 0, 0, 0.3);
}
#charmPixelGrid {
  display: grid;
  grid-template-columns: repeat(16, 16px);
  grid-template-rows: repeat(16, 16px);
  gap: 1px;
  /* the grid container's own background shows through each 1px gap between cells, giving a
       faint grid line grid-wide. cheaper than a border on every one of the 256 cells, which
       would also eat into each cell's fillable area under border-box sizing. */
  background: rgba(255, 255, 255, 0.15);
  width: fit-content;
  /* dragging across cells to paint shouldn't also scroll the page on touch devices. */
  touch-action: none;
  user-select: none;
}
.charm-pixel-cell {
  width: 16px;
  height: 16px;
  padding: 0;
  border: none;
  background: rgba(0, 0, 0, 0.3);
  cursor: pointer;
}
#charmPixelEditorActions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 12px;
}
#charmPixelEditorActions button {
  background: rgba(180, 30, 30, 0.4);
  border: 1px solid rgba(220, 60, 60, 0.5);
  border-radius: 4px;
  color: rgba(240, 224, 208, 0.95);
  padding: 6px 14px;
  cursor: pointer;
  font-family: Consolas, monospace;
  font-size: 12px;
}
#charmPixelEditorActions button:hover { background: rgba(180, 30, 30, 0.6); }
#charmPixelClearBtn, #charmPixelCancelBtn {
  background: transparent;
  color: rgba(220, 190, 110, 0.65);
}
#charmPixelClearBtn:hover, #charmPixelCancelBtn:hover {
  background: rgba(180, 30, 30, 0.15);
}

/* powerup unlocked mini-modal / the same centered chrome as #charmCreatePanel above, with
   two deliberate deviations required by it popping up ON TOP of the already-open
   #codexPanel, which must stay visibly open behind it:
     - z-index sits ABOVE every other modal's shared tier rather than inside it. a named,
       one-off exception.
     - the backdrop is fully transparent, no dim and no blur: it exists only to catch a
       click-outside-to-dismiss, never to darken Codex a second time on top of its own
       backdrop. */
#powerupUnlockedBackdrop {
  position: fixed;
  inset: 0;
  background: transparent;
  z-index: 3100;
  display: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#powerupUnlockedBackdrop.visible { display: block; }
#powerupUnlockedBackdrop.shown { opacity: 1; }
#powerupUnlockedBackdrop.fading { opacity: 0; transition: opacity 0.6s ease; }

#powerupUnlockedPanel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(340px, 90vw);
  background: rgba(8, 5, 18, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.35);
  border-radius: 5px;
  box-shadow: 0 0 22px rgba(180, 30, 30, 0.16), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3101;
  font-family: Consolas, monospace;
  display: none;
  flex-direction: column;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#powerupUnlockedPanel.visible { display: flex; }
#powerupUnlockedPanel.shown { opacity: 1; }
#powerupUnlockedPanel.fading { opacity: 0; transition: opacity 0.6s ease; pointer-events: none; }

#powerupUnlockedHeader {
  flex-shrink: 0;
  padding: 10px 12px;
  color: rgba(220, 190, 110, 0.75);
  border-bottom: 1px solid rgba(180, 30, 30, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
/* larger than the small-caps label size every other modal's header uses: unlike those,
   this header's own text IS the modal's headline content, not a label sitting above a
   separate body, so it reads as a heading rather than a tab. */
#powerupUnlockedTitle {
  font-size: 15px;
  font-weight: bold;
  letter-spacing: normal;
}
#powerupUnlockedCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.55);
  font-size: 16px;
  cursor: pointer;
  padding: 0 4px;
}
#powerupUnlockedCloseBtn:hover { color: rgba(240, 224, 208, 0.9); }

#powerupUnlockedDescription {
  margin: 0;
  padding: 10px 12px;
  color: rgba(240, 224, 208, 0.9);
  font-size: 12px;
  line-height: 1.5;
}

#powerupUnlockedActions {
  padding: 0 12px 12px;
  display: flex;
  justify-content: flex-end;
}
#powerupUnlockedActionBtn {
  background: rgba(180, 30, 30, 0.4);
  border: 1px solid rgba(220, 60, 60, 0.5);
  border-radius: 4px;
  color: rgba(240, 224, 208, 0.95);
  padding: 6px 16px;
  cursor: pointer;
  font-family: Consolas, monospace;
  font-size: 12px;
}
#powerupUnlockedActionBtn:hover { background: rgba(180, 30, 30, 0.6); }

/* insert table modal / the same centered frame-modal chrome as #charmCreatePanel above,
   the one difference being the pixelated grid picker in place of Charms' icon grid.
   no border-radius anywhere in this block, and hard flat-color cell fills with no
   gradients, are deliberate: that flat, square-edged look is the pixelated read here, not
   an actual bitmap asset. */
#tableInsertBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.24);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#tableInsertBackdrop.visible { display: block; }
#tableInsertBackdrop.shown { opacity: 1; }
#tableInsertBackdrop.fading { opacity: 0; transition: opacity 0.6s ease; }

#tableInsertPanel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(300px, 90vw);
  background: rgba(8, 5, 18, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.35);
  border-radius: 5px;
  box-shadow: 0 0 22px rgba(180, 30, 30, 0.16), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3000;
  font-family: Consolas, monospace;
  display: none;
  flex-direction: column;
  align-items: center;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#tableInsertPanel.visible { display: flex; }
#tableInsertPanel.shown { opacity: 1; }
#tableInsertPanel.fading { opacity: 0; transition: opacity 0.6s ease; pointer-events: none; }

.table-insert-active #toolbar,
.table-insert-active #statusbar {
  opacity: 0.55;
}

#tableInsertHeader {
  align-self: stretch;
  flex-shrink: 0;
  padding: 8px 12px;
  color: rgba(220, 190, 110, 0.75);
  font-size: 11px;
  letter-spacing: 1.2px;
  border-bottom: 1px solid rgba(180, 30, 30, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
#tableInsertCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.55);
  font-size: 16px;
  cursor: pointer;
  padding: 0 4px;
}
#tableInsertCloseBtn:hover { color: rgba(240, 224, 208, 0.9); }

#tableInsertGrid {
  display: grid;
  grid-template-columns: repeat(8, 18px);
  grid-template-rows: repeat(8, 18px);
  gap: 2px;
  padding: 14px;
}
.table-insert-cell {
  width: 18px;
  height: 18px;
  padding: 0;
  background: rgba(180, 30, 30, 0.08);
  border: 1px solid rgba(180, 30, 30, 0.3);
  border-radius: 0;
  cursor: pointer;
}
.table-insert-cell.active {
  background: rgba(200, 40, 40, 0.85);
  border-color: rgba(255, 90, 90, 0.95);
}

#tableInsertLabel {
  padding: 0 14px 14px;
  color: rgba(240, 224, 208, 0.85);
  font-size: 12px;
  letter-spacing: 0.5px;
  min-height: 1.4em;
}

/* charms list panel / the same corner-docked frame-modal language as #historyPanel above,
   own red accent matching the create modal and context menu rather than History's gold. */
#charmsListBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.24);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#charmsListBackdrop.visible { display: block; }
#charmsListBackdrop.shown { opacity: 1; }
#charmsListBackdrop.fading { opacity: 0; transition: opacity 0.6s ease; }

/* flush against the viewport's right edge, see #findReplacePanel above for why and for the
   width math. */
#charmsListPanel {
  position: fixed;
  top: 64px;
  right: 0;
  width: min(340px, calc(20vw - 16px), 90vw);
  max-height: calc(100vh - 96px);
  background: rgba(8, 5, 18, 0.96);
  border: 1px solid rgba(180, 30, 30, 0.35);
  border-radius: 5px;
  box-shadow: 0 0 22px rgba(180, 30, 30, 0.16), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3000;
  font-family: Consolas, monospace;
  display: none;
  flex-direction: column;
  opacity: 0;
  transition: opacity 0.4s ease;
}
#charmsListPanel.visible { display: flex; }
#charmsListPanel.shown { opacity: 1; }
#charmsListPanel.fading { opacity: 0; transition: opacity 0.6s ease; pointer-events: none; }

/* below this breakpoint the canvas is flush-left and full-width, so there is no side gap
   to fit into. fall back to the plain viewport-relative cap. */
@media (max-width: 480px), (orientation: landscape) and (max-height: 480px) and (max-width: 930px) {
  #charmsListPanel { width: min(340px, 90vw); max-height: 70vh; }
}

.charms-list-active #toolbar,
.charms-list-active #statusbar {
  opacity: 0.55;
}

#charmsListHeader {
  flex-shrink: 0;
  padding: 8px 12px;
  color: rgba(220, 190, 110, 0.75);
  font-size: 11px;
  letter-spacing: 1.2px;
  border-bottom: 1px solid rgba(180, 30, 30, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
#charmsListCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.55);
  font-size: 16px;
  cursor: pointer;
  padding: 0 4px;
}
#charmsListCloseBtn:hover { color: rgba(240, 224, 208, 0.9); }

#charmsList {
  overflow-y: auto;
  padding: 8px 0;
  min-height: 0;
}
.charm-list-item {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  border-bottom: 1px solid rgba(180, 30, 30, 0.12);
  padding: 10px 14px;
}
/* the row whose Charm is currently highlighted in the draft, auto-set to the nearest one
   on open or whichever row was last clicked. */
.charm-list-item.selected {
  background: rgba(180, 30, 30, 0.14);
  border-left: 2px solid rgba(220, 60, 60, 0.75);
  padding-left: 12px;
}
.charm-list-item-icon {
  font-size: 16px;
  flex-shrink: 0;
  line-height: 1.4;
}
.charm-list-item-icon canvas { display: block; }
.charm-list-item-body {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  color: rgba(230, 210, 170, 0.95);
  text-align: left;
  font-family: Consolas, monospace;
  font-size: 13px;
  cursor: pointer;
  padding: 0;
}
.charm-list-item-body:hover { color: rgba(255, 235, 200, 1); }
.charm-list-item-body.orphaned {
  color: rgba(200, 165, 80, 0.55);
  cursor: default;
  font-style: italic;
}
.charm-list-item-sub {
  display: block;
  color: rgba(200, 165, 80, 0.6);
  font-size: 11px;
  margin-top: 2px;
}
/* only rendered for a row whose charm has a customIcon. copies it into the same in-memory
   clipboard the gutter popover's own copy button feeds. */
.charm-list-item-copy {
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.4);
  cursor: pointer;
  font-size: 14px;
  padding: 0 2px;
}
.charm-list-item-copy:hover { color: rgba(220, 190, 110, 0.75); }
.charm-list-item-delete {
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: rgba(220, 190, 110, 0.4);
  cursor: pointer;
  font-size: 14px;
  padding: 0 2px;
}
.charm-list-item-delete:hover { color: rgba(220, 60, 60, 0.9); }
.charms-list-status {
  color: rgba(200, 165, 80, 0.6);
  padding: 20px 14px;
  font-size: 13px;
  text-align: center;
}

/* turn-based mode / the same frame-modal language as Skill Check above, own ids and
   classes throughout. */
#turnModeBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.48);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  z-index: 2999;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#turnModeBackdrop.visible { display: block; }
#turnModeBackdrop.shown { opacity: 1; }
#turnModeBackdrop.fading { opacity: 0; transition: opacity 2s ease; }

#turnModePanel {
  position: fixed;
  top: 8%;
  left: 50%;
  transform: translateX(-50%);
  width: min(640px, 90vw);
  max-height: 85vh;
  background: rgba(8, 5, 18, 0.94);
  border: 1px solid rgba(180, 140, 60, 0.35);
  border-radius: 5px;
  box-shadow: 0 0 22px rgba(180, 140, 60, 0.16), 0 4px 28px rgba(0,0,0,0.55);
  z-index: 3000;
  font-family: Consolas, monospace;
  display: none;
  flex-direction: column;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#turnModePanel.visible { display: flex; }
#turnModePanel.shown { opacity: 1; }
#turnModePanel.fading { opacity: 0; transition: opacity 2s ease; pointer-events: none; }

.turnmode-active #toolbar,
.turnmode-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

#turnModeHeader {
  flex-shrink: 0;
  padding: 10px 14px;
  color: rgba(200, 165, 80, 0.75);
  font-size: 12px;
  letter-spacing: 1.5px;
  border-bottom: 1px solid rgba(180, 140, 60, 0.20);
  display: flex;
  align-items: center;
  gap: 12px;
  user-select: none;
}
#turnModeCloseBtn {
  margin-left: auto;
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.55);
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}
#turnModeCloseBtn:hover { color: rgba(220, 190, 110, 0.9); }

#turnModeBodyScroll {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}

.turnmode-progress {
  font-size: 12px;
  color: rgba(200, 165, 80, 0.7);
  margin-bottom: 12px;
  text-align: center;
}

.turnmode-roster-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.turnmode-roster-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: rgba(220, 190, 110, 0.85);
  cursor: pointer;
}
.turnmode-roster-add { display: flex; gap: 8px; margin-bottom: 14px; }
.turnmode-roster-add-input {
  flex: 1;
  background: rgba(180, 140, 60, 0.05);
  border: 1px solid rgba(180, 140, 60, 0.25);
  border-radius: 3px;
  outline: none;
  color: rgba(220, 190, 110, 0.85);
  font-family: Consolas, monospace;
  font-size: 12.5px;
  padding: 6px 8px;
}
.turnmode-roster-add-input:focus-visible { border-color: rgba(220, 190, 110, 0.7); }

.turnmode-action-picker {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin-bottom: 14px;
}

.turnmode-dice-row {
  display: flex;
  justify-content: center;
  gap: 18px;
  margin-bottom: 14px;
}
.turnmode-die {
  min-width: 64px;
  text-align: center;
  border: 1px solid rgba(180, 140, 60, 0.3);
  border-radius: 4px;
  padding: 8px 10px;
}
.turnmode-die-label {
  display: block;
  font-size: 10px;
  letter-spacing: 0.5px;
  color: rgba(160, 130, 60, 0.6);
  margin-bottom: 3px;
}
.turnmode-die-value {
  display: block;
  font-size: 20px;
  color: rgba(220, 190, 110, 0.9);
}
.turnmode-die.success .turnmode-die-value { color: rgba(120, 200, 120, 0.95); }
.turnmode-die.fail .turnmode-die-value { color: rgba(220, 90, 70, 0.95); }

.turnmode-outcome {
  text-align: center;
  font-size: 13px;
  margin-bottom: 12px;
}
.turnmode-outcome.success { color: rgba(120, 200, 120, 0.9); }
.turnmode-outcome.fail { color: rgba(220, 90, 70, 0.9); }

.turnmode-beat-text {
  display: block;
  width: 100%;
  min-height: 90px;
  resize: vertical;
  background: rgba(180, 140, 60, 0.05);
  border: 1px solid rgba(180, 140, 60, 0.25);
  border-radius: 3px;
  outline: none;
  color: rgba(230, 210, 180, 0.92);
  font-family: Consolas, monospace;
  font-size: 13px;
  line-height: 1.5;
  padding: 8px 10px;
  margin-bottom: 12px;
}
.turnmode-beat-text:focus-visible { border-color: rgba(230, 210, 180, 0.7); }

.turnmode-actions {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
}
.turnmode-btn {
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.4);
  border-radius: 3px;
  color: rgba(200, 165, 80, 0.75);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 7px 14px;
  cursor: pointer;
}
.turnmode-btn:hover { color: rgba(220, 190, 110, 0.95); border-color: rgba(180, 140, 60, 0.7); }
.turnmode-btn-primary {
  background: rgba(120, 200, 120, 0.12);
  border-style: solid;
  border-color: rgba(120, 200, 120, 0.4);
  color: rgba(150, 220, 150, 0.95);
}
.turnmode-btn-primary:hover { background: rgba(120, 200, 120, 0.2); border-color: rgba(120, 200, 120, 0.6); }
.turnmode-btn-override {
  border-color: rgba(220, 140, 60, 0.5);
  color: rgba(230, 180, 110, 0.9);
}
.turnmode-btn-override:hover { background: rgba(220, 140, 60, 0.12); border-color: rgba(220, 140, 60, 0.75); }
.turnmode-btn-skip {
  border-color: rgba(160, 130, 60, 0.35);
  color: rgba(180, 150, 100, 0.7);
}
.turnmode-btn-skip:hover { border-color: rgba(160, 130, 60, 0.6); color: rgba(200, 170, 120, 0.9); }
.turnmode-btn:disabled { opacity: 0.4; cursor: default; }

/* review state: the curated, clickable result list */
.skillcheck-review-intro {
  font-size: 12.5px;
  color: rgba(200, 165, 80, 0.7);
  text-align: center;
  margin-bottom: 14px;
}
.skillcheck-review-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.skillcheck-review-row {
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(180, 140, 60, 0.04);
  border: 1px solid rgba(180, 140, 60, 0.14);
  border-radius: 3px;
  padding: 8px 10px;
}
.skillcheck-review-icon { flex-shrink: 0; font-size: 13px; }
.skillcheck-review-excerpt {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  color: rgba(220, 190, 110, 0.7);
}
.skillcheck-review-jump {
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: rgba(200, 165, 80, 0.75);
  font-size: 11px;
  text-decoration: underline;
  cursor: pointer;
  padding: 0;
}
.skillcheck-review-jump:hover { color: rgba(220, 190, 110, 1); }
.skillcheck-review-close {
  display: block;
  width: 100%;
  background: transparent;
  border: 1px dashed rgba(180, 140, 60, 0.4);
  border-radius: 4px;
  color: rgba(200, 165, 80, 0.75);
  font-family: Consolas, monospace;
  font-size: 12.5px;
  padding: 10px;
  cursor: pointer;
}
.skillcheck-review-close:hover { color: rgba(220, 190, 110, 0.95); border-color: rgba(180, 140, 60, 0.7); }

/* welcome, first-time visitors */
#welcomeBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  z-index: 3100;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#welcomeBackdrop.visible { display: block; }
#welcomeBackdrop.shown { opacity: 1; }
#welcomeBackdrop.fading { opacity: 0; transition: opacity 1.2s ease; }

#welcomePanel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 380px;
  max-width: calc(100vw - 36px);
  /* classic MS-DOS terminal chrome */
  --term-fg: #33ff33;
  background: #000;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(255,255,255,0.025) 0px,
    rgba(255,255,255,0.025) 1px,
    transparent 1px,
    transparent 3px
  );
  border: 1px solid rgba(51, 255, 51, 0.45);
  border-radius: 2px;
  box-shadow: 0 0 22px rgba(51, 255, 51, 0.15), 0 6px 32px rgba(0,0,0,0.7);
  z-index: 3101;
  font-family: Consolas, monospace;
  padding: 22px 24px 20px 24px;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#welcomePanel.visible { display: block; }
#welcomePanel.shown { opacity: 1; }
#welcomePanel.fading { opacity: 0; transition: opacity 1.2s ease; pointer-events: none; }

/* text-adventure engine / generic .adventure-* classes: any panel hosting a
   branching-narrative node reuses this same terminal-style reveal and choice-button styling
   rather than redefining it per panel.
   color comes from --term-fg on the host panel, so a differently-themed adventure elsewhere
   just sets that one variable. */
.adventure-text {
  position: relative;
  min-height: 78px;
  margin-bottom: 18px;
}
.adventure-line {
  position: absolute;
  top: 0; left: 0; right: 0;
  color: var(--term-fg, #33ff33);
  font-size: 15px;
  line-height: 1.7;
  margin: 0;
  text-shadow: 0 0 6px rgba(51, 255, 51, 0.5);
  opacity: 0;
  transition: opacity 0.9s ease;
}
.adventure-line.shown {
  opacity: 1;
}
.adventure-cursor {
  display: inline-block;
  width: 0.55em;
  height: 1.05em;
  margin-left: 2px;
  background: currentColor;
  vertical-align: text-bottom;
  animation: adventureCursorBlink 1.06s step-start infinite;
}
@keyframes adventureCursorBlink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}
.adventure-choices {
  display: flex;
  flex-direction: column;
}
.adventure-choice {
  display: block;
  width: 100%;
  background: #000;
  color: var(--term-fg, #33ff33);
  border: 1px solid rgba(51, 255, 51, 0.45);
  border-radius: 2px;
  padding: 11px 14px;
  font-family: Consolas, monospace;
  font-size: 15px;
  font-weight: bold;
  letter-spacing: 1px;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.adventure-choice::before { content: "> "; }
.adventure-choice + .adventure-choice { margin-top: 10px; }
.adventure-choice:hover {
  background: var(--term-fg, #33ff33);
  color: #000;
}

.welcome-active #toolbar,
.welcome-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

@media (max-width: 480px) {
  #welcomePanel { width: auto; left: 18px; right: 18px; transform: translateY(-50%); }
}

/* beware, first editable document */
#bewareBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  z-index: 3100;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#bewareBackdrop.visible { display: block; }
#bewareBackdrop.shown { opacity: 1; }
#bewareBackdrop.fading { opacity: 0; transition: opacity 1.2s ease; }

#bewarePanel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 380px;
  max-width: calc(100vw - 36px);
  background: rgba(18, 6, 6, 0.95);
  border: 1px solid rgba(180, 30, 30, 0.55);
  border-radius: 6px;
  box-shadow: 0 0 26px rgba(180, 20, 10, 0.28), 0 6px 32px rgba(0,0,0,0.65);
  z-index: 3101;
  font-family: Consolas, monospace;
  padding: 22px 24px 20px 24px;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#bewarePanel.visible { display: block; }
#bewarePanel.shown { opacity: 1; }
#bewarePanel.fading { opacity: 0; transition: opacity 1.2s ease; pointer-events: none; }

#bewareTitle {
  color: #ff2a18;
  font-size: 26px;
  font-weight: bold;
  letter-spacing: 3px;
  text-align: center;
  margin: 0 0 16px 0;
  text-shadow: 0 0 10px rgba(255, 40, 10, 0.8), 0 0 22px rgba(180, 0, 0, 0.5);
}
.beware-body {
  color: rgba(225, 180, 170, 0.85);
  font-size: 15px;
  line-height: 1.7;
  margin: 0 0 16px 0;
}
#bewareAck {
  display: block;
  width: 100%;
  background: rgba(180, 30, 30, 0.22);
  color: #f0b0a0;
  border: 1px solid rgba(180, 30, 30, 0.6);
  border-radius: 4px;
  padding: 11px 14px;
  font-family: Consolas, monospace;
  font-size: 15px;
  font-weight: bold;
  letter-spacing: 1px;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
#bewareAck:hover {
  background: rgba(180, 30, 30, 0.4);
  color: #ffe0d0;
  border-color: rgba(255, 50, 30, 0.85);
}

.beware-active #toolbar,
.beware-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

@media (max-width: 480px) {
  #bewarePanel { width: auto; left: 18px; right: 18px; transform: translateY(-50%); }
  #bewareTitle { font-size: 22px; }
}

/* skull collector warning, the same red-alert visual language as #beware above, under its
   own ids and appClass since the two can in principle both exist. */
#skullWarningBackdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  z-index: 3100;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#skullWarningBackdrop.visible { display: block; }
#skullWarningBackdrop.shown { opacity: 1; }
#skullWarningBackdrop.fading { opacity: 0; transition: opacity 1.2s ease; }

#skullWarningPanel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 380px;
  max-width: calc(100vw - 36px);
  background: rgba(18, 6, 6, 0.95);
  border-radius: 6px;
  box-shadow: 0 0 26px rgba(180, 20, 10, 0.28), 0 6px 32px rgba(0,0,0,0.65);
  z-index: 3101;
  font-family: Consolas, monospace;
  padding: 22px 24px 20px 24px;
  display: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}
#skullWarningPanel.visible { display: block; }
#skullWarningPanel.shown { opacity: 1; }
#skullWarningPanel.fading { opacity: 0; transition: opacity 1.2s ease; pointer-events: none; }

/* the same red arcade-flash treatment as #needSkullsFlash above. it restarts automatically
   every time the panel goes from display:none back to visible, since a keyframe animation
   on a non-rendered element simply doesn't run. no JS restart trick needed. */
#skullWarningTitle {
  color: #ff3020;
  font-size: 26px;
  font-weight: bold;
  letter-spacing: 3px;
  text-align: center;
  text-transform: uppercase;
  margin: 0 0 16px 0;
  text-shadow: 0 0 6px rgba(255,40,10,0.8), 0 0 14px rgba(180,0,0,0.5);
  animation: needSkullsBlink 0.3s steps(1) 6;
}
.skull-warning-body {
  color: rgba(225, 180, 170, 0.85);
  font-size: 15px;
  line-height: 1.7;
  margin: 0 0 16px 0;
}

.skull-warning-active #toolbar,
.skull-warning-active #statusbar {
  opacity: 0.25;
  filter: blur(1.5px);
}

@media (max-width: 480px) {
  #skullWarningPanel { width: auto; left: 18px; right: 18px; transform: translateY(-50%); }
  #skullWarningTitle { font-size: 22px; }
}

/* generic tool and game modal, hosting a page in an iframe. above all other overlays. */
#frameModalBackdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.66);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  display: none; z-index: 4000;
}
#frameModalBackdrop.visible { display: block; }
#frameModal {
  position: fixed; z-index: 4001; display: none;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(900px, 94vw); height: min(680px, 90vh);
  /* frosted, semi-transparent panel: the blurred page shows through it and the soft glow
       lets the edges feather into the backdrop rather than reading as a hard solid box. the
       iframe below is transparent so this shows through. */
  background: rgba(18, 16, 22, 0.55);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 0 70px rgba(0,0,0,0.55), 0 0 34px rgba(176,32,16,0.08);
  overflow: hidden;
  /* slices a small 45° triangle out of each corner. a four-layer linear-gradient mask, one
       quadrant per corner, clips the panel and the iframe inside it, so the cut corners are
       fully transparent to whatever is behind, as if the panel material simply isn't there.
       triangles need linear, not radial, gradients: radial would round the cut into a scoop. */
  --cut: 18px;
  -webkit-mask:
    linear-gradient(135deg, #0000 var(--cut), #000 var(--cut)) top left,
    linear-gradient(225deg, #0000 var(--cut), #000 var(--cut)) top right,
    linear-gradient(45deg,  #0000 var(--cut), #000 var(--cut)) bottom left,
    linear-gradient(315deg, #0000 var(--cut), #000 var(--cut)) bottom right;
  -webkit-mask-size: 50% 50%;
  -webkit-mask-repeat: no-repeat;
  mask:
    linear-gradient(135deg, #0000 var(--cut), #000 var(--cut)) top left,
    linear-gradient(225deg, #0000 var(--cut), #000 var(--cut)) top right,
    linear-gradient(45deg,  #0000 var(--cut), #000 var(--cut)) bottom left,
    linear-gradient(315deg, #0000 var(--cut), #000 var(--cut)) bottom right;
  mask-size: 50% 50%;
  mask-repeat: no-repeat;
}
#frameModal.visible { display: block; }
#frameModalFrame { width: 100%; height: 100%; border: 0; display: block; background: transparent; }

/* Skullmage tool: an 80s terminal look. a sharp rectangle with no cut corners, plus a
   pixelated glitching red border drawn on its own canvas. border cells are 1-deep with a
   few px of jitter on top, so the padding below keeps clear of it. */
#frameModal.crt {
  mask: none;
  -webkit-mask: none;
  background: rgba(10, 6, 6, 0.75);
  /* keep the iframe content clear of the border blocks and their jitter. the border canvas
       stays full-box regardless of this padding. */
  padding: 13px;
  /* Skullmage's internal layout wraps below a certain content width, its fixed-pixel grid
       and preview column only just fitting the old unpadded iframe. widen by the padding this
       rule adds plus the same buffer the original sizing kept, so the border doesn't force a
       wrap. */
  width: min(938px, 96vw);
  height: min(706px, 94vh);
}
#frameModalBorder {
  display: none;
  position: absolute; inset: 0;
  pointer-events: none;
  image-rendering: pixelated;
}

/* reading view and PDF export / the one real typographic move: every piece of CHROME below
   stays in Consolas, the app's one typeface everywhere else, while ONLY the reading column,
   shared with PDF export, switches to a serif stack.
   --reading-body-size and --reading-title-size are set from JS per the chosen preset, so
   the chapter rules just reference the custom properties rather than hardcoding a size. */

:root {
  --reading-body-size: 19px;
  --reading-title-size: 27px;
}

#readingViewBackdrop {
  position: fixed; inset: 0;
  background: #000;
  z-index: 2999;
  display: none;
  opacity: 0;
}
#readingViewBackdrop.visible { display: block; }
#readingViewBackdrop.shown { opacity: 0.6; }
#readingViewBackdrop.fading { opacity: 0; transition: opacity 2s ease; }

#readingView {
  position: fixed; inset: 0;
  z-index: 3000;
  display: none;
  opacity: 0;
  background: #1a1a1a;
  flex-direction: column;
}
#readingView.visible { display: flex; }
#readingView.shown { opacity: 1; }
#readingView.fading { opacity: 0; transition: opacity 2s ease; pointer-events: none; }

#readingTopbar {
  flex: 0 0 auto;
  height: 52px;
  background: #0d0d0d;
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 0 18px;
  border-bottom: 1px solid rgba(200, 184, 152, 0.14);
}
#readingBrand { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
#readingBrand canvas { image-rendering: pixelated; display: block; }
#readingBrand span {
  color: #b02010;
  font-family: Consolas, monospace;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 3px;
}
/* the open document's title, sitting between the mark and the chapter counter. the one
   topbar label whose length is unbounded, being a writer's own title rather than app copy,
   so it is the flex item that absorbs available space and the one that must never wrap: a
   two-line topbar would push the counter and exit button around on every open. */
#readingDocTitle {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-family: Consolas, monospace;
  font-size: 13px;
  letter-spacing: 0.5px;
  color: rgba(200, 184, 152, 0.7);
}
#readingTopbarRight { flex: 0 0 auto; display: flex; align-items: center; gap: 18px; }
#readingPosition {
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: 1.5px;
  color: rgba(200, 184, 152, 0.5);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
#readingExitBtn {
  background: transparent;
  border: 1px solid rgba(200, 184, 152, 0.14);
  color: rgba(200, 184, 152, 0.5);
  font-family: Consolas, monospace;
  font-size: 16px;
  line-height: 1;
  width: 30px;
  height: 30px;
  border-radius: 3px;
  cursor: pointer;
}
#readingExitBtn:hover, #readingExitBtn:focus-visible { color: #ff3820; border-color: #ff3820; }

#readingScrollHost {
  flex: 1 1 auto;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: rgba(200, 184, 152, 0.14) transparent;
}
#readingScrollHost::-webkit-scrollbar { width: 9px; }
#readingScrollHost::-webkit-scrollbar-track { background: transparent; }
#readingScrollHost::-webkit-scrollbar-thumb { background: rgba(200, 184, 152, 0.14); border-radius: 5px; }

#readingColumn {
  max-width: 66ch;
  margin: 0 auto;
  padding: 64px 24px 120px;
}
@media (max-width: 700px) {
  #readingColumn { padding: 40px 16px 100px; }
  /* narrow screens: the wordmark is app-chrome, not reading content, so drop it and keep
       the mark, giving the one unbounded-length label as much room as possible before it
       truncates. */
  #readingTopbar { padding: 0 12px; gap: 10px; }
  #readingBrand span { display: none; }
  #readingDocTitle { font-size: 12px; }
  #readingTopbarRight { gap: 10px; }
}

#readingBottombar {
  flex: 0 0 auto;
  height: 56px;
  background: #0d0d0d;
  border-top: 1px solid rgba(200, 184, 152, 0.14);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  position: relative;
}
.reading-navbtn {
  background: transparent;
  border: none;
  color: rgba(200, 184, 152, 0.5);
  font-family: Consolas, monospace;
  font-size: 13px;
  letter-spacing: 1px;
  cursor: pointer;
  padding: 8px 10px;
}
.reading-navbtn:hover:not(:disabled), .reading-navbtn:focus-visible:not(:disabled) { color: #ff3820; }
.reading-navbtn:focus-visible { outline: 2px solid #ff3820; outline-offset: 2px; }
.reading-navbtn:disabled { opacity: 0.25; cursor: default; }
#readingChaptersBtn {
  color: #c8b898;
  border: 1px solid rgba(200, 184, 152, 0.14);
  border-radius: 3px;
  padding: 8px 16px;
}
#readingChaptersBtn:hover, #readingChaptersBtn:focus-visible { border-color: #ff3820; color: #ff3820; }

/* font-size preset menu, a small popover anchored above its button. */
#readingFontSizeWrap { position: relative; }
#readingFontSizeMenu {
  position: absolute;
  bottom: 100%; left: 50%;
  transform: translateX(-50%);
  margin-bottom: 8px;
  background: #0d0d0d;
  border: 1px solid rgba(200, 184, 152, 0.2);
  border-radius: 4px;
  display: flex;
  gap: 2px;
  padding: 4px;
}
#readingFontSizeMenu button {
  background: transparent;
  border: none;
  color: rgba(200, 184, 152, 0.6);
  font-family: Consolas, monospace;
  font-size: 12px;
  padding: 6px 10px;
  border-radius: 3px;
  cursor: pointer;
}
#readingFontSizeMenu button:hover, #readingFontSizeMenu button:focus-visible { color: #d4c4a8; background: rgba(200, 184, 152, 0.08); }
#readingFontSizeMenu button.current { color: #b02010; }

/* chapters slide-over */
#readingChaptersScrim {
  position: fixed; inset: 0;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0; pointer-events: none;
  transition: opacity 0.18s ease;
  z-index: 3001;
}
#readingChaptersScrim.open { opacity: 1; pointer-events: auto; }
#readingChaptersPanel {
  position: fixed; top: 0; right: 0; bottom: 0;
  width: min(320px, 84vw);
  background: #0d0d0d;
  border-left: 1px solid rgba(200, 184, 152, 0.14);
  transform: translateX(100%);
  transition: transform 0.22s ease;
  z-index: 3002;
  display: flex;
  flex-direction: column;
}
#readingChaptersPanel.open { transform: translateX(0); }
#readingChaptersPanel h2 {
  font-family: Consolas, monospace;
  font-size: 12px;
  letter-spacing: 2px;
  color: rgba(200, 184, 152, 0.5);
  text-transform: uppercase;
  margin: 22px 20px 12px;
}
#readingChaptersList { list-style: none; margin: 0; padding: 0 8px 20px; overflow-y: auto; }
#readingChaptersList li { margin: 2px 0; }
#readingChaptersList button {
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  color: rgba(200, 184, 152, 0.6);
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  font-size: 15px;
  padding: 10px 12px;
  border-radius: 3px;
  cursor: pointer;
  display: block;
}
#readingChaptersList button:hover, #readingChaptersList button:focus-visible { background: rgba(200, 184, 152, 0.06); color: #c8b898; }
#readingChaptersList button:focus-visible { outline: 2px solid #ff3820; outline-offset: -2px; }
#readingChaptersList button.current { color: #b02010; }
/* the title page is front matter, not a chapter: set in the topbar's label register rather
   than the chapter list's serif, and given its own bottom rule so it reads as a heading for
   the list below it. */
#readingChaptersList button.reading-chapters-titlepage {
  font-family: Consolas, monospace;
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: rgba(200, 184, 152, 0.4);
  border-bottom: 1px solid rgba(200, 184, 152, 0.1);
  border-radius: 0;
  margin-bottom: 4px;
}
#readingChaptersList button.reading-chapters-titlepage:hover,
#readingChaptersList button.reading-chapters-titlepage:focus-visible { color: rgba(200, 184, 152, 0.7); }
#readingChaptersList button.reading-chapters-titlepage.current { color: #b02010; }

/* the shared chapter fragment, reused verbatim by the reading host, one at a time, and the
   PDF export root, every chapter concatenated. */
.manuscript-chapter-head { text-align: center; margin-bottom: 8px; }
.manuscript-chapter-head h1 {
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  color: #b02010;
  font-size: var(--reading-title-size);
  font-weight: 400;
  letter-spacing: 4px;
  text-transform: uppercase;
  text-wrap: balance;
  margin: 0;
}
.manuscript-chapter-ornament {
  display: flex;
  justify-content: center;
  margin: 22px 0 40px;
}
.manuscript-chapter-ornament canvas { image-rendering: pixelated; opacity: 0.85; }

/* title page / the front matter every reading view opens on. unlike the topbar's doc title,
   which must stay a single line, this is display text: centered and free to wrap across as
   many lines as a long title needs, the way a real book cover would set it. */
.manuscript-title-page {
  /* vertically centers within the space between the fixed-height topbar and bottombar. a
       percentage height wouldn't resolve, since the column's own height is content-sized. */
  min-height: calc(100vh - 108px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 10vh 0;
}
.manuscript-title-page-title {
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  color: #b02010;
  font-size: calc(var(--reading-title-size) * 1.6);
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 4px;
  text-transform: uppercase;
  text-wrap: balance;
  margin: 0;
}
.manuscript-title-page-author {
  margin-top: 32px;
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  font-size: calc(var(--reading-body-size) * 1.05);
  font-style: italic;
  color: rgba(200, 184, 152, 0.6);
  letter-spacing: 1px;
}

.manuscript-prose {
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
}
.manuscript-prose p, .manuscript-prose div {
  font-size: var(--reading-body-size);
  line-height: 1.75;
  margin: 0 0 1.35em;
  color: #c8b898;
  /* the first paragraph's drop cap floats past its own paragraph's box. without this, a
       short first paragraph lets the very next one wrap up alongside the float instead of
       starting cleanly beneath it. */
  clear: both;
}
/* scene headings always render, verbatim, whatever they say. a quieter subheading than the
   chapter title: no red, no letter-spacing, and smaller than body text is loud. */
.manuscript-prose h2 {
  font-size: calc(var(--reading-body-size) * 0.95);
  font-weight: 400;
  font-style: italic;
  text-align: center;
  color: rgba(200, 184, 152, 0.6);
  margin: 1.6em 0 1.2em;
}
.manuscript-prose h1 {
  /* a stray H1 landing inside a chapter body shouldn't happen, since h1 is the split point,
       but stay defensive rather than let one balloon to the title's own size if it ever does. */
  font-size: var(--reading-body-size);
  color: #c8b898;
  margin: 0 0 1.35em;
}
.manuscript-prose p:first-of-type::first-letter {
  font-size: 3.4em;
  line-height: 0.78;
  float: left;
  padding: 0.04em 0.09em 0 0;
  color: #b02010;
}

.resume-marker {
  padding-left: 16px;
  border-left: 2px solid #ff3820;
}
/* a real block, not an absolutely-positioned pseudo-element pulled upward over whatever
   precedes it: that could land on top of the first paragraph's drop-cap float. taking real
   layout space means it can never overlap anything above it, whatever that is. */
.resume-marker-label {
  clear: both;
  font-family: Consolas, monospace;
  font-size: 10.5px;
  letter-spacing: 1px;
  color: #ff3820;
  opacity: 0.85;
  margin: 0 0 0.4em 16px;
}

/* pdf export */
#pdfExportRoot { display: none; }
.pdf-cover {
  text-align: center;
  padding-top: 30vh;
}
.pdf-cover-title {
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  color: #b02010;
  font-size: 34px;
  letter-spacing: 3px;
  text-transform: uppercase;
}
.pdf-cover-byline {
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  color: #c8b898;
  font-size: 16px;
  margin-top: 14px;
}

@media print {
  /* browsers strip background colors from printed output by default, an ink-saving default
       the user can override in their print dialog. without this, the dark background this
       whole feature is built around silently disappears and prints white.
       print-color-adjust is the standard property; the prefixed form is still needed for
       older WebKit engines. */
  html {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  /* show only the PDF export root: everything else the app renders lives as a sibling
       top-level element under <body>, so hiding every OTHER top-level child hides all of it
       in one rule regardless of z-index or position. */
  body > *:not(#pdfExportRoot) { display: none !important; }
  #pdfExportRoot {
    display: block;
    background: #1a1a1a;
    padding: 0 8mm;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .pdf-cover { break-after: page; }
  .manuscript-chapter-head h1 { font-size: 26px; }
  .manuscript-prose p, .manuscript-prose div { font-size: 13px; line-height: 1.6; }
  /* printing a dark full-bleed background on every physical page burns a LOT of ink. an
       accepted, deliberate tradeoff, this being a personal reading copy rather than a
       print-friendly submission format. */
  @page { margin: 12mm; }
}
