.original-map {

&{
  --gold:#f0a500; --gold-dim:#a06800; --cyan:#00e5ff; --cyan-dim:#0096a8;
  /* Field-level edit status (EDIT-TRACKING-AND-DISPUTES.md) -- reuses this
     site's own existing green/amber/red convention (CONFLICT_COL in the
     script below) rather than introducing a second palette; "flagged"
     happening to equal --gold is intentional, not a collision -- CONFLICT_COL
     already overloads this exact hex for its own "medium" tier. */
  --fs-edited:#6bffa8; --fs-flagged:#f0a500; --fs-disputed:#ff5a4a;
  --bg:#070b11; --bg2:#0b1119; --bg3:#0f1620;
  --panel:rgba(255,255,255,0.035); --panel-2:rgba(0,229,255,0.05);
  --border:rgba(0,229,255,0.16); --border-soft:rgba(255,255,255,0.07);
  --text:#cfe0f0; --text-dim:rgba(207,224,240,0.6); --text-faint:rgba(207,224,240,0.4);
  --white:#eaf6ff; --r-sm:8px;
  /* ---- spec item 10: nms-hud-card/nms-btn theme variables ---- */
  --nms-bg-glass:rgba(4,10,20,0.85);
  --nms-border-neon:rgba(0,229,255,0.3);
  --nms-border-neon-hover:rgba(0,229,255,0.7);
  --color-magenta:#ff0055;
  --color-matrix:#00ff66;
  --color-slate:#70a1ff;
}
*{box-sizing:border-box;margin:0;padding:0}
/* Themed thin scrollbar for every scrollable box in the app (modals, Filters,
   Hyperdrive panel, admin panel, etc). Without this, every overflow-y:auto
   element falls back to the OS/browser's own default scrollbar -- on Windows
   Chrome/Edge that's the old bulky non-overlay style with visible track +
   arrow buttons at each end (Tony's screenshots: "why slide bars" on a
   one-time notice modal whose text clearly fit without needing to scroll at
   all). A real scrollbar only ever shows when content genuinely overflows --
   this doesn't change WHETHER one appears, only makes it look like a
   deliberate part of the UI instead of a jarring stock OS widget when it
   does. Firefox gets the equivalent via scrollbar-width/scrollbar-color.

   IMPORTANT, hit repeatedly (#hyperPanel/#tweak/hyperdriveModal/#searchPop):
   "only shows when content genuinely overflows" is NOT reliably true for
   plain overflow-y:auto -- a sub-pixel scrollHeight-vs-clientHeight
   rounding quirk (confirmed live as 181 vs 180) shows this themed
   scrollbar even when content visibly fits. Any NEW box with a max-height
   must instead default to overflow-y:hidden, add a .scroll class that sets
   overflow-y:auto, and toggle it from JS after layout:
     el.classList.remove("scroll");
     if(el.scrollHeight>el.clientHeight+1) el.classList.add("scroll");
   called once when the box opens and again after anything that changes its
   content height. Copy #searchPop's remeasureScroll() as the reference
   implementation -- do not re-derive this from scratch again. */
*{scrollbar-width:thin;scrollbar-color:var(--cyan-dim) rgba(0,0,0,.25)}
*::-webkit-scrollbar{width:7px;height:7px}
*::-webkit-scrollbar-track{background:rgba(0,0,0,.25)}
*::-webkit-scrollbar-thumb{background:var(--cyan-dim);border-radius:4px}
*::-webkit-scrollbar-thumb:hover{background:var(--cyan)}
*::-webkit-scrollbar-corner{background:transparent}
&{height:100%;overflow:hidden;background:var(--bg);color:var(--text);
  font-family:'Rajdhani',system-ui,sans-serif;-webkit-font-smoothing:antialiased;
  -webkit-tap-highlight-color:transparent}
/* 2026-09-13, Tony: "when the curser is over either the galaxy map or local
   map it needs to change to a grabbing hand so users know its moveable, as
   its just a arrow at the moment" -- grab at rest (the canvas is always
   pan/orbit-draggable in every mode, not just Galaxy/Local), swapped for
   grabbing the moment a real drag starts (see the pointerdown/endPointer
   handlers below) and back to grab when it ends. The existing hover-pointer
   affordance for a clickable Galaxy-view marker (handleHoverRay) already
   clears its own inline cursor back to "" while not hovering one, which
   now falls through to this grab default instead of the plain arrow. */
canvas#c{display:block;position:fixed;inset:0;touch-action:none;cursor:grab}
canvas#tstars{display:none;position:fixed;inset:0;pointer-events:none;z-index:1}

.kicker{font-family:'Orbitron',sans-serif;font-size:10px;letter-spacing:.18em;
  color:var(--gold);text-transform:uppercase}
.box{background:rgba(11,17,25,.93);border:1px solid var(--border);border-radius:12px;
  backdrop-filter:blur(6px);position:relative}
/* Filters/Panel/Course-plotted headers double as drag handles (see makeDraggable) --
   Tony's own suggestion after several sessions of chasing device-specific overlap
   bugs one at a time: instead of guessing every viewport/orientation combination,
   let him move the boxes himself. touch-action:none stops the browser's own
   scroll/zoom gestures from stealing the pointer before the drag handler sees it. */
#fHead,.phead,.chead,#a11yHead,.edhead,#searchHead,#keysHead{cursor:grab;touch-action:none;
  padding:6px 6px 0 6px;margin:-6px -6px 0 -6px;border-radius:8px}
/* 2026-08-23: #keysHead alone needed an explicit user-select:none too, unlike
   the others above -- it's scoped to just the bare "Portal glyph keypad" text
   (deliberately, so the galaxy search input next to it stays fully clickable,
   see makeDraggable's own call site comment), which means every single click
   inside the handle lands directly on a text glyph with zero padding around
   it to miss. Without this the browser's native click-and-drag-to-select-text
   behaviour won as visible in Tony's own screenshot (label just highlights
   blue on drag, box doesn't move) -- none of the wider header handles above
   hit this in practice since they have padding/icons around their text a
   drag more often starts on instead. */
#keysHead{user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.box::before,.box::after{content:'';position:absolute;width:9px;height:9px;pointer-events:none}
.box::before{top:-1px;right:-1px;border-top:1px solid var(--cyan);border-right:1px solid var(--cyan);border-radius:0 12px 0 0}
.box::after{bottom:-1px;left:-1px;border-bottom:1px solid var(--cyan);border-left:1px solid var(--cyan);border-radius:0 0 0 12px}

/* align-items:flex-end (not center) so every same-height toolbar button
   bottom-lines-up with the address group's input row -- #addrGrp is a
   two-line stack (label + input row) so under center alignment its taller
   box got vertically centered as a whole, leaving the actual Jump/Clear/
   input row sitting visibly lower than every single-line button next to
   it (Tony's "buttons are not level" screenshot). .sep below restores its
   own centering since a short 18px divider bottom-anchored to a ~30px row
   would look stuck to the floor instead of centred between the buttons. */
#top{position:fixed;top:0;left:0;right:0;z-index:30;display:flex;align-items:flex-end;
  gap:6px;padding:8px 10px;background:linear-gradient(#070b11f2,#070b1100);flex-wrap:wrap}
/* 2026-08-30, Tony: mobile-only "hide controls" toggle (see #top>*:not(...)
   rule in the @media(max-width:900px) block) -- desktop's single-row bar
   never covers the map, so this button stays hidden there and only turns
   on for the mobile stacked layout where the full grouped toolbar can run
   to 7+ rows and bury the system view underneath it. */
#toolbarToggle{display:none}
/* #toolbarBody only becomes a real box on mobile (see the media query) so it
   can be animated as one collapsible unit -- here on desktop it's invisible
   scaffolding, not a layout box, so every button/select/sep inside it still
   flows as a direct flex child of #top exactly as before this wrapper was
   added. */
#toolbarBody{display:contents}
/* 2026-08-30, Tony: the title used to be plain 12px text sitting in the same
   flex row as the toolbar buttons -- same size class as the secondary GRID/
   ATLAS/TOUR row, easy to lose among 24 other flex items. Now it's its own
   bordered badge (same bordered-box language the tag/panel UI already uses
   elsewhere) with the site's real favicon as a spinning icon, and the text
   types itself out once per page load before settling into a slow ambient
   glow -- see the boot script near the end of this file for the one-shot
   typewriter, and prefers-reduced-motion below for the escape hatch. */
#title{display:flex;align-items:center;gap:9px;
  font-family:'Orbitron',sans-serif;font-weight:700;font-size:20px;letter-spacing:.1em;
  color:var(--white);white-space:nowrap;margin-right:10px;
  padding:6px 14px;border-radius:var(--r-sm);
  border:1px solid rgba(0,229,255,.15);background:rgba(0,229,255,.02);
  animation:titleBoxBreathe 3.6s ease-in-out infinite}
#title.typing{animation:none;border-color:rgba(0,229,255,.15);background-color:rgba(0,229,255,.02)}
#titleIcon{width:26px;height:26px;flex:none;display:block;
  animation:titleIconSpin 5s linear infinite}
#titleText{display:inline-block;animation:titleTextBreathe 3.6s ease-in-out infinite}
#title.typing #titleText{animation:none;color:var(--white);text-shadow:none}
@keyframes titleIconSpin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
@keyframes titleTextBreathe{
  0%{color:rgb(140,200,220);text-shadow:none}
  50%{color:rgb(235,246,255);text-shadow:0 0 18px rgba(0,229,255,.85),0 0 40px rgba(0,229,255,.6),0 0 60px rgba(0,229,255,.3)}
  100%{color:rgb(140,200,220);text-shadow:none}
}
@keyframes titleBoxBreathe{
  0%{border-color:rgba(0,229,255,.15);background-color:rgba(0,229,255,.02)}
  50%{border-color:rgba(0,229,255,.9);background-color:rgba(0,229,255,.11)}
  100%{border-color:rgba(0,229,255,.15);background-color:rgba(0,229,255,.02)}
}
/* A visitor with reduced-motion set gets the finished title and a static
   icon -- no spin, no breathing, no typewriter (guarded again in the boot
   script below, so it never even starts building the animation timers). */
@media (prefers-reduced-motion: reduce){
  #titleIcon,#title,#titleText{animation:none}
}
.btn{font-family:'Orbitron',sans-serif;font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;
  background:var(--panel);border:1px solid var(--border);color:var(--text);
  padding:6px 10px;border-radius:var(--r-sm);cursor:pointer;white-space:nowrap}
.btn:hover{background:var(--panel-2);border-color:var(--gold)}
.btn.on{border-color:var(--gold);color:var(--gold);background:rgba(240,165,0,.08)}
/* Same gold "this matters" language as .btn.on, but for a primary action
   rather than an active toggle state -- used on the Jump button so it's
   visually the obvious next step right after typing an address. */
#top .btn.primary{border-color:var(--gold);color:var(--gold)}
.btn.off{opacity:.4;border-color:var(--border-soft);color:var(--text-faint)}
/* 2026-08-30, Tony: was 1px at var(--border-soft) (7% white) -- functionally
   invisible against the starfield, which is the real reason the toolbar's
   five groups (galaxy picker / view mode / travel / display / camera / info)
   never read as grouped even though the dividers were always there. Bumped
   to a visible cyan line with a little breathing room on each side. */
.sep{width:2px;height:22px;background:rgba(0,229,255,.35);align-self:center;margin:0 4px}
/* 2026-09-13, Tony: "can the yellow be in sync on all buttons as its
   distracting when look that they rotate at different times" -- inherits
   was false, so every .btn.beveled got its OWN independent --bevAng
   starting from 0deg at whatever real moment IT first started animating
   (page load for toolbar buttons always on screen, but much later --
   and different again each time -- for one that only exists inside a
   popover, or that only just gained .beveled/.on). Same duration on all
   of them was never enough to keep them in phase with each other.
   Fix: inherits:true plus ONE animation on html below (not each button)
   means every button just reads the SAME live --bevAng off the document
   root, so they're mechanically always in lockstep, regardless of when
   each one appeared or turned on -- not a coincidence of matching timers. */
/* 2026-09-17, Tony (real-device playtest): "remove the animation/
   circling colours around the buttons as i believe thats why is making it
   lag". The animated version below drove a registered @property --bevAng
   through a conic-gradient() background on every single .btn.beveled
   element via one shared html-level @keyframes -- same root-cause class as
   the toolbarRowReveal filter:drop-shadow bug fixed back in Session 49:
   animating something a gradient/filter depends on forces a real per-frame
   repaint of every element using it, which a fast desktop GPU hides
   completely and a weaker device's GPU visibly cannot keep up with. A
   static, non-animated fallback for this exact styling already existed
   below for prefers-reduced-motion -- promoted it to the permanent default
   instead of writing a new one, so this is a pure removal, not a redesign.
   If this turns out not to be the whole story, the git history has the
   animated version to restore/compare against. */
.btn.beveled{
  position:relative;
  clip-path:polygon(7px 0,100% 0,100% calc(100% - 7px),calc(100% - 7px) 100%,0 100%,0 7px);
  border:1.5px solid transparent;
  background:
    linear-gradient(rgba(13,19,28,.92),rgba(13,19,28,.92)) padding-box,
    linear-gradient(90deg,var(--cyan),var(--border)) border-box;
  text-shadow:0 0 5px rgba(0,229,255,.3);
}
.btn.beveled:hover{border-color:transparent;
  background:
    linear-gradient(rgba(19,27,39,.92),rgba(19,27,39,.92)) padding-box,
    linear-gradient(90deg,var(--cyan),var(--border)) border-box;
}
.btn.beveled.on,.btn.beveled.primary{
  color:var(--gold);text-shadow:0 0 7px rgba(240,165,0,.55);
  background:
    linear-gradient(rgba(240,165,0,.10),rgba(240,165,0,.10)) padding-box,
    linear-gradient(90deg,var(--gold),var(--border)) border-box;
}
input[type=text]{font-family:'Orbitron',monospace;font-size:11px;letter-spacing:.12em;
  background:#04070c;border:1px solid var(--border);color:var(--cyan);
  padding:6px 8px;border-radius:var(--r-sm);width:140px;text-transform:uppercase;outline:none}
input[type=text]:focus{border-color:var(--gold)}
#addrGrp{display:flex;flex-direction:column;gap:2px}
/* Width pinned to #inAddr's own rendered box (140px content + 8px*2
   padding + 1px*2 border = 158px) so the centered text sits over just
   the HEX ADDRESS input, not the whole addr-row (which also includes
   Jump/Clear and would otherwise stretch this label wider, per Tony's
   screenshot -- centered on the row, not the box). */
.addr-lb{font-size:8px;letter-spacing:.14em;color:var(--text-faint);text-transform:uppercase;text-align:center;width:158px}
.addr-row{display:flex;gap:4px;align-items:center}
#panel.panel-min>*:not(.phead){display:none}
/* Enlarged 2026-09-12 (Tony's original report: minimize and drag were "too
   close, very little difference" to reliably pick apart) -- the drag handle
   is back to being the whole header (see makeDraggable() call for #panel),
   which already can't be dragged from a click that lands on this button
   (makeDraggable ignores clicks on any <button>); a bigger button is what
   actually makes it easy to land on that exclusion in the first place. */
.phead #bPanelMin{margin-left:auto;flex:none;padding:7px 13px;font-size:13px}
select{font-family:'Rajdhani',sans-serif;font-size:12px;background:#04070c;color:var(--text);
  border:1px solid var(--border);border-radius:var(--r-sm);padding:5px 6px;outline:none;width:100%}
#galSel{width:auto;max-width:168px;font-family:'Orbitron',sans-serif;font-size:10px;
  letter-spacing:.06em;color:var(--gold)}
/* Cosmetic rebuild 2026-08-17 (Tony): Economy/Economy strength/Conflict in
   Edit system used to be plain native <select> elements -- no browser can
   render an icon inside an <option>, so getting the same per-choice icons
   Tony wanted (matching the info panel's own race/economy/conflict icons)
   meant swapping all 3 for a small custom pop-open list instead. Anchored
   under its own field (position:relative wrapper) rather than floating
   over the whole viewport like #searchPop/#accessPop, since this lives
   inside the already-scrollable Edit system modal. */
.icombo{position:relative}
.icomboList{position:absolute;left:0;right:0;top:100%;margin-top:4px;z-index:20;
  background:rgba(11,17,25,.98);border:1px solid var(--border);border-radius:var(--r-sm);
  max-height:200px;overflow-y:auto;display:none;padding:4px 0}
.icomboList.show{display:block}
.icomboGrp{font-size:9px;letter-spacing:.12em;color:var(--text-faint);text-transform:uppercase;
  padding:6px 10px 3px}
.icomboOpt{display:flex;align-items:center;gap:8px;padding:5px 10px;cursor:pointer;
  font-size:11.5px;color:var(--text)}
.icomboOpt:hover{background:rgba(0,229,255,0.08)}
.icomboOpt svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-linecap:round;
  stroke-linejoin:round;flex:none}
.icomboOpt .icimg{width:15px;height:15px;object-fit:contain;flex:none}
.icomboOpt .icimg.tint{width:13px;height:13px}
.icomboEmpty{padding:8px 10px;color:var(--text-faint);font-size:10.5px}

/* Telemetry and Rendering tuning (below) are debug/power-user tools, not
   meant for regular visitors -- per Tony's own call, both are hidden
   entirely by default and only appear once the existing hidden Ctrl+Shift+F
   debug hotkey (toggleStats(), see keydown handler) has been used, the same
   gate the FPS counter already lives behind. html.debug-on is added there. */
#tel{display:none;width:190px;padding:10px 12px;font-family:'Orbitron',sans-serif;
  font-size:10px;letter-spacing:.08em;line-height:1.75}
html.debug-on #tel{display:block}
#tel b{color:var(--cyan);font-weight:600}
#gType{font-size:9px;margin-top:4px}
#tCore{font-size:9px;color:var(--text-faint);margin-top:2px}

#leftcol{position:fixed;left:10px;bottom:10px;z-index:25;display:flex;gap:8px;align-items:flex-end}
/* Filters escapes #leftcol's shared stacking cap while open (Tony,
   2026-08-24 follow-up: dragged it clear of the toolbar per the fix
   above, then got stuck behind the "GALAXY #N OF N" badge instead --
   "can the filter be the only one that is always on top of everything
   when opened"). Root cause the toolbar-clamp fix above couldn't reach:
   #filt is a DOM descendant of #leftcol, and #leftcol is itself a
   stacking context (position:fixed + z-index:25) -- no z-index on
   #filt itself can ever out-rank something OUTSIDE #leftcol with a
   higher z-index (like #galInfo's 39), because a nested stacking
   context never lets a descendant's own z-index compete past its own
   ancestor's rank in the OUTER context -- that's inherent to how CSS
   stacking works, not a value that could be raised away. The fix has
   to raise the ANCESTOR'S rank instead: bumping #leftcol's own
   z-index, but only while #filt is actually open (:has() keys off
   #filt:not(.min), so Telemetry and a collapsed Filters pill are both
   unaffected). 45 clears every other floating HUD/toolbar element in
   this file (the highest short of this is #toast/#hyperPanel at 40)
   while staying below real modals (#err 60, #modalWrap 80, the tour
   overlay, the warp transition) so those can still cover Filters if
   they ever appear at the same time. */
#leftcol:has(#filt:not(.min)){z-index:45}
#tweak{display:none;width:100%;padding:8px 10px;box-sizing:border-box}
html.debug-on #tweak{display:block}
/* Rendering tuning expands SIDEWAYS out of #accessPop instead of downward in
   its own flow -- with 9+ sliders, expanding in place pushed the whole
   accessibility popup taller than the viewport with no way to scroll down to
   it or reach anything below (Tony's screenshot). position:absolute anchored
   to #accessPop itself (its nearest positioned ancestor, being position:fixed)
   rather than position:fixed with a one-off computed offset, so it
   automatically travels with the popup if Tony drags it afterward -- no extra
   tracking needed. Side (.sideL/.sideR) is chosen once per open by
   positionTweakPanel(), based on real available viewport space. */
#tweak.sideR,#tweak.sideL{position:absolute;top:0;width:190px;
  max-height:calc(100vh - 40px);overflow-y:auto;overflow-x:hidden;z-index:1}
#tweak.sideR{left:100%;margin-left:8px}
#tweak.sideL{right:100%;margin-right:8px}
#filt{width:176px;padding:10px 12px;position:relative}
/* Desktop counterpart to the mobile #filt .fbody max-height rule further down
   (site review 2026-09-13: "Opening Filters in Local (galaxy) view renders
   the panel directly on top of the top-left Galactic Core distance card").
   Root cause: #leftcol is bottom:10px-anchored on desktop and Filters simply
   grows upward with however many filter rows are showing -- on a short-ish
   browser window (confirmed live at 1536x695) that growth reaches past
   #galHud's own top:calc(var(--top-h,60px)+10px) position, and #leftcol gets
   bumped to z-index:45 while Filters is open (see that rule's own comment)
   specifically so Filters stays usable on top of everything -- which is
   exactly what was clipping galHud's two text lines instead of just tucking
   Filters beside them. The mobile media query below already solves this the
   same way for its own (top-anchored) layout; this is that same fix for the
   bottom-anchored desktop one -- cap .fbody and let it scroll internally
   rather than letting the whole panel grow into galHud's space. Budget:
   var(--top-h) + galHud's worst-case bottom edge (.withBadge pushes its
   top down another 36px, so top-h+46+84px tall) + a visible gap (20px) +
   #leftcol's own bottom offset (10px) + #filt's own header row/padding
   above .fbody (~52px, measured live) -- verified live against the real
   site at 1536x695 (a common short-window size) with both the normal and
   .withBadge states before shipping this number. */
#filt .fbody{max-height:calc(100vh - var(--top-h,60px) - 222px);overflow-y:auto;overflow-x:hidden}
/* Filters header (2026-08-24): needs its own flex layout (unlike the
   shared .fhead class other fold-headers use unmodified) because it now
   holds a label span *and* a dedicated #bFiltMin button side by side --
   see the #bFiltMin wiring comment near toggleFold() for why. */
#fHead{display:flex;align-items:center;justify-content:space-between;gap:6px}
#fHeadLabel{flex:1;min-width:0}
#fHead #bFiltMin{flex:none;padding:3px 8px;font-size:11px}
/* Expands SIDEWAYS out of #filt, same proven mechanism #tweak already uses
   out of #accessPop -- position:absolute anchored to the nearest positioned
   ancestor (#filt, position:relative), NOT position:fixed. History: v1
   (.sideL/.sideR with no max-height) let the panel run past the bottom of
   a short viewport; v2 (position:fixed anchored to the button) fixed that
   but opened the panel BELOW the button instead of beside Filters; v3
   (2026-08-24, position:fixed anchored to #filt's own rect, computed in JS)
   fixed mobile visibility but broke the moment #filt moved for any reason
   other than the one drag-hook it had wired -- Tony: "as move right the
   warp drive window gap increases till disappears off screen... worked
   absolutely fine before". Root cause of v3's regression: JS-computed
   position:fixed has to be told about every single way #filt can move
   (drag, resize, rotate, zoom); position:absolute needs telling about NONE
   of them -- it just re-renders relative to its ancestor's current spot on
   every paint, automatically, forever. v3 only reached for position:fixed
   in the first place because #hyperPanel used to be a DOM descendant of
   #filt's own mobile-only overflow:hidden .fbody, which clipped it
   invisible out there. Real fix (this version): #hyperPanel now lives
   OUTSIDE .fbody (still inside #filt -- see the markup) so it's no longer
   inside anything that clips it, which means going back to plain
   position:absolute -- the original v1 mechanism -- finally works
   correctly on both counts at once: never clipped, and never needs any
   JS repositioning call for ANY way #filt can move, not just a drag. */
#hyperPanel{display:none}
#hyperPanel.show{display:block}
#hyperPanel.sideR,#hyperPanel.sideL{position:absolute;top:0;width:198px;padding:10px 12px;
  max-height:calc(100vh - 40px);overflow-y:hidden;overflow-x:hidden;z-index:40}
#hyperPanel.sideR{left:100%;margin-left:8px}
#hyperPanel.sideL{right:100%;margin-right:8px}
/* .scroll: only added by positionHyperPanel() once it's actually measured
   real overflow against max-height. overflow-y:auto by default here (like
   #tweak has) shows a 1px phantom scrollbar on some viewports even when
   content visibly fits -- a sub-pixel scrollHeight-vs-clientHeight rounding
   quirk, confirmed live. Measuring in JS and only opting into scroll when
   truly needed sidesteps that rounding entirely instead of guessing at a
   CSS fudge factor. */
#hyperPanel.scroll{overflow-y:auto}
#hyperSummary{color:var(--gold);font-family:'Orbitron',sans-serif;font-size:10px;letter-spacing:.04em}
.tag.wp{border-color:#ffb454;color:#ffb454}
.tag.ruin{border-color:#c9a06a;color:#c9a06a}
.tag.noreach{border-color:#ff5a4a;color:#ff5a4a}

.fold.min .fbody{display:none}
.fold.min{width:auto}
/* The floating minimized "▸ Filters" pill duplicates the toolbar's own
   Filters button (#bFiltToggle, inside the collapsible controls section)
   one-for-one -- two ways to open the same panel. First scoped to mobile
   only (2026-08-31), then Tony confirmed he wants it gone everywhere, not
   just on phones -- it's redundant on desktop too, same toolbar button
   sits right there. Only hides it while minimized (.min); an OPEN Filters
   panel still renders normally on every width. */
#filt.min{display:none}
.fhead{cursor:pointer;user-select:none;white-space:nowrap}
.fbody{margin-top:8px}
.sl{margin-bottom:8px}
.sl label{display:flex;justify-content:space-between;font-size:9.5px;letter-spacing:.1em;
  color:var(--text-faint);text-transform:uppercase;margin-bottom:3px}
.sl label b{color:var(--cyan);font-weight:600;font-family:'Orbitron',sans-serif}
input[type=range]{width:100%;height:4px;-webkit-appearance:none;appearance:none;
  background:rgba(0,229,255,.18);border-radius:2px;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;
  border-radius:50%;background:var(--cyan);cursor:pointer;box-shadow:0 0 8px rgba(0,229,255,.7)}
input[type=range]::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:var(--cyan)}
.chk{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--text-dim);
  margin-bottom:4px;cursor:pointer;letter-spacing:.04em}
.chk input{accent-color:#00e5ff;width:13px;height:13px}
.fld{margin-bottom:7px}
.fld .lb{font-size:9px;letter-spacing:.12em;color:var(--text-faint);text-transform:uppercase;margin-bottom:3px}

/* ============ accessibility ============
   Font size uses zoom (not transform or a rem refactor) on each floating
   UI box -- same technique the Phone Preview Tool already relies on for its
   own "Fit to screen" toggle, chosen there specifically because it avoids a
   touch-routing bug transform has on real touchscreens, and it keeps each
   box's fixed top/right anchor point stable while the box grows/shrinks from
   that corner instead of reflowing. Deliberately scoped to UI chrome only --
   the 3D canvas and in-scene star/planet labels are a separate system with
   their own pixel-math collision-avoidance and aren't touched here.
   Colour-blind filters apply to the whole page (html), including the
   canvas, via SVG feColorMatrix -- a colourblind player needs the actual
   star colours corrected too, not just the UI text. (Invert was in the
   original spec too but Tony dropped it after trying it live -- it made
   the whole map, including the cyan/gold accent colours that carry real
   meaning, unreadable rather than more readable.)
   High Contrast reuses the existing --text-*/--border-* variable system
   instead of a filter, so it boosts real contrast without touching the
   cyan/gold accent colours that already carry meaning (selection, warnings). */
#bAccess{width:30px;height:30px;padding:0;border-radius:50%;display:flex;
  align-items:center;justify-content:center;font-size:14px;flex:none}
/* 10th-anniversary fan tribute badge (2026-08-23). Tony's ask: a small,
   ALWAYS-visible marker -- deliberately unlike #galInfo, which only shows
   in Galaxy view. There is no `mode` branching anywhere on this element on
   purpose, so it never hides/moves between Galaxy, Local or System. Not a
   flex child of #top (so it can't disturb the toolbar's own wrap layout);
   instead it's positioned by JS measuring #bAccess's real rendered rect
   (positionAnnivBadge(), same measure-don't-guess pattern as
   positionUnderButton() below), so it tracks the button correctly on any
   window width and after any toolbar re-wrap. Real badge art is taller
   than wide (1336x1536 source crop), so the box keeps that ratio rather
   than forcing a square and distorting it. */
#annivBadge{position:fixed;z-index:31;width:26px;height:30px;pointer-events:none;
  transition:left .15s,top .15s}
#annivBadge img{width:100%;height:100%;display:block;
  filter:drop-shadow(0 0 5px rgba(240,90,40,.4))}
#top,#leftcol,#tel,#panel,#course,#modalWrap .modal,#galHud,#accessPop,#searchPop,#routesPop,#favPop,#keys{
  zoom:var(--ui-zoom,1)}
#accessPop{position:fixed;z-index:36;width:198px;padding:11px 13px;
  top:calc(var(--top-h,60px) + 8px);right:10px;display:none}
#accessPop.show{display:block}
#searchPop{position:fixed;z-index:36;width:260px;padding:11px 13px;
  top:calc(var(--top-h,60px) + 8px);right:10px;display:none;
  max-height:60vh;overflow-y:hidden;overflow-x:hidden}
/* overflow-x:hidden above is the actual fix for the phantom horizontal
   scrollbar Tony flagged (2026-08-22): per the CSS spec, setting overflow-y
   to anything other than "visible" while leaving overflow-x unset forces
   the browser to compute overflow-x as "auto" instead of "visible" -- so
   any sub-pixel horizontal rounding (the same zoom-driven drift already
   diagnosed for the vertical axis below) was enough to trigger a real,
   visible horizontal scrollbar even though content fit. #hyperPanel and
   #tweak already set overflow-x:hidden explicitly for this exact reason;
   #searchPop was the one box built without it, which is why it was the
   only one showing the bar. */
#searchPop.show{display:block}
/* #searchPop.scroll: only added once real overflow is measured in JS, same
   fix as #hyperPanel/#tweak/hyperdriveModal -- overflow-y:auto by default
   shows a themed (very visible) scrollbar even when content visibly fits,
   a sub-pixel scrollHeight-vs-clientHeight rounding quirk, not a real
   overflow. Measuring after layout sidesteps it instead of guessing at a
   CSS fudge factor. */
#searchPop.scroll{overflow-y:auto}
#inSearch{width:100%;box-sizing:border-box;margin-bottom:8px}
.searchRes{padding:6px 4px;border-top:1px solid var(--border-soft);cursor:pointer}
.searchRes:first-child{border-top:none}
.searchRes:hover{background:rgba(0,229,255,0.06)}
.searchResName{color:var(--gold);font-size:11px;letter-spacing:.04em}
.searchResMeta{color:var(--text-dim);font-size:9px;letter-spacing:.04em;margin-top:1px}
/* #routesPop: same popover box as #searchPop, just wide enough for a row's
   action buttons; #inRouteLink reuses #inSearch's own input styling via the
   shared input rule elsewhere in this file, no separate rule needed. Rows
   aren't a single click target like .searchRes (each has its own Load/
   Link/Rename/Delete buttons instead), so cursor stays default there. */
#routesPop{position:fixed;z-index:36;width:300px;padding:11px 13px;
  top:calc(var(--top-h,60px) + 8px);right:10px;display:none;
  max-height:60vh;overflow-y:hidden;overflow-x:hidden}
#routesPop.show{display:block}
#routesPop.scroll{overflow-y:auto}
#inRouteLink{box-sizing:border-box}
/* #favPop: same popover box as #routesPop, same reasons (see its own HTML
   comment) -- width matches #routesPop since favourite rows carry a Remove
   button same as a route row's action row. */
#favPop{position:fixed;z-index:36;width:300px;padding:11px 13px;
  top:calc(var(--top-h,60px) + 8px);right:10px;display:none;
  max-height:60vh;overflow-y:hidden;overflow-x:hidden}
#favPop.show{display:block}
#favPop.scroll{overflow-y:auto}
.routeRes{cursor:default}
.routeRowActions{display:flex;gap:5px;margin-top:5px;flex-wrap:wrap}
.routeActBtn{background:transparent;border:1px solid var(--border-soft);color:var(--text-dim);
  font-size:9px;letter-spacing:.05em;padding:3px 7px;border-radius:4px;cursor:pointer;
  font-family:inherit}
.routeActBtn:hover{border-color:var(--cyan);color:var(--cyan)}
.routeActBtn.danger:hover{border-color:#ff5a4a;color:#ff5a4a}
.searchEmpty{color:var(--text-faint);font-size:10px;line-height:1.5;padding:4px 0}
/* Find scan result cards (2026-09-12) -- separate from .searchRes (the
   plain by-name/documented rows just above) because these need a "Plan
   journey" sub-row that only appears once a card is the active/just-clicked
   one, which .searchRes has no room or hook for. */
.findCard{border-radius:6px;border:1px solid var(--border-soft);margin-bottom:7px;padding:9px;cursor:pointer}
.findCard:hover{border-color:var(--cyan-dim);background:var(--panel-2)}
.findCard .top{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
.findCard .name{font-size:13px;color:var(--white)}
.findCard .meta{font-size:10.5px;color:var(--text-dim);margin-top:2px}
.findCard .dist{font-size:11.5px;color:var(--gold);text-align:right;white-space:nowrap}
.findCard .dist small{display:block;font-size:9px;color:var(--text-faint);font-weight:normal}
/* 2026-09-11: header block above an alliance's grouped results (see
   renderMatches) -- Orbitron/cyan/uppercase to match the site's other
   small section labels (e.g. .edScanlabel) rather than inventing a new
   look for it. */
.searchAllianceHead{font-family:'Orbitron',sans-serif;font-size:9px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--cyan);padding:6px 4px 3px;border-top:1px solid var(--border-soft)}
.searchAllianceHead:first-child{border-top:none;padding-top:0}
.pillrow{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px}
.pill{font-family:'Orbitron',sans-serif;font-size:8.5px;letter-spacing:.06em;
  padding:5px 7px;border-radius:var(--r-sm);border:1px solid var(--border);
  color:var(--text-dim);background:transparent;cursor:pointer;text-transform:uppercase}
.pill.on{border-color:var(--gold);color:var(--gold);background:rgba(240,165,0,.08)}
.pill[disabled]{opacity:.35;cursor:default}
html.hc-on{--text-dim:#ffffff;--text-faint:#e6e6e6;--border:#ffffff;--border-soft:#ffffff}
html.hc-on .box{background:#000;border-width:2px}
html.hc-on .box::before,html.hc-on .box::after{border-color:#fff}
html.hc-on .btn{border-width:2px}

#panel{position:fixed;top:50px;right:10px;width:278px;z-index:25;padding:13px 14px;
  display:none;max-height:calc(100vh - 70px);overflow-y:auto;overflow-x:hidden}
#panel.show{display:block}
.phead{display:flex;gap:10px;align-items:flex-start;margin-bottom:8px}
.stars{display:flex;gap:4px;flex:none;padding-top:2px}
.starsw{width:20px;height:20px;border-radius:50%}
#panel h2{font-family:'Orbitron',sans-serif;font-size:13px;font-weight:600;color:var(--white);line-height:1.25}
#panel .reg{font-size:11px;letter-spacing:.1em;color:var(--text-faint);text-transform:uppercase}
#panel .cls{font-family:'Orbitron',sans-serif;font-size:11px;color:var(--gold);letter-spacing:.06em;margin-bottom:3px}
#pCore{font-size:10.5px;color:var(--text-faint);letter-spacing:.04em;margin-bottom:10px}
.row{display:flex;gap:9px;align-items:flex-start;margin-bottom:9px}
/* Tighter gap specifically after the Economy row -- its icon+star column
   runs taller than its own text at higher star tiers, which was pushing
   the Conflict row down with a visible dead gap (Tony flagged via a real
   screenshot vs. the game's own tighter layout). */
/* .row-eco tight-margin override removed 2026-08-18 -- only needed while
   the star rating lived in a tall vertical iconstack column; now it is a
   single inline line next to the "Economy" label, so this row is back to
   the default .row margin-bottom like every other row (Race/Conflict). */
.row svg{width:16px;height:16px;flex:none;margin-top:2px;color:var(--cyan);
  stroke:currentColor;fill:none;stroke-linecap:round;stroke-linejoin:round}
.row .icimg{width:26px;height:26px;flex:none;margin-top:-2px;object-fit:contain}
.row .icimg.tint{width:23px;height:23px}
.row .lb{font-size:9.5px;letter-spacing:.12em;color:var(--text-faint);text-transform:uppercase}
.row .vl{font-size:14px;color:var(--text);line-height:1.3}
.row .sub{font-size:11px;color:var(--text-dim);margin-top:1px}
.tags{display:flex;flex-wrap:wrap;gap:5px;border-top:1px solid var(--border-soft);padding-top:9px;margin-top:3px}
.tag{font-family:'Orbitron',sans-serif;font-size:8.5px;letter-spacing:.1em;padding:3px 7px;
  border-radius:var(--r-sm);border:1px solid var(--border);color:var(--text-dim);text-transform:uppercase}
.tag.hot{border-color:var(--gold);color:var(--gold)}
.tag.bh{border-color:#c78aff;color:#c78aff}
.tag.ghost{border-color:#8892a6;color:#8892a6;border-style:dashed}
/* Icon + its rating/badge stacked vertically underneath, matching the real
   game's own economy panel (Tony's reference screenshot) rather than the
   badge floating off at the far right of the row like it was before. */
.starRating{display:inline-flex;flex-direction:row;gap:2px;align-items:center;vertical-align:middle;margin-left:6px}
.conBadge{display:inline-flex;align-items:center;gap:4px;vertical-align:middle}
.conBadge b{font-family:'Orbitron',sans-serif;background:#2a0d0d;color:#ff8a80;font-size:10px;border-radius:8px;padding:1px 6px}
.bodylist{margin-top:9px;border-top:1px solid var(--border-soft);padding-top:8px}
.brow{display:flex;align-items:center;gap:7px;font-size:12px;padding:3px 0;cursor:pointer;border-radius:4px;transition:background .6s ease,box-shadow .6s ease}
.brow.isMoon{margin-left:16px}
.brow:hover{color:var(--white)}
/* Search-result-matched row highlight, added 2026-08-21 -- brief fade rather
   than a looping keyframe, since it only needs to draw the eye once right
   after a body/base search result is clicked, not keep pulsing. Cleared via
   a plain class removal after ~2.4s (see the search click handler). */
.brow.searchHit{background:rgba(240,165,0,.16);box-shadow:0 0 0 1px rgba(240,165,0,.45) inset}
.bdot{width:12px;height:12px;border-radius:50%;flex:none;border:1px solid rgba(255,255,255,.22)}
/* 2026-08-23 fix: .bn (name) has flex-basis:0 (from "flex:1"), so when .bb
   (biome/water/ring text) runs long -- e.g. "Radioactive . water . golden
   rings (procedural)" -- it was free to claim effectively the whole row,
   squeezing .bn down to a sliver (a couple of clipped pixels of the name's
   first letter, easy to mistake for a stray "[" glyph -- not a generator
   bug, a pure layout one, per Tony/GoodGuysFree's own screenshot). Fix:
   give .bn a floor it can't be squeezed below, and give .bb the exact same
   nowrap/ellipsis truncation .bn already had so IT degrades gracefully
   too instead of just pushing the name out of the row. */
.brow .bn{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:60px}
.brow .bb{font-size:10px;color:var(--text-faint);letter-spacing:.05em;flex:0 1 auto;max-width:50%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* ============ field-level edit status (EDIT-TRACKING-AND-DISPUTES.md) ============
   inset box-shadow rather than a real border -- keeps every row's box model
   and existing flex gaps exactly as they were, this is purely a colour cue
   down the left edge of whichever row/tag a category's status applies to. */
.fs-edited,.fs-flagged,.fs-disputed{padding-left:8px}
.fs-edited{box-shadow:inset 3px 0 0 var(--fs-edited)}
.fs-flagged{box-shadow:inset 3px 0 0 var(--fs-flagged)}
.fs-disputed{box-shadow:inset 3px 0 0 var(--fs-disputed)}
.tag.fs-edited{border-color:var(--fs-edited);color:var(--fs-edited);box-shadow:none;padding-left:7px}
.tag.fs-flagged{border-color:var(--fs-flagged);color:var(--fs-flagged);box-shadow:none;padding-left:7px}
/* ============ unverified "guess" fields (race/economy/conflict/rings) ============
   Added 2026-08-22 per Tony/GoodGuysFree: these four used to be entirely
   invented flavour (see nms-core/economy.js's own pre-2026-08-23 header) --
   not derived from the portal address the way name/region/planet-count are
   -- so unlike a plain fs-original field (which just means "nobody's
   edited this yet" on data that's already accurate), these needed to
   visibly read as unconfirmed by default, not just in the About modal.
   Deliberately its OWN class rather than reusing fs-original: fs-original
   also covers name/region/starClass/planet-count, which ARE the real
   ported algorithm and should NOT be greyed out or tagged "Procedural"
   just because nobody's edited them yet.

   2026-08-23 update: race/economy/conflict are now ALSO the real ported
   algorithm (nms-core/system.js's systemAttributes(), same footing as
   starClass/planet-count above) whenever a portal address is available --
   see generateSystem()'s flavorIsReal / s.raceReal/econReal/conflictReal.
   This class/tag now only actually fires for those three in the rare
   fallback case (no portal address, or a component failed to load) -- see
   updatePanel()'s raceIsGuess/econIsGuess/conIsGuess. Rings used to be the
   one field here still genuinely, unconditionally invented (nms_namegen
   never modelled planet rings at all) and got the dashed .tag.guess
   treatment for it -- but 2026-09-09 (Tony: rings/water should get the
   same treatment biome already has) moved rings (and water, alongside it)
   to the stricter Biome pattern instead: simply omitted from display
   entirely until a traveller confirms via Edit system (b.bodyOverridden),
   rather than shown with a dashed "guess" tag. .guessVal/.tag.guess now
   only ever fire for race/economy/conflict's rare fallback case above. */
.guessVal{opacity:.55}
.tag.guess{border-color:var(--text-faint);color:var(--text-faint);border-style:dashed}
.tag.fs-disputed{border-color:var(--fs-disputed);color:var(--fs-disputed);box-shadow:none;padding-left:7px}
/* 2026-09-13: collapsible sub-sections inside #panel (see the HTML comment
   just above #pSeqHead) -- deliberately plain text + arrow, not a boxed
   .btn, so these read as section labels you *can* click rather than
   actions sitting in the same visual rank as Enter System/Bookmark/etc. */
.panelFoldHead{cursor:pointer;user-select:none;margin-top:10px;font-size:9px;
  letter-spacing:.12em;color:var(--text-faint);text-transform:uppercase}
.panelFoldHead:hover{color:var(--cyan)}
.seqbox{border:1px solid var(--border-soft);border-radius:var(--r-sm);padding:7px;margin-top:6px;background:#04070c}
.seqbox .lb{font-size:9px;letter-spacing:.16em;color:var(--gold);text-align:center;margin-bottom:5px}
.seq{display:grid;grid-template-columns:repeat(12,1fr);gap:2px}
.seq img{width:100%;filter:drop-shadow(0 0 4px rgba(0,229,255,.5))}
#pAddr{font-family:'Orbitron',monospace;font-size:10px;color:var(--cyan);letter-spacing:.14em;
  text-align:center;margin-top:6px}
.seqbox .lb.pCoordsLb{margin-top:8px;margin-bottom:3px}
#pCoords{font-family:'Orbitron',monospace;font-size:10px;color:var(--text-dim);letter-spacing:.1em;
  text-align:center}
textarea{width:100%;background:#04070c;border:1px solid var(--border);color:var(--text);
  border-radius:var(--r-sm);padding:6px 7px;font-family:'Rajdhani',sans-serif;font-size:12px;
  resize:vertical;min-height:50px;outline:none;margin-top:8px}
.prow{display:flex;gap:5px;margin-top:6px}
.prow .btn{flex:1;text-align:center}
/* 2026-08-26, Tony: "PLOTTED COURSE" button clipped -- .prow rows share width
   50/50 between two buttons with white-space:nowrap inherited from .btn, and
   that label is longer than every other .prow button text ever was. Rather
   than shrink font/padding (fragile against the accessibility zoom feature),
   let a long label wrap onto a 2nd line -- still centred, still fits inside
   the button's own box, no clipping regardless of label length or zoom. */
.prow .btn{white-space:normal;line-height:1.25}

#course{position:fixed;left:50%;transform:translateX(-50%);top:calc(var(--top-h,60px) + 8px);z-index:27;
  width:246px;padding:10px 12px;display:none}
#course.show{display:block}
#course .cline{display:flex;justify-content:space-between;font-size:11.5px;padding:2px 0}
#course #cJumpsNote{display:none;font-size:9.5px;color:var(--text-faint);line-height:1.35;margin:-2px 0 4px}
#course .cline span:first-child{color:var(--text-faint)}
/* letter-spacing added 2026-08-16: every other Orbitron stat readout in the
   app (#hyperSummary, #pCore, etc.) sets some, this one didn't -- at this
   size/font, tight digit pairs like "17" can visually fuse into what Tony
   correctly read as looking like a single merged glyph. */
#course .cline b{color:var(--cyan);font-family:'Orbitron',sans-serif;font-size:10.5px;letter-spacing:.04em}
#cTarget{font-family:'Orbitron',sans-serif;font-size:11px;color:var(--white);margin:5px 0 6px}
#course .chead{display:flex;align-items:center;gap:6px}
#course .chead>div{flex:1;min-width:0}
#course .chead #bToggleManifest{margin-left:auto;flex:none;padding:4px 8px;font-size:10.5px}
#course .chead #bCourseMin{flex:none;padding:4px 9px;font-size:12px}
/* Collapsed state: hide everything below the header row (distance/jumps/bearing/
   target-from-centre/buttons) so the box shrinks to one line instead of a tall
   block sitting over the star field -- Tony's "stays so can not see jumps" report.
   Stays anchored in the same top-centre spot rather than relocating to the bottom,
   since on mobile the bottom is already taken by the info panel's own sticky
   Enter system/Set course row -- moving it there would just recreate the same
   overlap problem this session's other fixes were about. */
#course.min #cBody{display:none}
#course.min{width:auto}
#route-itinerary-card.min #riBody{display:none}
#route-itinerary-card.min{width:auto}
/* Edit system, made draggable/collapsible like Filters/Course/Manifest
   (Tony, 2026-08-17) -- .edhead is the drag handle (title + collapse
   button), #edBody holds everything else so collapsing just hides the
   long form and leaves the header visible, same pattern as #course/
   #route-itinerary-card above. */
.edhead{display:flex;align-items:flex-start;gap:8px;margin-bottom:2px}
.edhead>div{flex:1;min-width:0}
.edhead #bEditMin{margin-left:auto;flex:none;padding:4px 9px;font-size:12px}
#editModal.min #edBody{display:none}
#editModal.min{width:auto}

#keys{position:fixed;left:50%;transform:translateX(-50%);bottom:10px;z-index:26;
  width:min(560px,calc(100vw - 20px));padding:10px 12px;display:none}
#keys.show{display:block}
/* Searchable galaxy picker docked next to the keypad's own title (2026-08-22,
   real per-galaxy addressing). Reuses the same native
   <datalist> autocomplete pattern this file already relies on elsewhere
   (populateDatalists()) rather than a custom dropdown widget. */
.kheadrow{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
.kgalpick{display:flex;align-items:center;gap:5px;text-transform:none}
.kgalpick label{font-family:'Rajdhani',sans-serif;font-size:9px;letter-spacing:.08em;
  color:var(--text-faint);text-transform:uppercase;white-space:nowrap}
#kGalaxyInput{width:150px;font-family:'Rajdhani',sans-serif;font-size:11px;
  letter-spacing:.02em;text-transform:none}
.kgrid{display:grid;grid-template-columns:repeat(16,1fr);gap:3px;margin-top:7px}
.kbtn{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:3px;cursor:pointer;line-height:0}
.kbtn:hover{border-color:var(--gold);background:var(--panel-2)}
.kbtn img{width:100%;filter:drop-shadow(0 0 4px rgba(0,229,255,.55))}
.kseq{display:grid;grid-template-columns:repeat(12,1fr);gap:3px;margin-top:8px;
  background:#04070c;border:1px solid var(--border-soft);border-radius:var(--r-sm);padding:5px}
.kslot{aspect-ratio:1;border:1px dashed rgba(0,229,255,.14);border-radius:4px;line-height:0}
.kslot img{width:100%;filter:drop-shadow(0 0 5px rgba(240,165,0,.6))}
.krow{display:flex;gap:5px;margin-top:8px;align-items:center;flex-wrap:wrap}
#kHex{font-family:'Orbitron',monospace;font-size:11px;color:var(--gold);letter-spacing:.16em;flex:1;min-width:110px}

#stats{position:fixed;right:10px;bottom:10px;z-index:25;font-family:'Orbitron',sans-serif;
  font-size:9.5px;letter-spacing:.1em;color:var(--text-faint);text-align:right;line-height:1.7;
  display:none}
#stats.show{display:block}
#stats b{color:var(--cyan);font-weight:600}
#labels{position:fixed;inset:0;z-index:15;pointer-events:none;overflow:hidden}
#atlasBadges{position:fixed;inset:0;z-index:15;pointer-events:none;overflow:hidden}
.atlasBadge{position:absolute;transform:translate(-50%,-50%) rotate(45deg);width:22px;height:22px;border:1.6px solid var(--cyan);background:rgba(4,10,20,0.85);box-shadow:0 0 8px rgba(0,229,255,.5);pointer-events:auto;cursor:pointer;align-items:center;justify-content:center}
.atlasBadge span{transform:rotate(-45deg);font-family:'Orbitron',sans-serif;font-size:10px;color:var(--cyan);font-weight:700}
.lbl{position:absolute;font-family:'Orbitron',sans-serif;font-size:calc(9.5px * var(--ui-zoom,1));letter-spacing:.1em;
  color:var(--white);text-shadow:0 1px 2px #000,0 -1px 2px #000,1px 0 2px #000,-1px 0 2px #000,0 0 6px #000;
  white-space:nowrap;transform:translate(-50%,-150%);opacity:.92;
  background:rgba(4,6,10,0.55);padding:2px 6px;border-radius:4px}
.hoverpop{position:fixed;z-index:36;pointer-events:none;font-family:'Orbitron',sans-serif;
  /* 2026-08-25, Tony: this popup is pinned centred-above the cursor with no
     awareness of what's underneath (the course ring/line included), and
     screenshots showed it sometimes burying the ring depending on luck of
     where the star happened to sit on screen. Dropping opacity from .95 to
     .82 -- cheap, fixes every overlap case regardless of direction, no new
     positioning logic -- lets the ring/line stay faintly visible through the
     card instead of vanishing behind it. Text/icons stay readable at .82. */
  background:rgba(11,17,25,.82);border:1px solid var(--border);border-radius:var(--r-sm);
  padding:6px 10px;transform:translate(-50%,calc(-100% - 14px));white-space:nowrap}
.hoverpop .hpName{color:var(--gold);font-size:12px;letter-spacing:.06em;margin:0 0 2px}
.hoverpop .hpCls{color:var(--cyan);font-size:10px;letter-spacing:.05em;margin:0 0 4px}
.hoverpop .hpIcons{display:flex;gap:10px;align-items:center;margin-top:2px}
.hoverpop .hpIcons svg{width:22px;height:22px;stroke:currentColor;fill:none;
  stroke-linecap:round;stroke-linejoin:round}
.hoverpop .hpIcons .icimg{width:24px;height:24px;object-fit:contain}
.hoverpop .hpIcons .icimg.tint{width:21px;height:21px}
.hoverpop .hpDetail{margin-top:4px}
.hoverpop .hpRow{display:flex;align-items:center;gap:7px;margin-top:4px}
.hoverpop .hpRow svg{width:16px;height:16px;stroke:currentColor;fill:none;
  stroke-linecap:round;stroke-linejoin:round;flex:none}
.hoverpop .hpRow .icimg{width:17px;height:17px;object-fit:contain;flex:none}
.hoverpop .hpRow .icimg.tint{width:15px;height:15px}
.hoverpop .hpLbl{color:var(--text);font-size:11px;letter-spacing:.03em}
.hoverpop .hpSub{color:var(--text-dim);font-size:9px;letter-spacing:.02em;margin:1px 0 0 23px}
/* System-name banner (2026-09-13, Phase 2) -- System view's own "you are
   here" label, shown only while mode==="system" (see setMode()). Fixed
   top-center, non-interactive (pointer-events:none) so it never blocks the
   toolbar or canvas clicks underneath it even if it happens to overlap on
   a narrow/mobile layout -- flagged to Tony as worth a live check there. */
#sysBanner{display:none;position:fixed;left:50%;transform:translateX(-50%);
  z-index:32;pointer-events:none;font-family:'Orbitron',sans-serif;color:var(--gold);
  font-size:13px;letter-spacing:.12em;text-transform:uppercase;
  background:rgba(11,17,25,.72);border:1px solid var(--border);border-radius:var(--r-sm);
  padding:5px 16px;text-shadow:0 1px 2px #000}
/* Tony (2026-08-17): the conBadge() pill reused for the hover popup's
   Conflict row is sized for the full info panel/dropdown (10px text, 8px
   radius) -- too bulky next to this popup's smaller 16px icons, so scoped
   down just here rather than shrinking it everywhere it's reused. */
.hoverpop .conBadge b{font-size:8px;padding:0 4px;border-radius:6px}
.hoverpop .conBadge{gap:3px}
.resreport{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);z-index:37;
  width:280px;font-family:'Orbitron',sans-serif;
  background:rgba(6,12,20,0.94);border:1px solid var(--border);border-radius:var(--r-sm);
  overflow:hidden;box-shadow:0 8px 28px rgba(0,0,0,0.5)}
.resreport .rrHead{background:rgba(107,255,168,0.14);border-bottom:1px solid rgba(107,255,168,0.35);
  padding:9px 30px 9px 12px;position:relative}
.resreport .rrName{color:var(--fs-edited);font-size:12px;letter-spacing:.05em;font-weight:600}
.resreport .rrCat{color:rgba(207,224,240,0.75);font-size:9px;letter-spacing:.04em;margin-top:2px;font-family:'Rajdhani',sans-serif}
.resreport .rrClose{position:absolute;right:6px;top:6px;width:20px;height:20px;border:none;
  background:transparent;color:var(--text-dim);font-size:16px;line-height:1;cursor:pointer;border-radius:4px}
.resreport .rrClose:hover{color:var(--white);background:rgba(255,255,255,0.08)}
.resreport .rrBody{padding:10px 12px;font-family:'Rajdhani',sans-serif;font-size:11px;color:var(--text)}
.resreport .rrRow{margin-bottom:6px;line-height:1.4}
.resreport .rrRow:last-child{margin-bottom:0}
.resreport .rrLbl{color:var(--cyan);letter-spacing:.03em;margin-right:4px}
.resreport .rrMarker{display:block;width:100%;background:rgba(0,229,255,0.08);border:none;
  border-top:1px solid var(--border);color:var(--cyan);font-family:'Orbitron',sans-serif;
  font-size:10px;letter-spacing:.1em;padding:9px;cursor:pointer;transition:background .15s}
.resreport .rrMarker:hover{background:rgba(0,229,255,0.16)}
#toast{position:fixed;left:50%;top:calc(var(--top-h,60px) + 8px);transform:translateX(-50%);z-index:40;
  font-family:'Orbitron',sans-serif;font-size:10px;letter-spacing:.12em;color:var(--gold);
  background:rgba(11,17,25,.95);border:1px solid var(--gold);border-radius:var(--r-sm);
  padding:7px 13px;display:none}
#galInfo{position:fixed;left:10px;top:calc(var(--top-h,60px) + 8px);z-index:39;
  font-family:'Orbitron',sans-serif;font-size:10px;letter-spacing:.12em;color:var(--gold);
  background:rgba(11,17,25,.95);border:1px solid var(--gold);border-radius:var(--r-sm);
  padding:7px 13px;display:none;pointer-events:none}
#galHud{position:fixed;left:10px;top:calc(var(--top-h,60px) + 10px);z-index:24;
  font-family:'Orbitron',sans-serif;pointer-events:none;color:var(--white);display:flex;
  align-items:center;gap:9px;transition:top .15s}
#galHud.withBadge{top:calc(var(--top-h,60px) + 46px)}
#galIcon{width:84px;height:84px;flex:none;display:block;pointer-events:auto;
  -webkit-user-select:none;user-select:none;-webkit-touch-callout:none}
#galHud div{white-space:nowrap}
#galHud .galHudTxt{text-shadow:0 1px 2px #000,0 -1px 2px #000,1px 0 2px #000,-1px 0 2px #000,0 0 6px #000}
#galHudName{font-size:11px;letter-spacing:.06em;opacity:.92}
#galHudDist{font-size:13px;letter-spacing:.04em;margin-top:1px}
#err{position:fixed;left:10px;right:10px;bottom:10px;z-index:60;background:#2a0d0d;
  border:1px solid #ff4a4a;color:#ffb0b0;border-radius:var(--r-sm);padding:9px 12px;
  font-family:monospace;font-size:11px;display:none;white-space:pre-wrap;max-height:30vh;overflow:auto}
#empty{position:fixed;left:50%;top:46%;transform:translate(-50%,-50%);z-index:18;display:none;
  font-family:'Orbitron',sans-serif;font-size:11px;letter-spacing:.16em;color:var(--gold);
  border:1px solid var(--gold);border-radius:var(--r-sm);padding:10px 16px;
  background:rgba(11,17,25,.9);pointer-events:none;text-transform:uppercase}
#flyhint{position:fixed;left:220px;bottom:10px;z-index:20;max-width:250px;
  font-family:'Orbitron',sans-serif;font-size:9px;letter-spacing:.06em;color:var(--white);
  text-align:left;pointer-events:none;display:none;line-height:1.9;
  background:rgba(11,17,25,.9);border:1px solid var(--cyan-dim);border-radius:var(--r-sm);
  padding:7px 10px;text-shadow:0 1px 2px rgba(0,0,0,.7)}
#flyhint b{color:var(--cyan);background:rgba(0,229,255,.12);border:1px solid var(--cyan-dim);
  border-radius:4px;padding:1px 5px;font-weight:700;letter-spacing:.04em;white-space:nowrap}
@media(max-width:900px){#flyhint{left:10px;bottom:64px}}
#joyBase{position:fixed;width:96px;height:96px;margin:-48px 0 0 -48px;border:2px solid var(--cyan-dim);
  border-radius:50%;background:rgba(0,229,255,.06);pointer-events:none;z-index:19;display:none}
#joyKnob{position:absolute;left:28px;top:28px;width:40px;height:40px;border-radius:50%;
  background:rgba(0,229,255,.3);border:1.5px solid var(--cyan);box-shadow:0 0 10px rgba(0,229,255,.65)}

/* ============ edit / report modal ============ */
#modalWrap{position:fixed;inset:0;z-index:80;display:none;background:rgba(2,4,7,.72);
  align-items:flex-start;justify-content:center;overflow-y:auto;overflow-x:hidden;padding:50px 12px}
#modalWrap.show{display:flex}
.modal{width:100%;max-width:420px;padding:16px 16px 14px;margin-bottom:40px;max-height:calc(100vh - 100px);overflow-y:auto;overflow-x:hidden}
/* 2026-09-11, Tony's ask (Save/Cancel still sitting right at the bottom
   edge after tightening the Star class/Race gap): editModal's content --
   6 flip tabs plus up to 6 real fields per section -- regularly runs
   longer than most of this shared .modal class's other users, so it gets
   its own taller allowance here (ID beats class, so this overrides the
   two properties above for #editModal only; every other modal keeps the
   original 100px/40px budget untouched). */
#editModal{max-height:calc(100vh - 60px);margin-bottom:20px}
/* hyperdriveModal, feedbackModal: .modal's blanket overflow-y:auto showed
   an always-on scrollbar here even on viewports where the content
   comfortably fits -- same 1px scrollHeight-vs-clientHeight rounding quirk
   already documented above for #hyperPanel. Default both to hidden and let
   openHyperNotice()/openFeedbackModal() opt into real scrolling only when
   they measure genuine overflow, same fix, same reason (Tony hit this on
   feedbackModal on his S26 the same way he'd hit it on hyperdriveModal
   before). */
#hyperdriveModal,#feedbackModal,#saveImportModal,#disclaimerModal{overflow-y:hidden}
#hyperdriveModal.scroll,#feedbackModal.scroll,#saveImportModal.scroll,#disclaimerModal.scroll{overflow-y:auto}
.modal h3{font-family:'Orbitron',sans-serif;font-size:13px;letter-spacing:.1em;
  color:var(--white);margin-bottom:2px;text-transform:uppercase}
.modal .msub{font-size:11px;color:var(--text-faint);margin-bottom:12px}
#disclaimerModal .msub{font-style:italic;cursor:default}
.mfld{margin-bottom:10px}
.mfld .lb{font-size:9px;letter-spacing:.12em;color:var(--text-faint);text-transform:uppercase;margin-bottom:3px}
/* About-modal accordion header (2026-08-25, honest-review fix): every .lb
   elsewhere is a dim caption sitting next to its own bright value, but
   inside #disclaimerModal the .lb text IS the whole collapsed row -- the
   only thing there to read or click -- so caption-dim contrast was actually
   too low for what functions as a button label. Bumped to full --text plus
   a small chevron (JS toggles .open) so it also reads as interactive. */
#disclaimerModal .mfld>.lb{color:var(--text);display:flex;align-items:center;gap:6px;
  padding:5px 4px;margin:-5px -4px -5px -4px;border-radius:6px;cursor:pointer}
#disclaimerModal .mfld>.lb::before{content:'\25B8';color:var(--cyan);font-size:8px;transition:transform .15s ease;flex:none}
#disclaimerModal .mfld>.lb.open::before{transform:rotate(90deg)}
/* Read-aloud additions (Tony, 2026-09-05): hidden entirely unless the
   toggle below is switched on -- panel looks exactly as it always has
   until then. See the JS comment near openDisclaimer() for the full
   design reasoning (why Web Speech API, why no native pause/resume). */
#discReadToggleRow{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin:-4px 0 10px}
#discReadToggleLabel{font-size:9px;letter-spacing:.1em;color:var(--text-faint);text-transform:uppercase}
#discReadToggle{width:30px;height:16px;border-radius:8px;background:rgba(120,140,150,.2);border:1px solid rgba(120,140,150,.35);position:relative;cursor:pointer;padding:0;flex:none}
#discReadToggle .thumb{width:12px;height:12px;border-radius:50%;background:#5b7480;position:absolute;top:1px;left:1px;transition:left .15s ease,background .15s ease}
#discReadToggle[aria-checked="true"]{background:rgba(111,216,255,.25);border-color:var(--cyan)}
#discReadToggle[aria-checked="true"] .thumb{left:15px;background:var(--cyan)}
#discReadBar{display:none;align-items:center;gap:10px;margin:-2px 0 12px;font-size:10px;color:var(--text-faint);flex-wrap:wrap}
#disclaimerModal.read-mode #discReadBar{display:flex}
#discReadBar button{background:transparent;border:1px solid var(--cyan);color:var(--cyan);font-size:10px;padding:3px 8px;border-radius:4px;cursor:pointer;font-family:inherit}
#discReadBar button:disabled{opacity:.35;cursor:default}
#disclaimerModal .mfld>.lb .rd-ic{display:none;margin-left:auto;font-size:11px;opacity:.4;flex:none}
#disclaimerModal.read-mode .mfld>.lb .rd-ic{display:inline-block}
#disclaimerModal .mfld>.lb.reading{color:var(--white)}
#disclaimerModal .mfld>.lb .rd-ic.pulse{opacity:1;color:var(--cyan);animation:rdPulse 1.1s ease-in-out infinite;display:inline-block}
@keyframes rdPulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.85)}}
.mfld input[type=text],.mfld input[type=number]{width:100%;text-transform:none;font-family:'Rajdhani',sans-serif;
  letter-spacing:.02em;color:var(--text);font-size:12px}
.mfld textarea{margin-top:0}
/* Tony (2026-08-17): the per-item 24-char cap on comma-separated fields
   (Resources/Flora/Fauna/Minerals/Salvageable tech/Fossils) is enforced
   server-side but the client-side maxlength on these inputs only caps the
   WHOLE field, not each item -- a single long word could type fine, look
   saved, then get silently blocked by the server. .fldHint is the static
   one-time explanation shown once under Resources; .itemLenWarn is the
   live per-field warning toggled by checkItemLenWarn() the moment a typed
   item actually goes over the limit, so the block never comes as a
   surprise after the fact. */
.fldHint{font-size:10px;color:var(--text-faint);margin:-6px 0 8px;line-height:1.4}
.itemLenWarn{font-size:10px;color:#ff8a7a;margin-top:3px;display:none}
.itemLenWarn.show{display:block}
.mfld input.over24{border-color:#ff5a4a}
.flagFieldList{max-height:150px;overflow-y:auto;overflow-x:hidden;border:1px solid var(--border-soft);border-radius:var(--r-sm);
  padding:6px 8px;background:#04070c}
.flagFieldRow{display:flex;align-items:center;gap:7px;padding:3px 0;font-size:12px;color:var(--text)}
.flagFieldRow input[type=checkbox]{flex:none}
.flagFieldRow .fq{margin-left:auto;font-size:9px;color:var(--fs-flagged);letter-spacing:.06em;white-space:nowrap}
.merr{background:#2a0d0d;border:1px solid #ff4a4a;color:#ffb0b0;border-radius:var(--r-sm);
  padding:8px 10px;font-size:11px;margin-bottom:10px;display:none;white-space:pre-wrap;
  position:relative;overflow:hidden}
/* Text sits in its own stacking layer above the .errScan glow patch below --
   without this, the glow (an absolutely-positioned sibling) paints on top of
   the plain-flow text and makes it harder to read. */
.errTxt{position:relative;z-index:1}
/* Glow patch on the error text where the drone's beam "lands" -- only
   switched on (.merr.scanning, toggled in showStatusIcon/hideStatusIcon) for
   the "drift" failures (blocked text, rate limit). Tony's reference screenshot
   showed an actual ship with a physical light beam connecting it to the
   ground, not just two independent effects moving in the same rhythm -- so
   this, the new .driftBeam cone under the icon, and the icon itself all now
   share ONE animation (iconDrift, same keyframe used for the icon's own
   left/flip motion below) and are all exactly 56px wide, the same as the
   icon -- guaranteeing this glow always sits in the same column as the
   drone and its beam, frame for frame, rather than three separately-timed
   effects that could still drift apart.
   The icon actually sits BELOW the error text in the modal (error box, then
   Retry, then the icon), so the glow is brightest at the BOTTOM of this box
   -- that's the edge nearest the drone -- fading toward the top, matching
   where .driftBeam actually connects into it from underneath. (An earlier
   version had this reversed, glowing at the top -- caught from Tony's
   screenshot showing the beam pointing the wrong way entirely.) */
.merr .errScan{position:absolute;top:0;left:0;width:56px;height:100%;pointer-events:none;opacity:0;
  background:radial-gradient(ellipse at 50% 100%,rgba(255,138,138,.55),rgba(255,74,74,.18) 55%,transparent 75%)}
.merr.scanning .errScan{opacity:1;animation:iconDrift 7s ease-in-out infinite;
  filter:drop-shadow(0 0 10px rgba(255,74,74,.5))}
.mnotice{background:rgba(240,165,0,.06);border:1px solid rgba(240,165,0,.35);color:#f0c060;
  border-radius:var(--r-sm);padding:8px 10px;font-size:11px;margin-bottom:10px;display:none}
.bodyEdit{border:1px solid var(--border-soft);border-radius:var(--r-sm);padding:8px;margin-bottom:8px;
  background:rgba(255,255,255,.02)}
.bodyEdit .bhead{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;cursor:pointer}
.bodyEdit:not(.open) .bhead{margin-bottom:0}
.bodyEdit .bhead:hover{background:rgba(255,255,255,.03)}
.bodyEdit .bhead span{font-family:'Orbitron',sans-serif;font-size:9.5px;letter-spacing:.1em;color:var(--gold);
  text-transform:uppercase}
.bodyEdit .bhLabel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:8px}
.bodyEdit .bhBtns{display:flex;align-items:center;gap:2px;flex-shrink:0}
.bodyEdit .bchev{color:var(--text-faint);font-size:10px;padding:2px 4px}
.bodyEdit .bx{cursor:pointer;color:var(--text-faint);font-size:14px;line-height:1;padding:2px 6px}
.bodyEdit .bx:hover{color:#ff8080}
.bodyEdit .brow2{display:flex;gap:6px;margin-bottom:6px}
.bodyEdit .brow2>div{flex:1}
/* Icon-type picker for signal markers (2026-09-09, Tony: "add the icons in
   front of names" -- a native <select><option> can't reliably show an image
   next to its text across browsers, so this is a small custom dropdown
   sitting on top of a hidden real <select class="sgIcon"> that still carries
   the actual value/change-event wiring (see renderSignalEditList()) -- same
   hidden-select-plus-visible-custom-control pattern already used for the
   hyperdrive drive/range pills above, just a dropdown list instead of a pill
   row since 8 options is too many to fit as pills in this narrow column. */
.iconPick{position:relative}
.iconPickBtn{display:flex;align-items:center;gap:6px;width:100%;background:#04070c;
  border:1px solid var(--border-soft);border-radius:var(--r-sm);color:var(--text);
  font-family:'Rajdhani',sans-serif;font-size:12px;padding:5px 8px;cursor:pointer;text-align:left}
.iconPickBtn:hover{border-color:var(--cyan)}
.iconPickLbl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.iconPickCaret{color:var(--text-faint);font-size:10px;flex:none}
.iconPickImg{width:18px;height:18px;flex:none;display:block}
.iconPickList{display:none;position:absolute;top:calc(100% + 3px);left:0;right:0;z-index:20;
  background:#04070c;border:1px solid var(--border-soft);border-radius:var(--r-sm);
  max-height:220px;overflow-y:auto;box-shadow:0 6px 18px rgba(0,0,0,.5)}
.iconPick.open .iconPickList{display:block}
.iconPickOpt{display:flex;align-items:center;gap:6px;padding:5px 8px;cursor:pointer;
  font-size:12px;color:var(--text)}
.iconPickOpt:hover{background:rgba(255,255,255,.06)}
.iconPickOpt.on{background:rgba(111,216,255,.12);color:var(--cyan)}
.chk2{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text-dim);cursor:pointer;
  white-space:nowrap;padding-top:16px}
.chk2 input{accent-color:#00e5ff;width:13px;height:13px}
#bodyEditList{margin-bottom:8px}
.starRow{display:flex;align-items:center;gap:6px;margin-bottom:6px}
.starRow .mfld{flex:1}
.starRow .bx{cursor:pointer;color:var(--text-faint);font-size:14px;line-height:1;padding:2px 6px;flex-shrink:0}
.starRow .bx:hover{color:#ff8080}
.starSwatch{width:14px;height:14px;border-radius:50%;flex:0 0 auto;border:1px solid rgba(255,255,255,.3)}
#starEditList{margin-bottom:8px}
.modalBtns{display:flex;gap:6px;margin-top:12px}
.modalBtns .btn{flex:1;text-align:center}
.modalBtns .btn.primary{border-color:var(--gold);color:var(--gold)}
#editSpin,#reportSpin,#feedbackSpin{display:none;align-items:center;justify-content:center;gap:8px;
  font-size:10px;color:var(--text-faint);text-align:center;margin-top:6px;letter-spacing:.02em}
/* Status icon shown above the Saving/error text -- which of the 6 PNGs and which
   animation class is picked in JS (see statusIcon() / STATUS_ICONS). "Waiting" states
   (still might succeed -- normal save, heavy traffic) get a calm pulse; "resolved
   failure" states (conflict, offline, blocked, rate limit) get a left-right drift
   instead, so the motion itself signals success-still-possible vs something's-
   actually-wrong, not just the icon artwork. Drift is the same drone-patrol pattern
   already used for the Theme Pack's "click me to scan" flyer (flyAcross/droneBob in
   NMS_FINAL_SITE-9.6.26/index.html) -- reused rather than guessed at a new motion,
   since Tony already knows that one fits the site's style and this one didn't
   (an earlier 3D-tumble version read as "an icon flipping card, too fast"). */
.iconWrap{display:none;margin-bottom:6px}
.iconWrap.pulse{display:flex;justify-content:center}
/* Drift wrap is block-level and full-width (same width as the .merr text box
   ABOVE it in the modal -- error box, then Retry, then this icon -- so the
   drone's translateX(0) -> translateX(100%-its own width) keyframe naturally
   spans "start of the error text" to "end of the error text" without needing
   to measure the text in JS -- it just always matches whatever's above it.
   margin-TOP (not bottom) is widened to 14px so there's physical room ABOVE
   the icon for .driftBeam to actually reach up into the error text rather
   than the two floating with a visible dead gap between them -- an earlier
   version put this gap below the icon instead, which is why the beam was
   shooting down into empty space next to "System name" rather than up
   toward the text, per Tony's screenshot. overflow is left visible (not
   hidden) so the beam isn't clipped where it extends past this wrap's own
   56px box into that gap above it. */
.iconWrap.drift{display:block;position:relative;height:56px;margin-top:14px}
.iconWrap img{width:56px;height:56px;object-fit:contain}
.iconWrap.pulse img{animation:iconPulse 1.6s ease-in-out infinite}
.iconWrap.drift img{position:absolute;top:0;left:0;animation:iconDrift 7s ease-in-out infinite;
  filter:drop-shadow(0 0 6px rgba(0,229,255,.6)) drop-shadow(0 0 2px rgba(0,229,255,.85));z-index:2}
@keyframes iconPulse{
  0%,100%{transform:scale(1);filter:drop-shadow(0 0 2px rgba(0,229,255,.3))}
  50%{transform:scale(1.08);filter:drop-shadow(0 0 8px rgba(0,229,255,.6))}
}
/* Flies left->right across the full width, flips to face the other way
   (scaleX mirror, same static mirroring the Theme Pack's own flyer-drone
   image uses) at each end, then flies back -- "start of text, end of text,
   flip like theme pack, back to the beginning" as a continuous loop. Reused
   as-is (not duplicated) for .driftBeam and the error box's .errScan glow
   below, since all three are 56px wide -- one shared keyframe means the
   drone, its beam, and the glow it lands on can never drift out of sync. */
@keyframes iconDrift{
  0%{left:0;transform:scaleX(1)}
  46%{left:calc(100% - 56px);transform:scaleX(1)}
  50%{left:calc(100% - 56px);transform:scaleX(-1)}
  96%{left:0;transform:scaleX(-1)}
  100%{left:0;transform:scaleX(1)}
}
/* The physical connecting beam Tony's reference screenshot showed -- a
   tapered cone shining UP from the drone into the error text above it,
   narrow at the drone's body (bottom of this element) and widening out as
   it reaches the text (top of this element), sized to reach past the 14px
   gap and a few px into both the icon and the error box so it visibly
   touches each end rather than stopping short. (Direction fixed from an
   earlier version that shot the beam downward into empty space below the
   icon instead -- this modal's real order is error text, then Retry, then
   the icon, so "up" is the only direction that actually reaches the text.)
   Hidden entirely for "pulse" icons (Grave/Atlas Pass v3), which don't fly
   and so have nothing to beam from. */
.driftBeam{display:none}
.iconWrap.drift .driftBeam{display:block;position:absolute;top:-16px;left:0;width:56px;height:24px;
  animation:iconDrift 7s ease-in-out infinite;pointer-events:none;z-index:1;
  background:linear-gradient(0deg,rgba(0,229,255,.55),rgba(0,229,255,.15) 65%,transparent);
  clip-path:polygon(8% 0%,92% 0%,60% 100%,40% 100%);
  filter:drop-shadow(0 0 5px rgba(0,229,255,.45))}

@media(max-width:900px){
  #panel{width:auto;left:10px;right:10px;top:auto;bottom:10px;max-height:44vh}
  #panel.show{display:flex;flex-direction:column}
  /* Enter system / Set course are the buttons someone actually needs to act on. Buried
     below Race/Economy/Conflict/tags/body-list, they scrolled out of sight on first
     open (confirmed on Tony's S26 -- "missing unless scroll up"). Pulling them to the
     top of the panel's flex order and pinning them there means they're always visible
     the instant you tap a system, and the rest of the detail scrolls underneath. */
  #prowEnter{order:-1;position:sticky;top:0;z-index:2;background:rgba(11,17,25,.97);
    margin:0 -14px;padding:10px 14px 8px;border-bottom:1px solid var(--border-soft)}
  /* 2026-09-17, Tony (real-device playtest): "can not click collapse on some
     screens" -- on this mobile layout #panel is a scrolling flex column and
     #prowEnter above is pinned (position:sticky, opaque bg) to its top. .phead
     (the name row that holds #bPanelMin) isn't positioned, so as soon as the
     panel is scrolled at all, .phead slides up underneath that pinned bar and
     paints BEHIND it -- the whole header, collapse button included, becomes
     covered and unclickable for as long as anything is scrolled past the top.
     Giving .phead a z-index above #prowEnter's (both need `position` for
     z-index to apply at all) keeps it painting on top whenever the two
     overlap during a scroll, so the collapse button stays reachable no
     matter where the panel is scrolled to. */
  .phead{position:relative;z-index:3}
  #course{top:calc(var(--top-h,150px) + 8px);left:50%;transform:translateX(-50%);width:210px}
  #leftcol{bottom:auto;top:calc(var(--top-h,150px) + 140px);flex-direction:column;align-items:flex-start}
  /* 2026-08-30, Tony: the toolbar hide/show cascade now keeps --top-h in
     sync at each step (see the boot script), but without this, every one
     of these repositions would still SNAP instantly since --top-h changes
     in six discrete steps, not continuously. Transitioning `top` turns each
     step into a glide, which combined with the cascade's own stagger reads
     as the whole overlay stack smoothly following the toolbar up and down
     rather than jumping. #galHud/#course/#toast/#galInfo get the same
     treatment just below/above since their own top:calc(--top-h...) rules
     live in the un-mediaed base stylesheet, not here. */
  #course,#leftcol,#keys,#toast,#galInfo{transition:top .2s ease-out}
  #filt{width:190px}
  /* Widened from 158px (2026-08-24, Tony: "the Y in 101ly is outside
     the box and both export and import logs are clipped" -- also
     "maybe expand the filter window out a little"). 158px left too
     little room for this panel's own longer button labels at this
     font/padding -- desktop's 176px doesn't clip, so mobile needed at
     least that much, plus some margin since mobile font rendering can
     run a little wider. See #bHyperOpen below for the other half of
     this fix (an ellipsis safety net for its own dynamic text). */
  /* Was on #filt itself; moved to just .fbody 2026-08-24 so #hyperPanel
     (now a sibling of .fbody, not a descendant) is never clipped by it
     again -- see the #hyperPanel CSS comment above for the full story. */
  #filt .fbody{max-height:calc(100vh - var(--top-h,150px) - 150px);overflow-y:auto;overflow-x:hidden}
  #keys{bottom:auto;top:calc(var(--top-h,150px) + 140px);width:calc(100vw - 20px)}
  #flyhint{display:none!important}
  input[type=text]{width:100%}

  /* Tweak (generation tuning) and Sensor telemetry (a debug X/Y/Z readout) are
     power-user tools, not something a first-time visitor needs. Both are now
     gated behind the Ctrl+Shift+F debug hotkey at every width (see html.debug-on
     rules above) -- this mobile-only rule predates that and would otherwise
     ALSO suppress them for Tony himself on his own phone with debug mode on,
     so it's scoped to only matter while debug mode is off. */
  html:not(.debug-on) #tweak,html:not(.debug-on) #tel{display:none}

  /* Toolbar reflow: title + Tour/About/Accessibility grouped first as one
     "what is this / help" row (Tony, 2026-08-21: "move title up so has
     ...tour accessibility etc together" -- title was previously order:1 but
     #bSearch/#bTour/#bAccess had no order set at all, so they defaulted to
     0 and landed BEFORE the title on real narrow screens, splitting a row
     that reads as one unit into two). Then Galaxy + galaxy-type together as
     their own row (Tony: "Galaxy dropdown, any type dropdown are not
     together" -- #galTypeFilter had the same missing-order bug, defaulting
     to 0 and landing all the way up with Search/Tour/Access instead of next
     to #galSel). Then view-mode toggles, the address input on its own line
     (with a Reset button, since there wasn't one before) plus a label so a
     first-time visitor knows what that box is for, then the secondary
     address actions (Glyphs/Random/Search/Filters -- #bSearch belongs here
     with the other address-related actions, not defaulting to the very
     front), then Labels/Grid, then whatever's left. Dividers are dropped on
     mobile since the row breaks already group things visually. Every
     toolbar child needs an explicit order here -- anything left unset
     defaults to 0 and jumps the queue, which is exactly how this bug
     happened twice already; the /10 gaps leave room to insert more later
     without renumbering everything. */
  /* 2026-08-30, Tony (round 5): brought dividers back on mobile instead of
     dropping them, since the round-4 regroup packs items across wrapping
     rows and a group can now land mid-row next to its neighbour with
     nothing to mark the seam. A vertical bar (the desktop .sep) doesn't
     read as a break once things wrap, so on mobile each #sep* becomes a
     full-width rule instead: flex:1 0 100% makes it consume the rest of
     its own row and forces whatever comes next to start a fresh line,
     turning it into a guaranteed group boundary regardless of how the
     button widths happen to pack. Each of the 5 sep elements gets an
     order value sitting between two groups' order ranges (see below) so
     it lands at the right seam instead of wherever it happens to be in
     the HTML. */
  #top .sep{display:block;width:auto;height:1px;flex:1 0 100%;
    margin:8px 0 4px;background:rgba(0,229,255,.35);align-self:stretch}
  #sepA{order:15} #sepB{order:27} #sepC{order:37} #sepD{order:47} #sepE{order:55}
  /* 2026-08-30, Tony (round 6): "intrusive, overwhelming, can't see the full
     system picture because of the menu" -- the grouped+divided toolbar
     (rounds 4-5) is easy to read but tall, up to 7 stacked rows, and stays
     fixed over the map the whole time. #toolbarToggle gets its own full-width
     row right under the title (order:2, flex-basis:100% so nothing else can
     share its line); title stays as a permanent anchor so the bar never
     disappears completely, and the toggle button's own label always says
     which way it currently goes (Hide vs. Show), so there's nothing to
     separately explain. State is remembered across visits via localStorage
     (see the boot script near the end of the file) so once someone hides
     it, it stays hidden until they choose to bring it back, on this device. */
  #toolbarToggle{display:flex;order:2;flex:0 0 100%;justify-content:flex-end;
    background:transparent;border:none;color:var(--cyan);
    font-family:'Orbitron',sans-serif;font-size:10px;letter-spacing:.08em;
    text-transform:uppercase;padding:2px 2px 4px;cursor:pointer}
  @keyframes toolbarTogglePress{
    0%{color:var(--cyan);text-shadow:none}
    35%{color:#fff;text-shadow:0 0 10px rgba(0,229,255,1),0 0 22px rgba(0,229,255,.7)}
    100%{color:var(--cyan);text-shadow:none}
  }
  #toolbarToggle.flash{animation:toolbarTogglePress .35s ease-out}
  /* 2026-08-30, Tony (round 8): "reveal one row at a time smoothly with same
     glow as title but then looses glow... remove one at a time... not too
     quick not too slow" -- round 7's single max-height slide on the whole
     #toolbarBody replaced with a per-GROUP cascade instead: each of the 6
     groups (see the TIERS list in the boot script -- same 6 groups as the
     dividers already mark) reveals or hides on its own staggered delay, so
     rows genuinely appear/disappear one at a time rather than the whole
     block resizing as one unit. #toolbarBody itself is now just a plain
     flex-wrap box again (no max-height/overflow -- overflow:hidden would
     have clipped the glow below), and each element's own show/hide state is
     a class (.tb-hidden/.tb-revealing/.tb-hiding below) toggled by the boot
     script rather than anything driven from here. */
  #toolbarBody{display:flex;flex-wrap:wrap;align-items:flex-end;gap:6px;
    order:3;flex:0 0 100%}
  /* #top .tb-hidden, not plain .tb-hidden -- the dividers' own base rule
     (#top .sep{display:block...} above) is an ID+class selector, so a bare
     class selector here would lose the specificity fight and the divider
     lines would stay visible even while "hidden" (exactly what happened
     the first time this was written as plain .tb-hidden -- buttons hid
     correctly since .btn is the same specificity as .tb-hidden and source
     order broke the tie, but #top .sep always won regardless of order). */
  #top .tb-hidden{display:none}
  @keyframes toolbarRowReveal{
    0%{opacity:0;transform:translateY(-6px)}
    100%{opacity:1;transform:translateY(0)}
  }
  @keyframes toolbarRowHide{
    0%{opacity:1;transform:translateY(0)}
    100%{opacity:0;transform:translateY(-6px)}
  }
  /* 2026-08-31, Tony: "jittery collapse and expand... needs a smoother
     animation" (video attached, real jank confirmed frame-by-frame -- not
     a report to retune timing again). This animation had already been
     through 8+ rounds of pure timing tuning (stagger/duration halved twice)
     without fixing it, which was the tell that the problem was never speed
     -- it was cost-per-frame. The `filter:drop-shadow(...)` glow this
     keyframe used to animate is the likely real culprit: unlike opacity and
     transform (both compositor-only, cheap on any GPU), `filter` forces the
     browser to rasterize/repaint the element every frame, and this was
     firing on up to 5 buttons at once per tier, 6 tiers in a row -- exactly
     the kind of per-frame repaint that a fast desktop Chrome hides but a
     real phone GPU can't keep up with, matching what the video actually
     shows. Dropped the glow, kept the same fade+slide motion (now pure
     opacity/transform, GPU-compositable, no repaint) -- see the boot
     script's own reflow-batching change alongside this for the other half
     of the fix. If Tony wants the glow back once this is confirmed smooth,
     it's a separate, much cheaper way to add it (e.g. box-shadow on a
     single wrapping element instead of filter on every button) rather than
     restoring this version. */
  .tb-revealing{animation:toolbarRowReveal .56s ease-out both;will-change:opacity,transform}
  .tb-hiding{animation:toolbarRowHide .32s ease-in both;will-change:opacity,transform}
  @media(prefers-reduced-motion:reduce){
    .tb-revealing,.tb-hiding{animation:none}
  }
  /* 2026-08-30, Tony (round 2 -- moved Info to the end): first pass just
     dropped Camera in next to the pre-existing Tour/About/Access/Install
     cluster without questioning why that cluster was order 2-5 in the first
     place -- it put four help/settings buttons as the very first thing a
     phone user sees, above the actual map controls, which reads backwards
     and doesn't match the desktop bar (where that same cluster is the LAST
     group, after the sep before it). Renumbered the whole block to mirror
     the desktop group order exactly: Galaxy picker -> View mode -> Travel ->
     Display -> Camera -> Info, Info now last instead of first. */
  /* 2026-08-30, Tony (round 3): #title forced to flex-basis:100% so it
     always claims the whole first row on its own -- previously it had no
     explicit basis, so anything narrow enough (e.g. #galSel before its
     options are populated) could end up squeezed onto the title's row
     instead of wrapping below it. */
  #title{order:1;flex:1 1 100%}
  /* 2026-08-30, Tony (round 4): a full-width hex box on its own row (round 3)
     read as too long/prominent by itself. Tony's own regroup instead:
     Galaxy dropdown, Any type dropdown, then [Galaxy, Local, hex address,
     Clear] as one group, then [Glyphs, Random, Search, Jump] as one group
     (Jump moves next to the other address-entry actions instead of sitting
     glued to the input), then [Filter, Labels, Grid, Atlas], then
     [Orbit, Camera], then [Install, Tour, About, Access] last. #addrGrp and
     .addr-row are normally their own flex boxes (so Jump/Clear/the label
     stay bundled with the hex input on desktop) -- display:contents here
     dissolves both of those boxes on mobile only, promoting the label,
     input, Jump and Clear to be direct flex children of #top so each can
     take its own order and land in a different visual group than its
     siblings. */
  #galSel{order:10} #galTypeFilter{order:11}
  #mGal{order:20} #mLoc{order:21}
  #addrGrp{display:contents}
  #addrGrp .addr-row{display:contents}
  /* Label hidden here, not just reordered -- it's redundant with the input's
     own "HEX ADDRESS" placeholder, and at 158px wide (see .addr-lb's base
     rule) it wouldn't fit in this row alongside Galaxy/Local/Clear anyway.
     The other mobile-only rule -- input[type=text]{width:100%} -- also has
     to be overridden here or the hex box eats the whole row by itself and
     shoves Clear onto the next line, same as it did before this box was
     grouped inline. */
  #addrGrp .addr-lb{display:none}
  #inAddr{order:23;width:128px;flex:0 0 auto}
  #bAddrReset{order:24}
  #bKeys{order:30} #bRand{order:31} #bSearch{order:32} #bJump{order:33}
  #bWarpToggle{order:34} #bRoutes{order:35}
  #bFiltToggle{order:40} #bLbl{order:41} #bGrid{order:42} #bAtlas{order:43}
  #cOrb{order:50} #bGalaxySpin{order:51} #cFly{order:52}
  #bTour{order:61} #bAbout{order:62} #bAccess{order:63}
}
@media(max-width:900px) and (orientation:landscape){
  /* Short landscape screens don't have room to stack the toolbar, the left-side
     Tweak/Filters/Telemetry boxes AND a bottom-docked panel without them overlapping
     (confirmed on Tony's S26 in landscape -- panel covered the Filters box). Docking
     the panel to the right instead, like desktop already does, keeps it clear of the
     left-docked boxes entirely rather than fighting over the same vertical space. */
  #panel{left:auto;right:10px;width:300px;top:calc(var(--top-h,60px) + 10px);bottom:10px;max-height:none}
}
/* ---- nms-hud-card / nms-btn theme wrappers (spec item 10) ---- */
.nms-hud-card{background:var(--nms-bg-glass);border:1px solid var(--nms-border-neon);
  border-radius:6px;font-family:monospace;box-shadow:0 0 15px rgba(0,229,255,0.15)}
.nms-btn{background:transparent;border:1px solid var(--nms-border-neon);color:var(--text);
  padding:5px 10px;font-size:10px;cursor:pointer;border-radius:4px;letter-spacing:1px;
  font-family:'Orbitron',sans-serif;transition:all 0.2s}
.nms-btn:hover{border-color:var(--nms-border-neon-hover);background:rgba(0,229,255,0.07)}
.nms-btn-action{color:var(--color-matrix);border-color:var(--color-matrix)}
.nms-btn-action:hover{background:rgba(0,255,102,0.1)}
.nms-btn-danger{color:#ff5a4a;border-color:#ff5a4a}
.nms-btn-danger:hover{background:rgba(255,74,74,0.1)}
/* ---- warp transition overlay (spec item 9) ---- */
#warp-overlay{display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;
  z-index:10000;background:#000;cursor:pointer;user-select:none;overflow:hidden}
#warp-overlay video{width:100%;height:100%;object-fit:cover;display:block;
  /* 2026-08-25, Tony: the portal clip has a real ~3.5% dark-green pillarbox
     baked into its own pixels (confirmed frame-by-frame -- flat, unchanging
     colour at the very edges, not a vignette), which object-fit:cover alone
     only crops away on very wide viewports. This extra 14% overscan crops a
     fixed margin off every side regardless of viewport shape, so the bar is
     gone everywhere, not just on wide windows. Harmless on the ship clip,
     which is already edge-to-edge content. If the source clip ever gets
     re-exported without the padding, this can drop back to no transform. */
  transform:scale(1.14)}
.warp-skip{position:absolute;bottom:22px;width:100%;text-align:center;
  font-family:'Orbitron',monospace;font-size:10px;letter-spacing:3px;
  color:rgba(0,229,255,0.35);pointer-events:none}
    /* ---- animated mini galaxy icon (spec item 11): rotation is now done by
       redrawing the canvas with an advancing polar-angle offset in drawGalIcon()
       (see JS, near galIconRot) instead of a CSS transform. A flat CSS rotate()
       spun the whole tilted ellipse like a clock hand -- reads as a coin
       spinning face-on, not a disc rotating on its own axis. Redrawing the
       spiral pattern inside a fixed-tilt ellipse keeps the ellipse's shape and
       orientation constant on screen while the arms sweep around inside it,
       the way a real tilted galactic disc actually looks when it spins. ---- */
/* ---- warp manifest itinerary card (spec item 7) ---- */
/* Draggable again as of 2026-08-26 (Tony: "wants to be like all other
   windows draggable and collapsible") -- opens as a dropdown auto-anchored
   near the Course plotted card / system info panel the first time each
   session, via #bToggleManifest, but can be dragged anywhere afterward
   exactly like every other floating box. See positionManifestCard(). */
#route-itinerary-card{position:fixed;left:50%;transform:translateX(-50%);
  top:calc(var(--top-h,60px) + 8px);z-index:26;width:300px;display:none;
  padding:12px 14px}
#route-itinerary-card.show{display:block}
#route-itinerary-card .rihead{display:flex;justify-content:space-between;align-items:center;
  margin-bottom:10px;user-select:none}
/* 2026-08-26, Tony: manifest hop rows "look clickable" (bold cyan [HOP N]
   label) but did nothing -- wired up in displayCalculatedItinerary() below;
   this just gives the now-real click target a visible hover state. */
.hopRow{padding:2px 4px;margin:0 -4px;border-radius:4px;cursor:pointer}
.hopRow:hover{background:rgba(0,229,255,.1)}
#bBackToTarget:hover{color:var(--cyan)}
/* ---- first-visit feature tour (2026-08-16, Tony's ask: a walkthrough of
   what the site can do, adapted from the README's screenshot walkthrough
   into a real in-site spotlight sequence). Not auto-shown -- reachable via
   "Take a quick tour" on the disclaimer modal and a standing Tour button in
   the toolbar, so it never stacks on top of the disclaimer itself and can be
   replayed any time, including live in front of someone. #tourHole is a
   small fixed box positioned exactly over the real target element each step
   (measure-don't-guess, same as positionUnderButton/positionCourseCard) --
   its own oversized box-shadow does the actual dimming of everything else,
   a plain CSS spotlight with no canvas/SVG masking needed. */
#tourOverlay{position:fixed;inset:0;z-index:9000;display:none;pointer-events:none}
#tourOverlay.show{display:block}
#tourHole{position:fixed;border-radius:8px;border:2px solid var(--cyan);
  box-shadow:0 0 0 9999px rgba(2,4,7,.8),0 0 16px rgba(0,229,255,.5);pointer-events:none;
  transition:top .25s ease,left .25s ease,width .25s ease,height .25s ease}
#tourBox{position:fixed;max-width:280px;padding:14px;pointer-events:auto;z-index:9001}
#tourBox .modalBtns{margin-top:12px}
#tourBox #tourBack{visibility:hidden}
/* Screenshot lightbox (2026-09-01) -- one shared overlay for both the
   Edit System modal's own preview and the public info panel's community
   screenshot, opened via openScreenshotLightbox(). z-index sits above
   #adminPanel (500) and every ordinary panel/modal so it can be triggered
   from any of them; well below the first-run tour overlay (9000+), which
   is a separate transient onboarding layer, not something a screenshot
   click needs to out-rank. */
.lightboxOverlay{position:fixed;inset:0;z-index:600;background:rgba(2,4,7,.88);
  display:flex;align-items:center;justify-content:center;flex-direction:column;
  gap:14px;padding:24px;cursor:zoom-out}
.lightboxOverlay img{max-width:min(92vw,1100px);max-height:80vh;border-radius:var(--r-sm);
  box-shadow:0 20px 60px rgba(0,0,0,.6);cursor:default}


  .edConsole{margin-bottom:10px}
  .edConsoleState{display:inline-flex;gap:4px;padding:3px;border:1px solid var(--border-soft);
    border-radius:8px;background:var(--panel);margin-bottom:10px}
  .edConsoleState button{font-family:'Orbitron',sans-serif;font-size:8.5px;letter-spacing:.07em;
    text-transform:uppercase;padding:7px 10px;border-radius:6px;border:none;background:transparent;
    color:var(--text-faint);cursor:pointer}
  .edConsoleState button.on{background:rgba(0,229,255,.1);color:var(--cyan)}
  .edScanbar{display:flex;align-items:center;gap:5px;margin-bottom:6px}
  .edScanbar i{flex:1;height:3px;border-radius:2px;background:var(--border-soft)}
  .edScanbar i.done{background:var(--cyan-dim)}
  .edScanbar i.now{background:var(--cyan);box-shadow:0 0 8px rgba(0,229,255,.6)}
  .edScanlabel{font-family:'Orbitron',sans-serif;font-size:9px;letter-spacing:.12em;color:var(--text-faint);
    text-transform:uppercase;margin-bottom:10px}
  .edScanlabel b{color:var(--cyan)}
  /* 2026-09-11, Tony's ask (screenshot showed 5 tabs then Attribution
     alone on its own row): switched from flex-wrap, which packs tiles
     left-to-right and wraps whatever doesn't fit, to a fixed 3-column
     grid -- always exactly 2 even rows of 3 for all 6 EDSEC_ORDER tabs,
     regardless of the modal's actual width, and each tile grows to fill
     its column (bigger tiles on a wider modal) instead of staying a fixed
     58px. */
  .edSwitchRow{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:10px;perspective:600px}
  .edSwitch{width:100%;height:54px;cursor:pointer;position:relative}
  .edSwInner{position:relative;width:100%;height:100%;transform-style:preserve-3d;
    transition:transform .45s cubic-bezier(.2,.7,.3,1.2)}
  .edSwitch.active .edSwInner{transform:rotateY(180deg)}
  .edSwFace{position:absolute;inset:0;backface-visibility:hidden;display:flex;flex-direction:column;
    align-items:center;justify-content:center;gap:3px;
    clip-path:polygon(8px 0,100% 0,100% calc(100% - 8px),calc(100% - 8px) 100%,0 100%,0 8px);
    border:1px solid var(--border-soft);background:rgba(255,255,255,.02)}
  .edSwFront img{width:20px;height:20px;object-fit:contain;opacity:.9}
  .edSwFront span{font-family:'Orbitron',sans-serif;font-size:6.6px;letter-spacing:.04em;
    text-transform:uppercase;color:var(--text-faint);text-align:center;line-height:1.15;padding:0 2px}
  .edSwBack{transform:rotateY(180deg);border-color:var(--cyan);background:rgba(0,229,255,.1)}
  /* 2026-09-11, Tony's ask: swap the plain text checkmark for the site's
     real Atlas icon (same status-atlas.png/pulse motion already used for
     "normal save, in flight" elsewhere) so the "you're on this tab" face
     matches the rest of the site's real game imagery instead of a generic
     glyph. Reuses the existing iconPulse keyframe rather than a new one. */
  .edSwBack img{width:22px;height:22px;object-fit:contain;animation:iconPulse 1.6s ease-in-out infinite}
  .edSwitch:not(.active):hover .edSwFront{border-color:var(--cyan-dim)}
  @media (hover:hover){
    .edSwitch:not(.active):hover .edSwFront{animation:edSwitchPulse 1.1s ease-in-out infinite}
  }
  @keyframes edSwitchPulse{
    0%,100%{box-shadow:0 0 0 0 rgba(0,229,255,0)}
    14%{box-shadow:0 0 0 3px rgba(0,229,255,.35)}
    28%{box-shadow:0 0 0 0 rgba(0,229,255,0)}
    42%{box-shadow:0 0 0 3px rgba(0,229,255,.35)}
    60%,100%{box-shadow:0 0 0 0 rgba(0,229,255,0)}
  }
  .edWizNav{display:flex;gap:6px;margin:4px 0 14px}
  .edWizNav .btn{flex:1}
  .edSec{animation:edSecIn .25s ease}
  @keyframes edSecIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
  
}