/* ============================================================================
   ui.css — shared chrome for every panel, button, figure, tooltip and dialog.
   ROOT-OWNED (app_shell).  Feature CSS lives in css/<feature>.css and is loaded
   by that feature with useStylesheet() from src/ui/registry.js.

   Contents
     1. reset + the field           5. figures, labels, meters
     2. layout docks                6. tooltip (delegated, data-tip)
     3. panel chrome                7. the layers over the map
     4. controls                    8. utilities
   ========================================================================= */

/* -- 1. reset + the field -------------------------------------------------- */

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

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;                 /* no scrollbars, ever */
  background: var(--void);
}

body {
  font-family: var(--font-body);
  font-size: var(--fs-md);
  line-height: 1.42;
  color: var(--ink-1);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overscroll-behavior: none;
  cursor: default;
}

::selection { background: var(--accent-dim); color: #fff; }

/* The deep field.  Canvas layers (starfield, galaxy) draw on top of this; it
   guarantees an edge-to-edge near-black blue even before any layer exists. */
#field {
  position: fixed;
  inset: 0;
  z-index: var(--z-canvas);
  pointer-events: none;
  background:
    /* galactic core, off-centre */
    radial-gradient(58% 42% at 47% 46%, var(--core-glow) 0%, rgba(0,0,0,0) 70%),
    /* nebula washes */
    radial-gradient(38% 34% at 22% 30%, var(--nebula-indigo) 0%, rgba(0,0,0,0) 72%),
    radial-gradient(44% 30% at 76% 66%, var(--nebula-teal)   0%, rgba(0,0,0,0) 74%),
    radial-gradient(30% 26% at 66% 20%, var(--nebula-violet) 0%, rgba(0,0,0,0) 76%),
    /* the field itself */
    radial-gradient(118% 98% at 50% 42%, var(--deep-2) 0%, var(--deep) 38%, var(--void) 92%);
}

/* Far dust: one large SVG tile of ~190 randomly placed pin-prick stars, so it
   reads as a scatter rather than a lattice. Static and free — the parallax
   starfield is a canvas layer drawn over this by `starfield_backdrop`.
   Regenerate with: node critics/kadry/gen-star-dust.mjs */
#field::before {
  content: "";
  position: absolute;
  inset: -2px;
  background-repeat: repeat;
  background-size: 1100px 1100px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1100' height='1100'%3E%3Ccircle cx='972' cy='513' r='0.55' fill='%23dce8ff' opacity='0.25'/%3E%3Ccircle cx='377' cy='222' r='0.85' fill='%23dce8ff' opacity='0.42'/%3E%3Ccircle cx='946' cy='1042' r='0.55' fill='%23dce8ff' opacity='0.28'/%3E%3Ccircle cx='711' cy='453' r='0.55' fill='%23dce8ff' opacity='0.33'/%3E%3Ccircle cx='597' cy='883' r='0.55' fill='%23c9dcff' opacity='0.28'/%3E%3Ccircle cx='101' cy='388' r='0.85' fill='%23c9dcff' opacity='0.47'/%3E%3Ccircle cx='294' cy='460' r='0.85' fill='%23c9dcff' opacity='0.47'/%3E%3Ccircle cx='589' cy='694' r='0.85' fill='%23c9dcff' opacity='0.50'/%3E%3Ccircle cx='715' cy='7' r='0.55' fill='%23dce8ff' opacity='0.30'/%3E%3Ccircle cx='1067' cy='149' r='0.55' fill='%23dce8ff' opacity='0.28'/%3E%3Ccircle cx='1043' cy='692' r='0.55' fill='%23dce8ff' opacity='0.19'/%3E%3Ccircle cx='352' cy='321' r='0.85' fill='%23dce8ff' opacity='0.61'/%3E%3Ccircle cx='466' cy='739' r='1.3' fill='%23ffe4dc' opacity='0.76'/%3E%3Ccircle cx='853' cy='885' r='0.55' fill='%23c9dcff' opacity='0.38'/%3E%3Ccircle cx='637' cy='444' r='0.55' fill='%23dce8ff' opacity='0.27'/%3E%3Ccircle cx='956' cy='295' r='0.85' fill='%23dce8ff' opacity='0.56'/%3E%3Ccircle cx='804' cy='767' r='0.55' fill='%23dce8ff' opacity='0.40'/%3E%3Ccircle cx='56' cy='416' r='0.55' fill='%23dce8ff' opacity='0.35'/%3E%3Ccircle cx='812' cy='902' r='0.85' fill='%23c9dcff' opacity='0.58'/%3E%3Ccircle cx='270' cy='1066' r='0.85' fill='%23dce8ff' opacity='0.61'/%3E%3Ccircle cx='52' cy='482' r='0.55' fill='%23dce8ff' opacity='0.42'/%3E%3Ccircle cx='633' cy='1057' r='0.55' fill='%23dce8ff' opacity='0.18'/%3E%3Ccircle cx='477' cy='475' r='0.55' fill='%23dce8ff' opacity='0.31'/%3E%3Ccircle cx='84' cy='275' r='0.55' fill='%23dce8ff' opacity='0.21'/%3E%3Ccircle cx='71' cy='326' r='0.55' fill='%23dce8ff' opacity='0.23'/%3E%3Ccircle cx='969' cy='85' r='0.55' fill='%23dce8ff' opacity='0.28'/%3E%3Ccircle cx='756' cy='723' r='1.3' fill='%23ffe4dc' opacity='0.75'/%3E%3Ccircle cx='863' cy='553' r='0.55' fill='%23dce8ff' opacity='0.20'/%3E%3Ccircle cx='733' cy='115' r='0.55' fill='%23dce8ff' opacity='0.40'/%3E%3Ccircle cx='134' cy='503' r='0.55' fill='%23dce8ff' opacity='0.35'/%3E%3Ccircle cx='72' cy='96' r='0.55' fill='%23dce8ff' opacity='0.34'/%3E%3Ccircle cx='1026' cy='83' r='0.55' fill='%23c9dcff' opacity='0.30'/%3E%3Ccircle cx='334' cy='540' r='0.55' fill='%23dce8ff' opacity='0.41'/%3E%3Ccircle cx='875' cy='288' r='0.55' fill='%23c9dcff' opacity='0.39'/%3E%3Ccircle cx='604' cy='553' r='0.85' fill='%23c9dcff' opacity='0.46'/%3E%3Ccircle cx='612' cy='294' r='0.85' fill='%23dce8ff' opacity='0.47'/%3E%3Ccircle cx='997' cy='726' r='0.55' fill='%23dce8ff' opacity='0.23'/%3E%3Ccircle cx='406' cy='893' r='0.55' fill='%23c9dcff' opacity='0.34'/%3E%3Ccircle cx='754' cy='707' r='0.85' fill='%23dce8ff' opacity='0.61'/%3E%3Ccircle cx='771' cy='452' r='0.55' fill='%23dce8ff' opacity='0.23'/%3E%3Ccircle cx='344' cy='400' r='0.55' fill='%23dce8ff' opacity='0.35'/%3E%3Ccircle cx='351' cy='409' r='0.55' fill='%23c9dcff' opacity='0.25'/%3E%3Ccircle cx='964' cy='367' r='0.85' fill='%23dce8ff' opacity='0.51'/%3E%3Ccircle cx='612' cy='371' r='0.85' fill='%23c9dcff' opacity='0.41'/%3E%3Ccircle cx='57' cy='322' r='0.85' fill='%23dce8ff' opacity='0.56'/%3E%3Ccircle cx='460' cy='988' r='0.85' fill='%23dce8ff' opacity='0.50'/%3E%3Ccircle cx='382' cy='699' r='0.55' fill='%23c9dcff' opacity='0.32'/%3E%3Ccircle cx='477' cy='147' r='1.3' fill='%23fff4e2' opacity='0.83'/%3E%3Ccircle cx='924' cy='566' r='0.55' fill='%23c9dcff' opacity='0.37'/%3E%3Ccircle cx='1093' cy='460' r='0.55' fill='%23c9dcff' opacity='0.26'/%3E%3Ccircle cx='954' cy='961' r='0.55' fill='%23c9dcff' opacity='0.41'/%3E%3Ccircle cx='1055' cy='92' r='0.55' fill='%23dce8ff' opacity='0.30'/%3E%3Ccircle cx='596' cy='239' r='0.55' fill='%23dce8ff' opacity='0.22'/%3E%3Ccircle cx='57' cy='917' r='1.3' fill='%23ffe4dc' opacity='0.92'/%3E%3Ccircle cx='128' cy='679' r='0.85' fill='%23dce8ff' opacity='0.63'/%3E%3Ccircle cx='618' cy='1063' r='0.55' fill='%23dce8ff' opacity='0.34'/%3E%3Ccircle cx='245' cy='1094' r='0.55' fill='%23dce8ff' opacity='0.38'/%3E%3Ccircle cx='117' cy='178' r='0.55' fill='%23dce8ff' opacity='0.18'/%3E%3Ccircle cx='926' cy='539' r='0.85' fill='%23dce8ff' opacity='0.54'/%3E%3Ccircle cx='551' cy='325' r='0.55' fill='%23dce8ff' opacity='0.19'/%3E%3Ccircle cx='52' cy='687' r='1.3' fill='%23ffe4dc' opacity='0.82'/%3E%3Ccircle cx='114' cy='382' r='0.55' fill='%23dce8ff' opacity='0.27'/%3E%3Ccircle cx='639' cy='687' r='0.55' fill='%23dce8ff' opacity='0.25'/%3E%3Ccircle cx='552' cy='887' r='0.85' fill='%23dce8ff' opacity='0.56'/%3E%3Ccircle cx='331' cy='818' r='0.55' fill='%23dce8ff' opacity='0.31'/%3E%3Ccircle cx='865' cy='90' r='0.85' fill='%23dce8ff' opacity='0.62'/%3E%3Ccircle cx='369' cy='160' r='1.3' fill='%23ffe4dc' opacity='0.87'/%3E%3Ccircle cx='1023' cy='124' r='0.55' fill='%23c9dcff' opacity='0.21'/%3E%3Ccircle cx='301' cy='914' r='1.3' fill='%23ffe4dc' opacity='0.72'/%3E%3Ccircle cx='273' cy='557' r='0.55' fill='%23dce8ff' opacity='0.39'/%3E%3Ccircle cx='1051' cy='398' r='1.3' fill='%23fff4e2' opacity='0.90'/%3E%3Ccircle cx='424' cy='849' r='0.85' fill='%23dce8ff' opacity='0.58'/%3E%3Ccircle cx='10' cy='113' r='0.55' fill='%23c9dcff' opacity='0.22'/%3E%3Ccircle cx='477' cy='589' r='0.55' fill='%23dce8ff' opacity='0.20'/%3E%3Ccircle cx='866' cy='974' r='0.85' fill='%23c9dcff' opacity='0.42'/%3E%3Ccircle cx='262' cy='811' r='0.85' fill='%23dce8ff' opacity='0.46'/%3E%3Ccircle cx='406' cy='218' r='0.85' fill='%23dce8ff' opacity='0.44'/%3E%3Ccircle cx='675' cy='30' r='0.55' fill='%23dce8ff' opacity='0.30'/%3E%3Ccircle cx='321' cy='540' r='0.85' fill='%23dce8ff' opacity='0.40'/%3E%3Ccircle cx='340' cy='616' r='0.55' fill='%23dce8ff' opacity='0.21'/%3E%3Ccircle cx='346' cy='60' r='0.55' fill='%23dce8ff' opacity='0.19'/%3E%3Ccircle cx='198' cy='699' r='0.55' fill='%23c9dcff' opacity='0.41'/%3E%3Ccircle cx='388' cy='756' r='0.55' fill='%23c9dcff' opacity='0.20'/%3E%3Ccircle cx='535' cy='842' r='0.55' fill='%23c9dcff' opacity='0.31'/%3E%3Ccircle cx='164' cy='1033' r='0.55' fill='%23dce8ff' opacity='0.38'/%3E%3Ccircle cx='147' cy='261' r='0.55' fill='%23dce8ff' opacity='0.37'/%3E%3Ccircle cx='129' cy='454' r='0.85' fill='%23dce8ff' opacity='0.40'/%3E%3Ccircle cx='453' cy='245' r='0.85' fill='%23dce8ff' opacity='0.63'/%3E%3Ccircle cx='1067' cy='23' r='0.85' fill='%23dce8ff' opacity='0.56'/%3E%3Ccircle cx='157' cy='354' r='0.55' fill='%23c9dcff' opacity='0.36'/%3E%3Ccircle cx='115' cy='291' r='0.85' fill='%23dce8ff' opacity='0.61'/%3E%3Ccircle cx='747' cy='661' r='0.85' fill='%23dce8ff' opacity='0.51'/%3E%3Ccircle cx='632' cy='49' r='0.55' fill='%23dce8ff' opacity='0.31'/%3E%3Ccircle cx='157' cy='281' r='0.85' fill='%23dce8ff' opacity='0.54'/%3E%3Ccircle cx='536' cy='191' r='0.55' fill='%23dce8ff' opacity='0.22'/%3E%3Ccircle cx='68' cy='9' r='0.85' fill='%23dce8ff' opacity='0.61'/%3E%3Ccircle cx='601' cy='78' r='0.55' fill='%23dce8ff' opacity='0.41'/%3E%3Ccircle cx='1028' cy='1070' r='1.3' fill='%23fff4e2' opacity='0.83'/%3E%3Ccircle cx='933' cy='997' r='0.55' fill='%23dce8ff' opacity='0.25'/%3E%3Ccircle cx='867' cy='789' r='0.85' fill='%23c9dcff' opacity='0.46'/%3E%3Ccircle cx='93' cy='80' r='1.3' fill='%23ffe4dc' opacity='0.75'/%3E%3Ccircle cx='573' cy='1015' r='0.55' fill='%23dce8ff' opacity='0.28'/%3E%3Ccircle cx='449' cy='52' r='0.55' fill='%23dce8ff' opacity='0.28'/%3E%3Ccircle cx='1057' cy='645' r='0.55' fill='%23c9dcff' opacity='0.27'/%3E%3Ccircle cx='280' cy='942' r='0.55' fill='%23dce8ff' opacity='0.24'/%3E%3Ccircle cx='560' cy='358' r='0.55' fill='%23dce8ff' opacity='0.25'/%3E%3Ccircle cx='345' cy='891' r='0.55' fill='%23dce8ff' opacity='0.26'/%3E%3Ccircle cx='255' cy='1006' r='0.55' fill='%23dce8ff' opacity='0.27'/%3E%3Ccircle cx='115' cy='114' r='0.55' fill='%23c9dcff' opacity='0.33'/%3E%3Ccircle cx='276' cy='909' r='0.85' fill='%23dce8ff' opacity='0.58'/%3E%3Ccircle cx='1053' cy='759' r='0.55' fill='%23dce8ff' opacity='0.33'/%3E%3Ccircle cx='682' cy='17' r='0.55' fill='%23c9dcff' opacity='0.27'/%3E%3Ccircle cx='851' cy='282' r='0.85' fill='%23dce8ff' opacity='0.46'/%3E%3Ccircle cx='898' cy='120' r='0.85' fill='%23c9dcff' opacity='0.62'/%3E%3Ccircle cx='752' cy='1027' r='1.3' fill='%23ffe4dc' opacity='0.86'/%3E%3Ccircle cx='302' cy='854' r='0.55' fill='%23dce8ff' opacity='0.39'/%3E%3Ccircle cx='902' cy='819' r='0.55' fill='%23dce8ff' opacity='0.41'/%3E%3Ccircle cx='21' cy='975' r='0.55' fill='%23c9dcff' opacity='0.22'/%3E%3Ccircle cx='868' cy='241' r='0.55' fill='%23c9dcff' opacity='0.31'/%3E%3Ccircle cx='485' cy='272' r='0.55' fill='%23c9dcff' opacity='0.29'/%3E%3Ccircle cx='494' cy='152' r='0.55' fill='%23c9dcff' opacity='0.25'/%3E%3Ccircle cx='612' cy='382' r='0.55' fill='%23dce8ff' opacity='0.39'/%3E%3Ccircle cx='248' cy='775' r='0.55' fill='%23dce8ff' opacity='0.39'/%3E%3Ccircle cx='994' cy='384' r='0.85' fill='%23dce8ff' opacity='0.40'/%3E%3Ccircle cx='414' cy='70' r='0.55' fill='%23dce8ff' opacity='0.41'/%3E%3Ccircle cx='620' cy='281' r='0.85' fill='%23c9dcff' opacity='0.66'/%3E%3Ccircle cx='294' cy='248' r='0.55' fill='%23dce8ff' opacity='0.32'/%3E%3Ccircle cx='469' cy='949' r='0.85' fill='%23dce8ff' opacity='0.59'/%3E%3Ccircle cx='97' cy='682' r='0.55' fill='%23dce8ff' opacity='0.35'/%3E%3Ccircle cx='258' cy='474' r='0.85' fill='%23dce8ff' opacity='0.56'/%3E%3Ccircle cx='817' cy='275' r='0.55' fill='%23c9dcff' opacity='0.36'/%3E%3Ccircle cx='157' cy='814' r='0.55' fill='%23dce8ff' opacity='0.35'/%3E%3Ccircle cx='451' cy='372' r='0.55' fill='%23dce8ff' opacity='0.38'/%3E%3Ccircle cx='170' cy='575' r='0.55' fill='%23c9dcff' opacity='0.29'/%3E%3Ccircle cx='85' cy='695' r='0.55' fill='%23dce8ff' opacity='0.32'/%3E%3Ccircle cx='430' cy='752' r='0.85' fill='%23dce8ff' opacity='0.60'/%3E%3Ccircle cx='725' cy='850' r='0.85' fill='%23dce8ff' opacity='0.51'/%3E%3Ccircle cx='260' cy='445' r='0.55' fill='%23dce8ff' opacity='0.23'/%3E%3Ccircle cx='233' cy='626' r='0.55' fill='%23dce8ff' opacity='0.25'/%3E%3Ccircle cx='308' cy='719' r='0.55' fill='%23dce8ff' opacity='0.35'/%3E%3Ccircle cx='548' cy='504' r='0.55' fill='%23dce8ff' opacity='0.39'/%3E%3Ccircle cx='1023' cy='476' r='0.55' fill='%23dce8ff' opacity='0.32'/%3E%3Ccircle cx='1068' cy='1079' r='0.55' fill='%23dce8ff' opacity='0.38'/%3E%3Ccircle cx='262' cy='117' r='1.3' fill='%23fff4e2' opacity='0.93'/%3E%3Ccircle cx='625' cy='880' r='0.55' fill='%23c9dcff' opacity='0.39'/%3E%3Ccircle cx='259' cy='314' r='0.55' fill='%23c9dcff' opacity='0.26'/%3E%3Ccircle cx='971' cy='57' r='0.85' fill='%23c9dcff' opacity='0.64'/%3E%3Ccircle cx='321' cy='844' r='1.3' fill='%23ffe4dc' opacity='0.93'/%3E%3Ccircle cx='93' cy='92' r='0.55' fill='%23c9dcff' opacity='0.37'/%3E%3Ccircle cx='253' cy='145' r='0.85' fill='%23c9dcff' opacity='0.54'/%3E%3Ccircle cx='885' cy='662' r='0.85' fill='%23dce8ff' opacity='0.42'/%3E%3Ccircle cx='574' cy='40' r='0.85' fill='%23c9dcff' opacity='0.66'/%3E%3Ccircle cx='656' cy='276' r='0.55' fill='%23dce8ff' opacity='0.22'/%3E%3Ccircle cx='983' cy='1025' r='0.85' fill='%23dce8ff' opacity='0.43'/%3E%3Ccircle cx='506' cy='941' r='0.55' fill='%23dce8ff' opacity='0.39'/%3E%3Ccircle cx='430' cy='27' r='0.85' fill='%23dce8ff' opacity='0.58'/%3E%3Ccircle cx='468' cy='341' r='0.55' fill='%23dce8ff' opacity='0.37'/%3E%3Ccircle cx='693' cy='360' r='0.55' fill='%23dce8ff' opacity='0.40'/%3E%3Ccircle cx='101' cy='8' r='1.3' fill='%23fff4e2' opacity='0.74'/%3E%3Ccircle cx='883' cy='155' r='0.55' fill='%23dce8ff' opacity='0.27'/%3E%3Ccircle cx='496' cy='410' r='0.55' fill='%23dce8ff' opacity='0.34'/%3E%3Ccircle cx='506' cy='405' r='0.55' fill='%23dce8ff' opacity='0.41'/%3E%3Ccircle cx='388' cy='668' r='0.85' fill='%23dce8ff' opacity='0.49'/%3E%3Ccircle cx='17' cy='451' r='1.3' fill='%23ffe4dc' opacity='0.85'/%3E%3Ccircle cx='703' cy='768' r='0.85' fill='%23dce8ff' opacity='0.41'/%3E%3Ccircle cx='1039' cy='350' r='0.85' fill='%23dce8ff' opacity='0.52'/%3E%3Ccircle cx='652' cy='16' r='0.55' fill='%23dce8ff' opacity='0.33'/%3E%3Ccircle cx='840' cy='278' r='0.55' fill='%23dce8ff' opacity='0.25'/%3E%3Ccircle cx='326' cy='254' r='1.3' fill='%23ffe4dc' opacity='0.68'/%3E%3Ccircle cx='466' cy='748' r='0.55' fill='%23dce8ff' opacity='0.40'/%3E%3Ccircle cx='0' cy='801' r='1.3' fill='%23ffe4dc' opacity='0.78'/%3E%3Ccircle cx='911' cy='1039' r='0.55' fill='%23dce8ff' opacity='0.38'/%3E%3Ccircle cx='97' cy='846' r='0.85' fill='%23dce8ff' opacity='0.47'/%3E%3Ccircle cx='615' cy='516' r='0.55' fill='%23c9dcff' opacity='0.19'/%3E%3Ccircle cx='460' cy='948' r='0.85' fill='%23dce8ff' opacity='0.66'/%3E%3Ccircle cx='10' cy='336' r='1.3' fill='%23fff4e2' opacity='0.72'/%3E%3Ccircle cx='132' cy='790' r='0.55' fill='%23dce8ff' opacity='0.21'/%3E%3Ccircle cx='407' cy='1043' r='0.85' fill='%23dce8ff' opacity='0.55'/%3E%3Ccircle cx='663' cy='834' r='0.55' fill='%23c9dcff' opacity='0.27'/%3E%3Ccircle cx='1080' cy='481' r='0.55' fill='%23dce8ff' opacity='0.34'/%3E%3Ccircle cx='1061' cy='1024' r='0.85' fill='%23dce8ff' opacity='0.51'/%3E%3Ccircle cx='888' cy='303' r='0.55' fill='%23c9dcff' opacity='0.30'/%3E%3Ccircle cx='922' cy='770' r='0.55' fill='%23dce8ff' opacity='0.32'/%3E%3Ccircle cx='173' cy='711' r='0.55' fill='%23c9dcff' opacity='0.36'/%3E%3Ccircle cx='379' cy='498' r='0.85' fill='%23c9dcff' opacity='0.43'/%3E%3Ccircle cx='950' cy='172' r='0.85' fill='%23c9dcff' opacity='0.59'/%3E%3Ccircle cx='305' cy='325' r='0.85' fill='%23dce8ff' opacity='0.59'/%3E%3Ccircle cx='153' cy='707' r='1.3' fill='%23ffe4dc' opacity='0.80'/%3E%3Ccircle cx='605' cy='762' r='0.55' fill='%23dce8ff' opacity='0.23'/%3E%3Ccircle cx='74' cy='957' r='0.55' fill='%23dce8ff' opacity='0.24'/%3E%3C/svg%3E");
}

/* vignette */
#field::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(108% 92% at 50% 44%, rgba(0,0,0,0) 44%, rgba(2,3,7,0.72) 100%);
}

/* film grain — kills banding in the gradients */
#grain {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.038;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'>\
<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/></filter>\
<rect width='140' height='140' filter='url(%23n)'/></svg>");
}

#galaxy {
  position: fixed;
  inset: 0;
  z-index: 2;
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

/* -- 2. layout docks ------------------------------------------------------- */
/* OWNED BY `panel_dock_layout` (with the mounts in index.html and the
   arbitration in css/dock.css).

   Panels mount into the containers declared in index.html.  Docks are inert;
   the panels inside them take pointer events back.  An empty mount takes no
   space at all, so the shell shows nothing where nothing is built yet.

   Two rules make "no panel ever covers another" structural rather than lucky:

     1. THE BANDS CANNOT INTERSECT.  Each dock is one absolutely positioned box
        and the five boxes are disjoint by construction: the columns are
        --col-left / --col-right wide at the window edges, and the banner and
        the bottom dock start after --col-left and stop before --col-right.
        Their x ranges are fixed, so no amount of content can move them; the
        banner hangs from the bar and the floor stands on the pad, and §2b caps
        the two of them at 0.34 + 0.44 of the window, so the pair cannot meet
        in the middle either.

     2. INSIDE A BAND, EVERY PANEL IS IN A PANE.  A pane is a hard box: the
        panel in it may be shorter, never taller, and when the column runs out
        of room the panes shrink and the panels scroll inside themselves
        instead of growing over the neighbour below.  A panel does not choose
        its own size; §2b says how big each one is allowed to be. */

/* -- THE SIZE THIS SHELL SUPPORTS, DECLARED (03.09) -------------------------
   The seat rule in §2b guarantees that no PANE is ever shown hollow.  It says
   nothing about a window in which the two columns themselves cannot both
   stand, and that has to be said out loud rather than discovered:

     The docks are laid out for a window of at least 1024 x 600 CSS pixels.
     The columns are a fixed 332 px and 270 px at the two edges, so below
     about 640 px of width they intersect each other, and below about 600 px
     of height the free band between them closes.  Neither of those is a pane
     the dock hands out and neither can be answered from this file alone — the
     top bar's seven resource cells and the tab rail fail at the same width,
     and both are other features' files.

   What the dock does below that size is the seat rule applied honestly rather
   than a redesign it cannot make: a pane that cannot be seated is folded to
   its header, and the one pane that has nowhere at all to stand — the faction
   flyout, whose band has closed — is not shown, instead of being laid out from
   x = -230 with a priced button off the left of the window.  That is
   predictable; it is not the same as supported, and it is not claimed to be. */

#ui-root {
  position: fixed;
  inset: 0;
  z-index: var(--z-dock);
  pointer-events: none;
  display: block;
}

.mount:empty { display: none !important; }
.mount > * { pointer-events: auto; }

.dock {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: var(--dock-gap);
  min-height: 0;
  pointer-events: none;
}

.dock--left {
  left: var(--dock-pad);
  top: calc(var(--topbar-h) + var(--dock-gap));
  bottom: var(--dock-pad);
  width: var(--col-left);
}

.dock--right {
  right: var(--dock-pad);
  top: calc(var(--topbar-h) + var(--dock-gap));
  bottom: var(--dock-pad);
  width: var(--col-right);
  align-items: stretch;
}

.dock--bottom {
  left: calc(var(--col-left) + var(--dock-pad) * 2);
  right: calc(var(--col-right) + var(--dock-pad) * 2);
  bottom: var(--dock-pad);
  flex-direction: row;
  align-items: flex-end;
  justify-content: center;
  gap: var(--dock-gap);
}

/* the free band under the bar, between the two columns: a strip, never a
   sheet — whatever docks here is capped in §2b and starts collapsed */
.dock--banner {
  left: calc(var(--col-left) + var(--dock-pad) * 2);
  right: calc(var(--col-right) + var(--dock-pad) * 2);
  top: calc(var(--topbar-h) + var(--dock-gap));
  align-items: center;
}

.dock--top {
  left: 0; right: 0; top: 0;
  height: var(--topbar-h);
  z-index: var(--z-topbar);
}
.dock--top > .mount { height: 100%; }

/* THE TAB RAIL — a sixth band, and the reason `panel_arbitration_pass_2` was
   sent back.  The two economy screens each hang a launcher on the screen at all
   times.  Both were positioned against the WINDOW from inside their own
   stylesheet (`.mkt-tab` fixed, `.wrk-tab` absolute in a full-screen overlay),
   which is exactly the "a panel that invented its own container" the brief
   forbids — and on a narrow window works.css moved its tab to
   `right: var(--dock-pad)`, which is the right column.  Measured at 1024x640 and
   at 1152x800, on the first frame with nothing done: the Empire works tab lay
   125x26 across Naval Command, and two live fleet buttons — "Merge 2 fleets,
   free" and "Reinforce +15" — hit-tested to the tab at their own centres.

   A launcher is a panel, so it gets a mount like every other panel.  The rail
   is one absolutely positioned box in the free band BETWEEN the two columns,
   under the bar and under the banner strip; the tabs are flow items inside it.
   They cannot reach either column at any width because the box they live in
   cannot, and if the band is ever too narrow for both they wrap downwards and
   the rail's own height (published as --dock-tabs-h) carries everything below
   it — the toast lane, the flyout ceiling and both screens — down with them.
   src/ui/dock.js §1b declares the rail; works-panel.js / market-panel.js seat
   their tab in it. */
.dock--tabs {
  left: calc(var(--col-left) + var(--dock-pad) * 2);
  right: calc(var(--col-right) + var(--dock-pad) * 2);
  top: calc(var(--topbar-h) + var(--dock-gap) + var(--dock-banner-h, 0px) + var(--dock-gap));
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  /* one step over the overlay band: the works screen dims the whole window
     while it is open, and the ticker — the thing that turns amber the moment a
     silo pins — may not go behind that scrim.  Still inside #ui-root, so a
     sheet and an ending outrank it. */
  z-index: calc(var(--z-overlay) + 1);
}
.mount--tab { display: flex; flex: 0 0 auto; min-width: 0; }

/* a mount pinned to the far end of its dock (alert feed, factions) */
.mount--bottom { margin-top: auto; }

/* THE PANE.  The box a panel is given.  It clips, so a panel that insists on
   being taller than its box is cut off inside its own column instead of
   painting over the one below it — and every panel in this build answers a
   short box by scrolling its body, which is what the pane wants. */
.mount--pane {
  position: relative;              /* the pane draws the "more below" edge */
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}
.mount--pane > * { flex: 0 1 auto; min-height: 0; }

/* -- 2b. how much of a column each panel may take -------------------------- */
/* One place, one owner — and from `panel_arbitration_pass_2` on, the share is
   not a fixed fraction any more.  Fixed fractions were what left a 245 px hole
   under the Colonial Office while the Situation Log, capped at 0.45 of the
   column, hid 1,457 px behind it, and what left a 460 px hole when the Upkeep
   pane was collapsed: a pane that does not want its share cannot give it back,
   and a pane that wants more cannot take it.
   So the column is handed out by DEMAND instead, in src/ui/dock.js §2b: every
   pane is measured (its panel's height plus whatever its body is hiding), and
   the column's height is shared out weighted-max-min — a pane that fits gets
   all it asks for, and what is left over goes to the ones that are still short,
   in proportion to the weights below.  Nothing here caps a column pane; these
   two properties are the whole vocabulary the solver reads.

     --dock-weight  its share of what is left after the small panes are full
     --dock-min     what this pane would LIKE not to go below.  A preference,
                    not a floor: it is a number typed here, in a window nobody
                    has seen, and the floor is measured on the live panel.

   THE SEAT RULE (03.09 — the decision `panel_arbitration_pass_2` was sent back
   to make, and the one every pane in this build obeys):

     A PANE IS EITHER SEATED OR IT IS NOT SHOWN.  THERE IS NO HOLLOW STATE.

   Seated means the panel's IRREDUCIBLE CHROME — its header, a footer it holds
   outside its scroller, a card it pins — is inside the box, and there is at
   least ONE OF THAT PANEL'S OWN ROWS of body left under it.  Both are measured
   by src/ui/dock.js §2b on the panel itself, because a panel's own reported
   height is a lie the moment its pane has clipped it: the faction flyout's
   panel measured 44 px while its head, body and footer added to 91, and the
   49 px difference was its only action — "Grant - 70 influence, 150 minerals",
   laid out 40 px BELOW the floor of the box that clipped it, answering
   `SPAN.label` from the research drawer when a real click landed on it.

   So the share-out degrades in three stages and stops at the first that fits:
   everyone at `max(--dock-min, floor)`; then everyone at their floor, the
   preference dropped; then panes are FOLDED — header only, body not laid out
   at all, the strip itself the control that opens it again (css/dock.css §4e)
   — lowest claim first, until the rest can be seated.  Cutting every floor in
   proportion, which is what this used to do, is gone: it is how a column ends
   up spending its height on chrome and handing four panes 25 px of body each.

   A pane that cannot be seated even with its neighbours folded is not shown. */

:root {
  /* the height of one full column: window less the bar, its gap, and the pad */
  --pane-col-h: calc(100vh - var(--topbar-h) - var(--dock-gap) - var(--dock-pad));
}

.dock--left > .mount--pane,
.dock--right > .mount--pane {
  --dock-weight: 1;
  --dock-min: 132px;
}

/* the empire's own worlds: compact, and it almost always gets all it asks */
#system-detail  { --dock-weight: 1;    --dock-min: 128px; }
/* THE RECORD FOR THE STAR THAT WAS JUST CLICKED.  It is the answer to the
   action the player took a quarter of a second ago, and it is the longest
   panel in the build (planets, fleets and every order that applies there), so
   it takes the largest claim in its column while it is up — and gives the
   whole of it back the moment the selection is dropped, because the mount goes
   `:empty` and leaves the column. */
#system-record  { --dock-weight: 1.9;  --dock-min: 190px; }
#alert-feed     { --dock-weight: 1.15; --dock-min: 150px; }
/* the log yields while a record is open: it is a standing feed, and the record
   is a question the player has just asked */
.dock--left:has(#system-record > *) #alert-feed { --dock-weight: 0.7; --dock-min: 132px; }

#war-room       { --dock-weight: 1.2;  --dock-min: 150px; }
#outliner       { --dock-weight: 0.95; --dock-min: 120px; }
#outliner-list  { --dock-weight: 1;    --dock-min: 120px; }
#factions       { --dock-weight: 1.15; --dock-min: 160px; }

/* a war doubles what Naval Command has to say — the wars, the shipyard and
   every fleet row's orders — so it is worth more of the column while one is on */
.dock--right:has(.war.is-atwar) #war-room { --dock-weight: 1.7; --dock-min: 190px; }

/* A PANEL DOES NOT CHOOSE ITS OWN SIZE.  Six of the panels in the two columns
   carry a max-height of their own, written when each of them was alone in the
   window; every one of them is below what the solver hands out at 1440×900 and
   would silently cap the box the dock just gave it.  The pane is the box. */
#ui-root .dock--left > .mount--pane > *,
#ui-root .dock--right > .mount--pane > * { max-height: none; }

/* the bottom dock is a drawer on the floor of the map: it may never become a
   sheet over the middle of it.  It is a row, not a column, so it is capped
   here rather than solved. */
#research      { max-height: min(50vh, 452px); }
#legend        { max-height: 22vh; }

/* the banner is a strip under the bar; open it and it still cannot reach the
   middle of the map */
.mount--banner {
  width: min(716px, 100%);
  max-height: min(34vh, 320px);
}

/* A FLYOUT MOUNT.  A panel that is opened FROM another panel and is about one
   row of it — the faction flyout is the only one so far.  It is not in a
   column, because it would starve the column it was opened from; it is a pane
   all the same, so it is measured, it clips, and it gets the same scroll rail
   and the same bottom edge as every other pane.  It stands in the free band
   against the inner edge of the right column, on the floor, clear of the
   drawer.

   IT SHARES THAT BAND WITH THE TOAST COLUMN, and "the toasts start at the top
   and this ends at the bottom, so the two can never meet" was wishful: a critic
   photographed a first-contact toast lying across an open Expansionists flyout,
   burying its whole SUPPORT / APPROVAL / THEY DEMAND block, and the flyout's own
   close button hit-tested to the toast at four window sizes.  The lane is now
   divided for real, and in one direction only so nothing can oscillate:

     THE FLYOUT NEVER RISES INTO THE TOAST LANE.  Its ceiling is the foot of the
     tab rail plus one toast's worth of room (--fly-lane), whatever the window.
     ui/alert-feed.js then trims its column to whatever is left above the
     flyout's measured top edge, retiring the oldest toast first.

   So the flyout's box does not depend on how many toasts are up, and the toast
   column does depend on the flyout — a one-way rule, the same shape as the one
   that keeps the two canvas cards apart.

   AND THE LANE IS A COURTESY, NOT A CLAIM (03.09).  The `calc` that used to
   live here subtracted the lane, the banner, the rail, the floor and five gaps
   from the window in one static expression, and at 1024x640 with the research
   drawer standing on the floor it came out at 44 px — while the free band the
   flyout stands in was 194 px the whole time.  A pane may not be shaved to a
   title bar to keep a lane clear for a toast that is also a row in the
   Situation Log and was not asked for.  The band is measured in src/ui/dock.js
   §2c and shared out by the same three stages the columns use: the lane is
   honoured while the flyout still gets `--dock-min`, it yields down to the
   flyout's measured floor, and below that the drawer standing on the band is
   closed through its own control.  What is left here is the box the flyout
   stands in and a ceiling that is the band itself — the fallback before the
   first share-out runs, and never again the reason a panel is hollow. */
:root {
  /* what the toast column is held above the flyout while there is room for it */
  --fly-lane: 140px;
}
.mount--fly {
  position: absolute;
  right: calc(var(--col-right) + var(--dock-pad) * 2);
  bottom: calc(var(--dock-pad) + var(--dock-bottom-h, 0px) + var(--dock-gap));
  width: 330px;
  max-height: calc(100vh - var(--topbar-h) - var(--dock-banner-h, 0px)
                   - var(--dock-tabs-h, 0px) - var(--dock-bottom-h, 0px)
                   - var(--dock-gap) * 4 - var(--dock-pad));
  z-index: calc(var(--z-overlay) + 2);
}
/* what it would like to keep: enough for the creed, the three figures and the
   demand — the reading the player opened it to do.  It is a preference like
   every other `--dock-min`; the guarantee under it is measured. */
#faction-flyout { --dock-min: 240px; }

/* full-screen mounts inside #ui-root: the hint layer and the diplomacy screen */
.mount--overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: grid;
  place-items: center;
  pointer-events: none;
}

/* -- 3. panel chrome ------------------------------------------------------- */

.panel {
  position: relative;
  background:
    linear-gradient(180deg, rgba(38,54,78,0.30) 0, rgba(38,54,78,0) 42px),
    var(--glass);
  border: 1px solid var(--rim);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-panel), inset 0 1px 0 var(--rim-inner);
  backdrop-filter: blur(var(--blur)) saturate(1.15);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(1.15);
  color: var(--ink-1);
  overflow: hidden;
}

/* corner brackets — the Paradox tell, kept faint */
.panel::before,
.panel::after {
  content: "";
  position: absolute;
  width: 11px; height: 11px;
  pointer-events: none;
  opacity: 0.55;
}
.panel::before {
  left: -1px; top: -1px;
  border-left: 1px solid var(--accent-dim);
  border-top: 1px solid var(--accent-dim);
}
.panel::after {
  right: -1px; bottom: -1px;
  border-right: 1px solid var(--accent-dim);
  border-bottom: 1px solid var(--accent-dim);
}

.panel__head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: 28px;
  padding: 0 var(--sp-4);
  border-bottom: 1px solid rgba(139,172,209,0.16);
  background: linear-gradient(180deg, rgba(122,160,205,0.10), rgba(122,160,205,0.02));
  flex: 0 0 auto;
}

.panel__title {
  font-family: var(--font-hud);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}

.panel__head .spacer { flex: 1 1 auto; }

.panel__body {
  padding: var(--sp-4);
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

.panel--flush .panel__body { padding: 0; }

/* the top bar is a panel that spans the window */
.topbar {
  height: var(--topbar-h);
  display: flex;
  align-items: stretch;
  background:
    linear-gradient(180deg, rgba(46,66,96,0.34) 0%, rgba(10,16,26,0.86) 62%, rgba(6,10,18,0.92) 100%);
  border: 0;
  border-bottom: 1px solid var(--rim);
  border-radius: 0;
  box-shadow: 0 10px 26px rgba(0,0,0,0.55), inset 0 1px 0 rgba(190,220,255,0.10);
  backdrop-filter: blur(var(--blur)) saturate(1.2);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(1.2);
}
.topbar::before, .topbar::after { display: none; }

/* the cyan hairline under the bar */
.topbar-underline {
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg,
    rgba(79,214,255,0) 0%,
    rgba(79,214,255,0.45) 18%,
    rgba(79,214,255,0.16) 50%,
    rgba(79,214,255,0.45) 82%,
    rgba(79,214,255,0) 100%);
  pointer-events: none;
}

/* -- 4. controls ----------------------------------------------------------- */

.btn {
  font-family: var(--font-hud);
  font-weight: 600;
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--ink-1);
  background: linear-gradient(180deg, rgba(84,116,158,0.22), rgba(84,116,158,0.06));
  border: 1px solid var(--rim);
  border-radius: var(--r-sm);
  padding: 5px 11px 4px;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.btn:hover {
  color: var(--ink);
  border-color: var(--rim-strong);
  background: linear-gradient(180deg, rgba(110,152,200,0.32), rgba(110,152,200,0.10));
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }

.btn--accent {
  color: #04121a;
  background: linear-gradient(180deg, var(--accent-2), var(--accent-dim));
  border-color: var(--accent);
  box-shadow: 0 0 14px var(--accent-glow);
}
.btn--accent:hover { color: #04121a; background: linear-gradient(180deg, #a5f0ff, #3ca7c9); }

.btn--danger { color: var(--danger); border-color: rgba(255,91,98,0.4); }
.btn--danger:hover { background: var(--danger-wash); border-color: var(--danger); }

.btn[disabled], .btn:disabled, .btn.is-disabled {
  opacity: 0.42;
  cursor: not-allowed;
  color: var(--ink-3);
  background: rgba(60,80,108,0.10);
  border-color: rgba(139,172,209,0.14);
  box-shadow: none;
}
.btn[disabled]:active { transform: none; }

.btn--icon {
  padding: 4px 6px;
  min-width: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.btn.is-active {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-wash);
  box-shadow: inset 0 0 12px rgba(79,214,255,0.14);
}

/* -- 5. figures, labels, meters -------------------------------------------- */

.label {
  font-family: var(--font-hud);
  font-weight: 600;
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--ink-3);
  line-height: 1.1;
}

.title {
  font-family: var(--font-hud);
  font-weight: 700;
  font-size: var(--fs-lg);
  letter-spacing: var(--ls-title);
  color: var(--ink);
}

.num {
  font-family: var(--font-hud);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  letter-spacing: var(--ls-num);
  color: var(--ink);
}

.num--pos { color: var(--good); }
.num--neg { color: var(--danger); }
.num--zero { color: var(--ink-3); }
.num--warn { color: var(--warn); }

.kv { display: flex; align-items: baseline; gap: var(--sp-3); }
.kv > .label { flex: 0 0 auto; }
.kv > .spacer { flex: 1 1 auto; border-bottom: 1px dotted rgba(139,172,209,0.16); }

.meter {
  position: relative;
  height: 4px;
  border-radius: 2px;
  background: var(--glass-sunken);
  box-shadow: inset 0 0 0 1px rgba(139,172,209,0.14);
  overflow: hidden;
}
.meter__fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: linear-gradient(90deg, var(--accent-dim), var(--accent));
  transition: width var(--dur) var(--ease);
}

.divider {
  height: 1px;
  background: linear-gradient(90deg, rgba(139,172,209,0), rgba(139,172,209,0.22), rgba(139,172,209,0));
  margin: var(--sp-3) 0;
  border: 0;
}

.sev-info  { color: var(--sev-info); }
.sev-good  { color: var(--sev-good); }
.sev-warn  { color: var(--sev-warn); }
.sev-alert { color: var(--sev-alert); }

.icon { width: 14px; height: 14px; display: block; flex: 0 0 auto; fill: currentColor; }
.icon--lg { width: 18px; height: 18px; }

/* scrollbars — thin, cool, never native grey */
* { scrollbar-width: thin; scrollbar-color: rgba(139,172,209,0.28) transparent; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgba(139,172,209,0.24);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: rgba(139,172,209,0.44); background-clip: padding-box; }

/* -- 6. tooltip ------------------------------------------------------------ */
/* One delegated tooltip for the whole app.  Any element carrying data-tip
   raises it; optional data-tip-title adds a header.  A line containing " :: "
   is laid out as a label/value row.  See src/ui/tooltip.js. */

#tooltip-host {
  position: fixed;
  inset: 0;
  z-index: var(--z-tooltip);
  pointer-events: none;
  overflow: hidden;
}

.tip {
  position: absolute;
  top: 0; left: 0;
  max-width: 288px;
  padding: 7px 10px 8px;
  background: var(--glass-strong);
  border: 1px solid var(--rim-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-panel);
  backdrop-filter: blur(10px) saturate(1.2);
  -webkit-backdrop-filter: blur(10px) saturate(1.2);
  color: var(--ink-1);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  line-height: 1.38;
  opacity: 0;
  transform: translateY(3px);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
  will-change: left, top, opacity;
}
.tip.is-open { opacity: 1; transform: translateY(0); }

.tip::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: linear-gradient(180deg, var(--accent), rgba(79,214,255,0.05));
  opacity: 0.75;
}

.tip__title {
  font-family: var(--font-hud);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 3px;
}

.tip__line { display: block; }
.tip__line + .tip__line { margin-top: 1px; }

.tip__row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  justify-content: space-between;
}
.tip__row > span:first-child { color: var(--ink-2); }
.tip__row > span:last-child {
  font-family: var(--font-hud);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* -- 7. the layers over the map -------------------------------------------- */
/* OWNED BY `modal_sheet_layout_and_toast_suppression`, with the containers in
   index.html (`panel_dock_layout`'s) and src/ui/layers.js, which is the code
   half of exactly this section.

   #ui-root sets a z-index, which makes it a stacking context: a sheet declared
   inside it can NEVER be drawn over the alert feed's toast column, because that
   column lives on <body>.  Measuring the toasts and dodging them was the old
   answer and it lost to a three-alert storm.  The answer is structural instead:
   the two layers that must be able to take the whole screen are siblings of
   #ui-root, not children of it, and the order is declared once, here.

       0 – 2          the field, the galaxy canvas, the grain
       --z-dock  20   #ui-root: every docked panel, the top bar, the drawers
       --z-overlay    A SCREEN inside #ui-root: EMPIRE WORKS, THE GALACTIC
                      MARKET.  Modeless, dimming the map with its own scrim,
                      and — src/ui/layers.js — never more than one of them
       --z-toast 100  .afd-toasts, the toast column on <body>
       --z-sheet 200  #sheet-layer: every clock-stopping sheet
       --z-ending 300 #ending-layer: victory and defeat, which outrank a sheet
       --z-tooltip    the one delegated tooltip, over everything by definition

   THE ORDER IS ONLY HALF OF IT.  Being underneath is not the same as standing
   down, and six critics found the difference: a toast column drawn correctly
   below a sheet still shouted over it, and two economy screens correctly
   ordered still both thought they were open.  src/ui/layers.js is the arbiter —
   it keeps one screen at a time, closes a screen when a sheet or an ending
   takes over, and publishes the winner as `<html data-top-layer>` so the toast
   column and the tooltip can hold back rather than merely lose.

   (css/theme.css's --z-modal named the old in-#ui-root modal and is no longer
   used by anything; --z-overlay still layers #hints and #diplomacy INSIDE
   #ui-root, where they belong.) */

:root {
  --z-toast:  100;
  --z-sheet:  200;
  --z-ending: 300;
}

#sheet-layer,
#ending-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;          /* the sheet's own scrim takes events back */
}
#sheet-layer  { z-index: var(--z-sheet); }
#ending-layer { z-index: var(--z-ending); }

/* The sheet mount fills its layer and centres the sheet in it.  It holds
   nothing at all while nothing is being asked, so `.mount--modal > *` — which
   ui/camera.js and ui/fleet-orders.js read before they take Escape — is true
   exactly when a sheet really is up. */
#sheet-layer .mount--modal {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--sheet-margin);
  pointer-events: none;
}

/* THE BOX A SHEET IS GIVEN.  The brief's rule for a docked panel is the rule
   for a sheet as well: it does not choose its own size, it is given one and
   must stay legible inside it — header fixed, body scrolling, nothing sliced.
   The only thing declared here is how much of the window it may have, because
   that is a question about the LAYER and not about any one sheet.

   The margin is generous where there is room and small where there is not.  A
   two-system demand at 1280×720 overflowed its body by eleven pixels against a
   flat 16px margin, and eleven pixels of overflow means the last target row
   spends its life under the "there is more below" fade — a row that is not
   clipped and is not readable either, which is the worst of both.  Below 820px
   of window the margin closes to 6px and the sheet takes the room instead. */
:root { --sheet-margin: var(--sp-5); }

@media (max-height: 820px) {
  :root { --sheet-margin: var(--sp-2); }
}

#sheet-layer [role="dialog"][aria-modal="true"] {
  max-height: min(calc(100vh - var(--sheet-margin) * 2), 860px);
}

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes slide-in-left {
  from { opacity: 0; transform: translateX(-12px); }
  to   { opacity: 1; transform: none; }
}

/* -- 8. utilities ---------------------------------------------------------- */

.row { display: flex; align-items: center; gap: var(--sp-3); }
.col { display: flex; flex-direction: column; gap: var(--sp-2); }
.spacer { flex: 1 1 auto; }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dim { color: var(--ink-3); }
.hidden { display: none !important; }
.clickable { cursor: pointer; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
