/* HTML styles for the splash screen.
   Plain static CSS -- this renders before the Avalonia/WASM runtime has booted, so it can't
   reference Theme.axaml's StaticResource tokens. Colors below are hardcoded copies of the
   "mission control" palette from src/ProjectMaelstrom.Client/Styles/Theme.axaml (design doc
   section 16). App.axaml locks RequestedThemeVariant="Dark" unconditionally, so this splash
   does the same and does not adapt to prefers-color-scheme. */
.avalonia-splash {
    position: absolute;
    height: 100%;
    width: 100%;
    background: #0A0E16; /* VoidColor */
    justify-content: center;
    align-items: center;
    display: flex;
    pointer-events: none;
}

.splash-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
}

.splash-wordmark {
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 1.75rem;
    font-weight: 600;
    letter-spacing: 0.35em;
    color: #E8EAED; /* InkColor */
    /* Squeeze the tracked-out letter-spacing back in on the right so it doesn't look lopsided. */
    padding-right: 0.35em;
}

.splash-wordmark-accent {
    color: #32D6C4; /* TealColor */
    margin-left: 0.35em;
}

.splash-status {
    display: flex;
    align-items: center;
    gap: 10px;
}

.splash-status-text {
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 0.75rem;
    font-weight: 400;
    letter-spacing: 0.15em;
    color: #7C8494; /* DimColor */
}

.splash-spinner {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #32D6C4; /* TealColor */
    animation: splash-pulse 1.1s ease-in-out infinite;
}

@keyframes splash-pulse {
    0%, 100% {
        opacity: 0.35;
        transform: scale(0.8);
    }
    50% {
        opacity: 1;
        transform: scale(1);
    }
}

.avalonia-splash.splash-close {
    transition: opacity 200ms, display 200ms;
    display: none;
    opacity: 0;
}

/* Avalonia.Browser (avalonia.js, AvaloniaDOM.createAvaloniaHost) creates a hidden, transparent
   HTML <input class="avalonia-input-element"> per view -- not something this project's own XAML
   creates or can style directly -- that it focuses/positions/shows to proxy real DOM keyboard
   focus into Avalonia's own TextBox/TextBoxes, since that's what actually triggers a mobile
   browser's on-screen keyboard (a real DOM element receiving focus() is required; Avalonia's own
   Skia-drawn TextBox never touches the DOM directly). It never gets an explicit font-size from
   avalonia.js, and mobile Safari auto-zooms the whole page on focusing any form control whose
   computed font-size is under 16px -- a real, well-documented mobile-Safari quirk, unrelated to
   whether the keyboard itself appears, but disruptive layout-wise right when it would. Targeting
   this stable class name (confirmed against the pinned Avalonia.Browser 12.1.0 package's own
   avalonia.js, not guessed) is the only hook this project has into that hidden element at all. */
.avalonia-input-element {
    font-size: 16px !important;
}

/* #out (Avalonia's own mounted content) needs its own real stacking context and position:fixed +
   inset:0 gives it an explicit full-viewport box (rather than relying on it being unnecessarily
   sized by absolutely-positioned children, which would otherwise leave it collapsed to 0x0 --
   the avalonia-splash screen's own height:100% depends on this). z-index is explicit (not just
   relying on default document-order stacking) so #galaxy-map-canvas below can deterministically
   sit above it regardless of DOM insertion order -- see that rule's own comment for why this
   matters beyond simple visual layering. */
#out {
    position: fixed;
    inset: 0;
    z-index: 10;
}

/* Real-time 3D Galaxy View canvas (design doc §17, webgl-3d-map) -- a plain DOM element, sibling
   of #out, driven entirely by wwwroot/galaxymap.js. Two real technical findings this rule encodes,
   both confirmed live by a now-deleted prior implementation of this exact screen (see
   docs/CHANGELOG.md's several 2026-07-20 "Galaxy View" entries for the full trail):

   1. Real Avalonia content can NEVER show through this canvas via CSS transparency, no matter
      what z-index/opacity tricks are tried -- Avalonia.Browser.BrowserTopLevelImpl.
      TransparencyLevel is hardcoded to WindowTransparencyLevel.None in the pinned Avalonia
      version (confirmed by decompiling the actual installed DLL, not guessed), so real
      window/compositor-level transparency for the Browser/WASM head is architecturally
      unimplemented here. This canvas is therefore positioned ABOVE #out (z-index 11 > 10) rather
      than below it -- there is no working "below, showing through a transparent hole" approach in
      this Avalonia version.

   2. Avalonia's browser backend injects its own native-input-host <div> (id="nativeHost<random>")
      inside #out that silently swallows real pointer/wheel events aimed at anything visually
      beneath it (experiments/3d-galaxy-map-spike/README.md's own documented gotcha, re-confirmed
      here). Putting this canvas at a real, explicit higher z-index than #out is what lets normal
      browser hit-testing route clicks to the canvas instead of that host div -- no
      `pointer-events` CSS override needed, unlike the spike's own throwaway demo pages (which had
      no real Avalonia UI to preserve input for at all). This canvas's on-screen rectangle is
      expected to be kept scoped to ONLY the Galaxy screen's own 3D content region (via
      GalaxyMapInterop.SyncViewportBounds, called from a real Avalonia viewport placeholder's own
      LayoutUpdated) once real chrome (topbar/nav rail) exists elsewhere on screen -- otherwise
      this full-viewport default would sit on top of and block clicks to that other real Avalonia
      UI too. Defaults to a full-viewport box (matching #out's own inset:0) until a caller starts
      calling SyncViewportBounds. display:none by default; wwwroot/galaxymap.js's setVisible
      toggles it.

      That scoping HAS since happened, on all four map canvases -- GalaxyView/SystemView/PlanetView/
      CityView each drive their own SyncViewportBounds from a real placeholder's LayoutUpdated, and
      the live result was measured, not assumed: #galaxy-map-canvas sits at 64,52 1116x818 in a
      1440x900 viewport, i.e. exactly inside the rail/topbar/sidebar/status-bar chrome. So the
      docked chrome above is NOT occluded, and never was.

      What the note above did NOT anticipate is the case that is left: a POPUP is not docked. Every
      ToolTip.Tip in the app opens off its own control and lands inside the canvas's rect (a nav-rail
      tooltip opens rightward off a 64px rail; a status-bar chip's opens upward), so it renders
      behind the canvas no matter how tightly the rect is scoped -- the real, user-reported
      2026-08-06 bug ("those small tooltips when hovering over the buttons are behind the 3d
      Window"). Shrinking the canvas cannot fix that class at all. It is fixed instead by cutting a
      per-popup clip-path hole in whichever canvases exist, driven generically off Avalonia's own
      popup overlay layers -- see Client/Services/MapCanvasOverlayGuard.cs and
      wwwroot/navRailOverlay.js. */
#galaxy-map-canvas {
    position: fixed;
    inset: 0;
    z-index: 11;
    display: none;
    touch-action: none; /* browser default touch gestures (scroll/pinch-zoom the PAGE) would
                            otherwise fight this canvas's own pan/pinch/orbit camera controls */
}

/* Real-time 3D System View canvas (design doc §17, webgl-3d-map) -- SystemMapInterop.cs/
   wwwroot/systemview.js's own canvas, a separate DOM element from #galaxy-map-canvas above (both
   default to a full-viewport box, only one is ever meant to be visible at once -- see
   SystemMapInterop.cs's own doc comment). Same z-index/touch-action/display:none-by-default
   reasoning as #galaxy-map-canvas -- see that rule's own comment for the full "why above #out,
   why no pointer-events override needed" write-up, identical here. */
#system-map-canvas {
    position: fixed;
    inset: 0;
    z-index: 11;
    display: none;
    touch-action: none;
}

/* Owned-system pill badge (galaxy map UX pass, ring-to-pill replacement, 2026-07-24) -- a
   screen-space nameplate over the viewer's own system marker(s), replacing an earlier world-space
   green ring that shrank with zoom like every other marker (wwwroot/galaxymap.js's own
   VERTEX_SHADER_MARKER_RING comment has the full story). #galaxy-owned-pills (created by
   ensureOwnedPillContainer) is a plain DOM sibling of #out/#galaxy-map-canvas, positioned/sized
   every render frame by galaxymap.js itself (see updateOwnedPillPositions) -- this rule only
   styles the pill's own look, matching the mockup's `.infocard .tag`/`.rt-active-pill` pill
   language (docs/mockups/client_redesign_concept.html) and this project's own locked palette
   (Styles/Theme.axaml): dark translucent/blurred background, colored border, small mono font,
   rounded rect. Green (Theme.axaml GreenColor #4ADE80) was already the established "this one is
   mine" token from the ring this replaces, and stays otherwise unused by the rest of this
   renderer's own palette (zone rings amber/teal/slate, hostile red, fog tiers brightness-only) --
   Theme.axaml has no locked "GreenDim" companion token the way Amber/Teal do, so the border below
   is GreenColor itself at reduced alpha rather than an invented flat color. */
.galaxy-owned-pill {
    position: absolute;
    display: none; /* toggled to 'block' per-frame by updateOwnedPillPositions -- see galaxymap.js */
    transform: translate(-50%, -100%); /* anchors the pill's bottom-center on its target point, which
                                           updateOwnedPillPositions already offsets above the marker */
    padding: 3px 9px;
    border-radius: 6px;
    background: rgba(18, 22, 31, .55);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border: 1px solid rgba(74, 222, 128, .45); /* GreenColor #4ADE80 @ 45% alpha */
    color: #4ADE80; /* GreenColor, Styles/Theme.axaml */
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .02em;
    white-space: nowrap;
    pointer-events: none; /* readout only -- never intercepts a click meant for the canvas beneath it */
}

/* System View in-3D name-label overlay (star/planet nameplates, 2026-07-26 ui-designer pass) -- a
   plain DOM sibling of #system-map-canvas, positioned ABOVE it (z-index 12 > 11) so labels
   actually composite over the opaque WebGL canvas. Same underlying platform constraint
   #galaxy-owned-pill above already documents (real Avalonia content can never composite over
   either map canvas -- see SystemLabelOverlayInterop.cs's own doc comment for the full citation),
   solved here with the identical "plain DOM sibling, higher z-index than the canvas" recipe
   instead of an Avalonia Canvas. Positioned/populated by wwwroot/systemLabelOverlay.js, called
   directly from systemview.js's own render() loop every real animation frame (2026-07-26 lag
   fix -- see that file's own top comment; this was originally driven from a separate, slower
   C#-side DispatcherTimer poll, which is why an old label could visibly lag behind its own 3D
   marker during a camera drag). */
#system-label-overlay {
    position: fixed;
    inset: 0;
    z-index: 12;
    display: none; /* toggled by setLabelOverlayVisible -- see systemLabelOverlay.js */
    pointer-events: none;
}

/* One label: a star's own name (no pill, just amber text with a shadow -- the star already
   anchors the whole scene visually as the brightest thing in frame, so boxing its own label would
   just add clutter right where the eye is already drawn) or a planet's name (a small translucent
   teal pill, same background/blur recipe as .galaxy-owned-pill, since a planet marker often sits
   over a bright corona/starfield backdrop where plain text alone reads poorly). Anchored ABOVE its
   body's own real screenX/screenY (translate -50%/-100% plus a small gap), matching this app's
   own "SYSTEM RENDER SURFACE" nameplate-above-target convention, and keeping labels clear of the
   orbit-ring markers a planet typically has directly beneath it. Deliberately no distance-based
   fade: getLabelPositions carries only a 2D screen position, no real world-space depth/scale a
   fade could honestly key off -- inventing one would be exactly the kind of fabricated-data shortcut
   this app's own conventions avoid (see SystemView.axaml's own doc comment on not showing fields
   that aren't real). Legibility instead comes from font weight/color/shadow-or-pill contrast alone. */
.system-body-label {
    position: absolute;
    transform: translate(-50%, calc(-100% - 10px));
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    letter-spacing: .02em;
    white-space: nowrap;
    /* Still none, and now for a stated reason rather than by default (GitHub issue #277; Camp View
       took the same change first as issue #265 item 2 -- see .camp-label's own comment).
       A label must not be a DOM click target: a drag begun on one would reach neither the label nor
       the canvas underneath, and this scene's pan/orbit/zoom and its click-vs-drag distinction all
       live on canvas pointer handlers. What DID change is that "the event goes to the canvas" no
       longer means "the click means whatever happens to be behind the words": systemview.js's
       handlePick now hit-tests these boxes first (labelPacker.js's labelIdAtPoint) and selects the
       body the label names. Before that it selected whatever was nearest the marker ANCHOR, which
       sits a box height plus the 10px below -- i.e. a different body, and on this screen usually the
       parent of the moon or satellite whose name was clicked.
       The transform above is therefore load-bearing for picking as well as for looks: change the
       offset and the plate moves, but the hit test moves with it, because it reads the real
       rendered box rather than restating this rule. */
    pointer-events: none;
}

.system-body-label.star {
    color: #F2A93B; /* AmberColor, Styles/Theme.axaml -- secondary/highlight accent per design doc
                        §16, used here since the star is the scene's non-interactive backdrop body */
    font-size: 11.5px;
    font-weight: 700;
    text-shadow: 0 1px 3px rgba(0, 0, 0, .9), 0 0 8px rgba(0, 0, 0, .6);
}

.system-body-label.planet {
    color: #32D6C4; /* TealColor, Styles/Theme.axaml -- primary accent, matching every other
                        interactive/claimable game object in this app */
    font-size: 10.5px;
    font-weight: 600;
    background: rgba(18, 22, 31, .6); /* PanelColor @ 60% alpha, same recipe as .galaxy-owned-pill */
    border: 1px solid rgba(50, 214, 196, .4); /* TealColor @ 40% alpha */
    border-radius: 5px;
    padding: 2px 6px;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}

/* Real-time 3D Planet View canvas (design doc §17, webgl-3d-map) -- PlanetMapInterop.cs/
   wwwroot/planetview.js's own canvas, a THIRD DOM element alongside #galaxy-map-canvas/
   #system-map-canvas (all three default to a full-viewport box, only one is ever meant to be
   visible at once -- see PlanetMapInterop.cs's own doc comment). Same z-index/touch-action/
   display:none-by-default reasoning as the other two map canvases -- see #galaxy-map-canvas's own
   comment for the full "why above #out, why no pointer-events override needed" write-up, identical
   here. */
#planet-map-canvas {
    position: fixed;
    inset: 0;
    z-index: 11;
    display: none;
    touch-action: none;
}

/* Planet View in-3D plot-name-label overlay -- a plain DOM sibling of #planet-map-canvas,
   positioned ABOVE it (z-index 12 > 11) so labels actually composite over the opaque WebGL canvas,
   same underlying platform constraint #system-label-overlay above already documents. Positioned/
   populated per-frame by wwwroot/planetLabelOverlay.js, never touches planetview.js's own WebGL
   rendering code. */
#planet-label-overlay {
    position: fixed;
    inset: 0;
    z-index: 12;
    display: none; /* toggled by setLabelOverlayVisible -- see planetLabelOverlay.js */
    pointer-events: none;
}

/* Real-time 3D City View canvas (design doc §6/§17, webgl-3d-map) -- CityMapInterop.cs/
   wwwroot/citymap.js's own canvas, a FOURTH DOM element alongside #galaxy-map-canvas/
   #system-map-canvas/#planet-map-canvas (all four default to a full-viewport box, only one is ever
   meant to be visible at once -- see CityMapInterop.cs's own doc comment). Same z-index/touch-action/
   display:none-by-default reasoning as the other three map canvases -- see #galaxy-map-canvas's own
   comment for the full "why above #out, why no pointer-events override needed" write-up, identical
   here. */
#city-map-canvas {
    position: fixed;
    inset: 0;
    z-index: 11;
    display: none;
    touch-action: none;
}

/* City View in-3D building-name-label overlay -- a plain DOM sibling of #city-map-canvas,
   positioned ABOVE it (z-index 12 > 11) so labels actually composite over the opaque WebGL canvas,
   same underlying platform constraint #system-label-overlay/#planet-label-overlay above already
   document. Positioned/populated per-frame by wwwroot/cityLabelOverlay.js, never touches citymap.js's
   own WebGL rendering code. */
#city-label-overlay {
    position: fixed;
    inset: 0;
    z-index: 12;
    display: none; /* toggled by setLabelOverlayVisible -- see cityLabelOverlay.js */
    pointer-events: none;
}

/* Real-time 3D Camp View (design doc §4's Stage 0 subsection / §17) -- a FIFTH canvas, sibling of
   #city-map-canvas rather than a level below it (a plot is Empty, then Camp, then City, design doc
   §6). Same fixed/inset/z-index/touch-action shape as every map canvas above; the two are never
   visible at the same time, so they share a z-index rather than needing to stack. */
#camp-map-canvas {
    position: fixed;
    inset: 0;
    z-index: 11;
    display: none;
    touch-action: none;
}

/* Camp View in-3D label overlay -- a plain DOM sibling of #camp-map-canvas, positioned ABOVE it
   (z-index 12 > 11) so labels actually composite over the opaque WebGL canvas, same underlying
   platform constraint every label overlay above already documents. Positioned/populated per-frame by
   wwwroot/campLabelOverlay.js, never touches campview.js's own WebGL rendering code. */
#camp-label-overlay {
    position: fixed;
    inset: 0;
    z-index: 12;
    display: none; /* toggled by setLabelOverlayVisible -- see campLabelOverlay.js */
    pointer-events: none;
}

/* One wreck section's / one piece of camp equipment's own label. Inherits .city-building-label's own
   settled reasoning wholesale -- the plate exists because the backdrop is a per-plot procedural
   TERRAIN mesh spanning Volcanic near-black basalt to Tundra near-white snow and NO flat text colour
   is right on all eight biomes; see that rule's own comment below for the full derivation, which is
   not repeated here.

   WHAT IS GENUINELY NEW: the state modifier classes. Design doc §4 asks that a player be able to see
   which sections are sealed, which are being worked and which are stripped bare, and the 3D scene
   carries most of that in shape and light (rib cage vs plating, work lamps vs dark hull). What a
   silhouette cannot say is WHY a section is shut, so the label says it -- and the four states are
   coloured apart here rather than being left to the text alone, because at a glance a colour reads
   before a word does.

   The four colours are the theme's own existing status grammar, not a new palette: DimColor for the
   default plate, AmberColor for work in progress (the same token the build-in-progress ring in the
   scene uses, and the same one every queued-build readout in this client already uses), TealColor
   for selection, and a deliberately COLDER, dimmer ink for sealed -- sealed has to read as
   unavailable, which is a recession, not an alert. Stripped is dimmest of all: it is finished with,
   and it should stop competing for attention. */
.camp-label {
    position: absolute;
    transform: translate(-50%, calc(-100% - 8px));
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 9px;
    letter-spacing: .02em;
    white-space: nowrap;
    padding: 2px 6px;
    border-radius: 4px;
    background: rgba(10, 14, 22, .78); /* VoidColor, Styles/Theme.axaml */
    border: 1px solid rgba(38, 44, 59, .9); /* LineColor */
    box-shadow: 0 2px 6px rgba(0, 0, 0, .45); /* lifts the plate off a bright biome; invisible on a dark one */
    color: #7C8494; /* DimColor -- see .city-building-label's own comment for why not Dim2Color */
    /* Still none, and now for a stated reason rather than by default (GitHub issue #265 item 2).
       A label must not be a DOM click target: a drag begun on one would reach neither the label nor
       the canvas underneath, and this scene's pan/orbit/zoom and its click-vs-drag distinction all
       live on canvas pointer handlers. What DID change is that "the event goes to the canvas" no
       longer means "the click means whatever happens to be behind the words": campview.js's
       handlePick now hit-tests these boxes first (labelPacker.js's labelIdAtPoint) and selects the
       marker the label names. Before that it selected whatever was nearest the marker ANCHOR, which
       sits a box height plus the 8px below -- i.e. a different object.
       The transform below is therefore load-bearing for picking as well as for looks: change the
       offset and the plate moves, but the hit test moves with it, because it reads the real
       rendered box rather than restating this rule. */
    pointer-events: none;
}

/* A crew is inside RIGHT NOW. The one state that is happening rather than merely being true, and the
   only one the scene animates -- so it is the only one that gets the alert token. */
.camp-label.working {
    color: #F2A93B; /* AmberColor */
    border-color: #8A611F; /* AmberDimColor */
}

/* Shut, and not because anything is wrong: §4 wants sections a commander can see and cannot yet
   reach ("a hold they can see but not yet reach is the foreshadowing that makes it one"). Recessed,
   not flagged. */
.camp-label.sealed {
    color: #4E5666; /* Dim2Color */
    border-color: rgba(38, 44, 59, .7);
    background: rgba(10, 14, 22, .68);
}

/* Finished with. Dimmest of the four, deliberately: a stripped section has nothing left to offer and
   should stop competing with the ones that do. */
.camp-label.stripped {
    color: #4E5666; /* Dim2Color */
    border-color: rgba(38, 44, 59, .55);
    background: rgba(10, 14, 22, .55);
    opacity: .78;
}

/* Selection wins over state, for the same reason it does on every other screen: it is the answer to
   the commander's own most recent action. Last in the cascade so it overrides all three above. */
.camp-label.selected {
    color: #32D6C4; /* TealColor */
    border-color: #1B6E66; /* TealDimColor -- same selected-border grammar as .pickRow.on and friends */
    background: rgba(10, 14, 22, .9);
    opacity: 1;
    z-index: 1; /* Backstop only, same as .city-building-label.selected: campLabelOverlay.js runs the
                   shared measured packing (wwwroot/labelPacker.js), so labels normally do not overlap
                   at all. This still decides the stack in that packer's own last-resort case, where a
                   viewport too small to separate everything falls back to overlapping-but-visible --
                   and the selected label is still the right one to keep whole there. */
}

/* One building slot's own label. This rule used to say it "mirrors .planet-plot-label field-for-field"
   and it did -- which was the bug (fixed 2026-08-08).
   .planet-plot-label sits over an orbital scene whose backdrop is space: near-black, always, so
   Dim2Color plus a dark text-shadow is a genuine match for the mockup's own dim-label-on-dark-panel
   condition there. City View's backdrop is a per-plot procedural TERRAIN mesh, and citymap.js's
   BIOME_PALETTES span Volcanic's near-black basalt (#241C1A) through Plains' mid-green (#4A8545) to
   Tundra's near-white snow (#C7D9DE). A capture of the real shader output at both extremes settles it:
   the identical Dim2 text is crisp on Tundra and all but invisible on Plains. NO flat text colour can
   be right on all eight -- anything bright enough for Plains disappears on Tundra, and vice versa.
   The locked mockup already conceded this in miniature, with its own one-off
   `#moongrid .bldg-node .lbl{ color:var(--ink) }` exception for its single lighter surface; City View
   has eight surfaces plus a water plane plus the starfield the labels cross at the plot's edges, so the
   exception-per-surface approach does not scale.

   So the label gets its own controlled background instead of a new colour, and the tokens stay honest
   rather than nominal: VoidColor plate, LineColor hairline, dim monospace ink -- which is precisely the
   surface/ink pairing the theme was designed around and the mockup's own `.bldg-node .lbl` assumes.

   The ink is DimColor, not Dim2Color, and that is a correction rather than a concession to the plate:
   the mockup's own City View label IS `var(--dim)` (`.bldg-node .lbl`), with `--dim2` reserved for the
   smaller level line beneath it. The Dim2 here came from copying .planet-plot-label -- whose mockup
   counterpart `.plot-node .lbl` really is `--dim2` -- rather than from City View's own rule. The token
   match was to the wrong screen. */
.city-building-label {
    position: absolute;
    transform: translate(-50%, calc(-100% - 8px));
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 9px;
    letter-spacing: .02em;
    white-space: nowrap;
    padding: 2px 6px;
    border-radius: 4px;
    background: rgba(10, 14, 22, .78); /* VoidColor, Styles/Theme.axaml */
    border: 1px solid rgba(38, 44, 59, .9); /* LineColor */
    box-shadow: 0 2px 6px rgba(0, 0, 0, .45); /* lifts the plate off a bright biome; invisible on a dark one */
    color: #7C8494; /* DimColor -- see this rule's own comment for why not Dim2Color */
    /* Still none, and now for a stated reason rather than by default (GitHub issue #277; Camp View
       took the same change first as issue #265 item 2 -- see .camp-label's own comment).
       A label must not be a DOM click target: a drag begun on one would reach neither the label nor
       the canvas underneath, and this scene's pan/orbit/zoom and its click-vs-drag distinction all
       live on canvas pointer handlers. What DID change is that "the event goes to the canvas" no
       longer means "the click means whatever happens to be behind the words": citymap.js's
       handlePick now hit-tests these boxes first (labelPacker.js's labelIdAtPoint) and selects the
       slot the label names. Before that it selected whatever was nearest the marker ANCHOR, which
       sits a box height plus the 8px below -- and since the ten slots sit on one ring, that is
       reliably a ring neighbour rather than the building whose name was clicked.
       The transform above is therefore load-bearing for picking as well as for looks: change the
       offset and the plate moves, but the hit test moves with it, because it reads the real
       rendered box rather than restating this rule. */
    pointer-events: none;
}

.city-building-label.selected {
    color: #32D6C4; /* TealColor */
    border-color: #1B6E66; /* TealDimColor -- same selected-border grammar as .pickRow.on and friends */
    background: rgba(10, 14, 22, .9);
    z-index: 1; /* Kept as a backstop, no longer the mechanism. The ten slot labels sit on one ring
                   and used to genuinely overlap at some camera angles, and this made the selected
                   one the label that stayed whole when they did; since the 2026-08-09 de-collision
                   pass (wwwroot/labelPacker.js) they are measured apart instead and normally do not
                   overlap at all. It still decides the stack in that packer's own last-resort case,
                   where a viewport too small to separate everything falls back to overlapping-but-
                   visible -- and the selected label is still the right one to keep whole there. */
}

/* NOTE (2026-08-09): a `.city-building-label:nth-child(even)` rule used to live here, overriding the
   base transform above to `calc(-100% - 23px)` and so lifting every other ring label a fixed 15px
   clear of its neighbours. Its own comment was honest that it was a STAGGER and not
   collision detection -- it guaranteed ring neighbours never shared a line, but could not know what
   actually overlapped at an arbitrary camera angle, and it moved every other label whether or not
   anything was in its way. It is gone: cityLabelOverlay.js now runs the same real measured packing
   System View does, via the shared wwwroot/labelPacker.js. Do not reintroduce a fixed offset here --
   separating two labels needs their real rendered box, which is exactly what that module measures
   and what a stylesheet rule structurally cannot see. */

/* One plot's own label -- a plain dim readout by default (matching the locked mockup's own
   `.plot-node .lbl` -- Dim2Color, no pill/background), switching to teal when that plot is
   currently selected (mockup's own `.plot-node.selected .lbl`). Deliberately no per-status color
   coding on the label text itself -- the mockup keeps that signal on the pad icon only (see
   planetview.js's own PLOT_STATUS_STYLE), not duplicated onto the label. */
.planet-plot-label {
    position: absolute;
    transform: translate(-50%, calc(-100% - 8px));
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 9px;
    letter-spacing: .02em;
    white-space: nowrap;
    color: #525A6B; /* Dim2Color, Styles/Theme.axaml */
    text-shadow: 0 1px 3px rgba(0, 0, 0, .9);
    /* Still none, and now for a stated reason rather than by default (GitHub issue #277; Camp View
       took the same change first as issue #265 item 2 -- see .camp-label's own comment).
       A label must not be a DOM click target: a drag begun on one would reach neither the label nor
       the canvas underneath, and this scene's pan/orbit/zoom and its click-vs-drag distinction all
       live on canvas pointer handlers. What DID change is that "the event goes to the canvas" no
       longer means "the click means whatever happens to be behind the words": planetview.js's
       handlePick now hit-tests these boxes first (labelPacker.js's labelIdAtPoint) and selects the
       plot or moon the label names. Before that it went straight to pickPlotAt/pickMoonAt, which
       measure from each marker's own projected centre -- a line height plus the 8px below.
       Note this rule styles the MOON labels too, not just plots (planetview.js's getLabelPositions
       emits both onto this one overlay); the class name predates them.
       The transform above is therefore load-bearing for picking as well as for looks: change the
       offset and the label moves, but the hit test moves with it, because it reads the real
       rendered box rather than restating this rule. */
    pointer-events: none;
}

.planet-plot-label.selected {
    color: #32D6C4; /* TealColor */
}

/* ---------------------------------------------------------------------------------------------
   Boot fallback -- the screen a player gets when the game cannot start (wwwroot/bootFallback.js,
   plus index.html's <noscript> twin, which reuses these exact classes).

   Written mobile-first and self-contained on purpose. It renders precisely when the Avalonia
   client did NOT load, so it can borrow nothing from Theme.axaml -- the palette below repeats the
   same four literals the splash above already inlines (VoidColor/InkColor/TealColor/DimColor)
   rather than referencing tokens that live inside the thing that failed to start.

   z-index 100 clears the entire map stack (#out is 10, the five map canvases 11, the label
   overlays 12). A failure screen underneath a stale WebGL canvas would be invisible, and the
   canvases are plain DOM siblings that Avalonia's own teardown never touches.
   --------------------------------------------------------------------------------------------- */
.boot-fallback {
    position: fixed;
    inset: 0;
    z-index: 100;
    background: #0A0E16; /* VoidColor */
    display: flex;
    justify-content: center;
    align-items: center;
    /* body carries overflow:hidden for the map scenes. Without its own scrolling, a long message on
       a small phone in landscape is clipped with no way to reach the reload button. */
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    /* Respect notches/home indicators -- this screen is most likely to be seen on a phone. */
    padding: max(24px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right))
             max(24px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
    box-sizing: border-box;
}

.boot-fallback-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 18px;
    max-width: 34rem;
    margin: auto; /* keeps it centred once the panel is taller than the viewport and scrolls */
}

.boot-fallback-heading {
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 1.1rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    color: #E8EAED; /* InkColor */
    margin: 6px 0 0 0;
    line-height: 1.45;
}

/* Body copy drops the monospace the wordmark and heading keep. The brand face is right for a short
   status line ("ESTABLISHING UPLINK...") and works against a paragraph someone has to actually read
   on a phone -- this is the one screen where being understood beats being on-brand. */
.boot-fallback-body,
.boot-fallback-detail {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    margin: 0;
    line-height: 1.6;
    /* 16px floor: readable, and it also avoids mobile Safari's auto-zoom-on-focus quirk that the
       .avalonia-input-element rule above documents. */
    font-size: 1rem;
}

.boot-fallback-body {
    color: #E8EAED; /* InkColor */
}

.boot-fallback-detail {
    color: #7C8494; /* DimColor */
    font-size: 0.9375rem;
}

.boot-fallback-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
    margin-top: 6px;
}

.boot-fallback-button {
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 0.875rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: #0A0E16; /* VoidColor -- reads on the teal fill */
    background: #32D6C4; /* TealColor */
    border: none;
    border-radius: 4px;
    /* >= 44px tall including padding: the iOS/Android minimum comfortable touch target. This button
       is the player's only way out of a dead page, so it must not be fiddly to hit. */
    min-height: 44px;
    padding: 12px 28px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.boot-fallback-button:hover {
    filter: brightness(1.1);
}

.boot-fallback-button:focus-visible {
    outline: 2px solid #E8EAED; /* InkColor */
    outline-offset: 3px;
}

/* The soft first-stage watchdog line, shown under the splash spinner at 20s. Deliberately styled as
   ordinary dim status text, NOT as a warning -- at this point nothing has gone wrong and the load is
   very possibly still progressing. */
.splash-slow-notice {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    font-size: 0.8125rem;
    line-height: 1.5;
    color: #7C8494; /* DimColor */
    text-align: center;
    max-width: 26rem;
    margin-top: 14px;
    padding: 0 20px;
}

/* Non-blocking WebGL2 warning. A separate, dismissible bar rather than a takeover, because the
   client genuinely works without WebGL2 -- only the 3D scenes don't. See bootFallback.js's
   checkRequired() for why this is not treated as a hard blocker. */
.boot-warning {
    position: fixed;
    z-index: 100;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    justify-content: center;
    background: #141A26;
    border-top: 1px solid #32D6C4; /* TealColor */
    padding: 12px max(16px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom))
                 max(16px, env(safe-area-inset-left));
    box-sizing: border-box;
}

.boot-warning-text {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    font-size: 0.875rem;
    line-height: 1.5;
    color: #E8EAED; /* InkColor */
    max-width: 44rem;
}

.boot-warning-dismiss {
    font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: #32D6C4; /* TealColor */
    background: transparent;
    border: 1px solid #32D6C4; /* TealColor */
    border-radius: 4px;
    min-height: 44px;
    padding: 8px 18px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.boot-warning-dismiss:focus-visible {
    outline: 2px solid #E8EAED; /* InkColor */
    outline-offset: 3px;
}

/* Phones in portrait: the wordmark's heavy letter-spacing overflows a narrow viewport, and the
   warning bar reads better stacked than squeezed onto one line.

   NOT scoped to .boot-fallback, and that is a fix rather than a detail. At 1.75rem with 0.35em
   tracking the wordmark is wider than a 375px phone, so the SPLASH -- the first screen every mobile
   player sees, long before any of this fallback work -- rendered as "ROJECT MAELSTRO" with the P and
   the M clipped off either side. Caught by looking at a screenshot of the warning-bar case; the
   automated overflow assertion had only ever run against the fallback panel, which replaces the
   splash and so could never have shown it. */
@media (max-width: 30rem) {
    .splash-wordmark {
        font-size: 1.25rem;
        letter-spacing: 0.22em;
        padding-right: 0.22em;
    }

    .boot-fallback-button {
        width: 100%;
    }

    .boot-warning {
        flex-direction: column;
        align-items: stretch;
        text-align: center;
    }
}

/* Someone who has asked the OS for less motion should not be shown a pulsing dot while they wait. */
@media (prefers-reduced-motion: reduce) {
    .splash-spinner {
        animation: none;
        opacity: 0.8;
    }
}
